Skip to content

Commit ff34c45

Browse files
kraenhansenCopilot
andauthored
Explicit weak-node-api linkage (#249)
* Expose includable WEAK_NODE_API_CONFIG to CMake projects * Use the `include(${WEAK_NODE_API_CONFIG})` syntax in our own tests * Turn define arguments into arrays of objects with key-value-pair * Add general documentation on weak-node-api * Make injection of CMAKE_JS_* variables opt-in and provide our own explicit way of linking with Node-API * Add --weak-node-api option to gyp-to-cmake * Use weak-node-api linkage in node-addon-examples * Opt into cmake-js compatibility in node-tests * Fixed bug passing type through defines * Fixed bug when calling cmake-rn in node-tests * Fix missing set_target_properties of .node suffix * Apply suggestion from @Copilot to documentation * Use [] instead of Array * Push into an array instead of creating a new on every --define * Update packages/gyp-to-cmake/src/transformer.ts Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 172ca06 commit ff34c45

File tree

14 files changed

+223
-100
lines changed

14 files changed

+223
-100
lines changed

.changeset/cold-symbols-refuse.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gyp-to-cmake": minor
3+
---
4+
5+
Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage.

.changeset/open-ducks-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"cmake-rn": minor
3+
---
4+
5+
Breaking: `CMAKE_JS_*` defines are no longer injected by default (use --cmake-js to opt-in)

.changeset/three-colts-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"cmake-rn": minor
3+
---
4+
5+
Expose includable WEAK_NODE_API_CONFIG to CMake projects

docs/WEAK-NODE-API.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# The `weak-node-api` library
2+
3+
Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`.
4+
5+
The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host.
6+
7+
While technically not a requirement on non-Android platforms, we choose to make this the general approach across React Native platforms. This keeps things aligned across platforms, while exposing just the Node-API without forcing libraries to build with suppression of errors for undefined symbols.

packages/cmake-rn/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,24 @@
33
A wrapper around Cmake making it easier to produce prebuilt binaries targeting iOS and Android matching [the prebuilt binary specification](https://github.com/callstackincubator/react-native-node-api/blob/main/docs/PREBUILDS.md).
44

55
Serves the same purpose as `cmake-js` does for the Node.js community and could potentially be upstreamed into `cmake-js` eventually.
6+
7+
## Linking against Node-API
8+
9+
Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`.
10+
11+
The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host.
12+
13+
To link against `weak-node-api` just include the CMake config exposed through `WEAK_NODE_API_CONFIG` and add `weak-node-api` to the `target_link_libraries` of the addon's library target.
14+
15+
```cmake
16+
cmake_minimum_required(VERSION 3.15...3.31)
17+
project(tests-buffers)
18+
19+
include(${WEAK_NODE_API_CONFIG})
20+
21+
add_library(addon SHARED addon.c)
22+
target_link_libraries(addon PRIVATE weak-node-api)
23+
target_compile_features(addon PRIVATE cxx_std_20)
24+
```
25+
26+
This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`.

packages/cmake-rn/src/cli.ts

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
} from "@react-native-node-api/cli-utils";
1515
import { isSupportedTriplet } from "react-native-node-api";
1616

17-
import { getWeakNodeApiVariables } from "./weak-node-api.js";
17+
import {
18+
getCmakeJSVariables,
19+
getWeakNodeApiVariables,
20+
} from "./weak-node-api.js";
1821
import {
1922
platforms,
2023
allTriplets as allTriplets,
@@ -85,8 +88,8 @@ const outPathOption = new Option(
8588
const defineOption = new Option(
8689
"-D,--define <entry...>",
8790
"Define cache variables passed when configuring projects",
88-
).argParser<Record<string, string | CmakeTypedDefinition>>(
89-
(input, previous = {}) => {
91+
)
92+
.argParser<Record<string, string>[]>((input, previous = []) => {
9093
// TODO: Implement splitting of value using a regular expression (using named groups) for the format <var>[:<type>]=<value>
9194
// and return an object keyed by variable name with the string value as value or alternatively an array of [value, type]
9295
const match = input.match(
@@ -98,9 +101,10 @@ const defineOption = new Option(
98101
);
99102
}
100103
const { name, type, value } = match.groups;
101-
return { ...previous, [name]: type ? { value, type } : value };
102-
},
103-
);
104+
previous.push({ [type ? `${name}:${type}` : name]: value });
105+
return previous;
106+
})
107+
.default([]);
104108

105109
const targetOption = new Option(
106110
"--target <target...>",
@@ -117,6 +121,11 @@ const noWeakNodeApiLinkageOption = new Option(
117121
"Don't pass the path of the weak-node-api library from react-native-node-api",
118122
);
119123

124+
const cmakeJsOption = new Option(
125+
"--cmake-js",
126+
"Define CMAKE_JS_* variables used for compatibility with cmake-js",
127+
).default(false);
128+
120129
let program = new Command("cmake-rn")
121130
.description("Build React Native Node API modules with CMake")
122131
.addOption(tripletOption)
@@ -129,7 +138,8 @@ let program = new Command("cmake-rn")
129138
.addOption(cleanOption)
130139
.addOption(targetOption)
131140
.addOption(noAutoLinkOption)
132-
.addOption(noWeakNodeApiLinkageOption);
141+
.addOption(noWeakNodeApiLinkageOption)
142+
.addOption(cmakeJsOption);
133143

134144
for (const platform of platforms) {
135145
const allOption = new Option(
@@ -296,19 +306,26 @@ async function configureProject<T extends string>(
296306
options: BaseOpts,
297307
) {
298308
const { triplet, buildPath, outputPath } = context;
299-
const { verbose, source, weakNodeApiLinkage } = options;
309+
const { verbose, source, weakNodeApiLinkage, cmakeJs } = options;
310+
311+
// TODO: Make the two following definitions a part of the platform definition
300312

301313
const nodeApiDefinitions =
302314
weakNodeApiLinkage && isSupportedTriplet(triplet)
303-
? getWeakNodeApiVariables(triplet)
304-
: // TODO: Make this a part of the platform definition
305-
{};
315+
? [getWeakNodeApiVariables(triplet)]
316+
: [];
306317

307-
const definitions = {
318+
const cmakeJsDefinitions =
319+
cmakeJs && isSupportedTriplet(triplet)
320+
? [getCmakeJSVariables(triplet)]
321+
: [];
322+
323+
const definitions = [
308324
...nodeApiDefinitions,
325+
...cmakeJsDefinitions,
309326
...options.define,
310-
CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath,
311-
};
327+
{ CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath },
328+
];
312329

313330
await spawn(
314331
"cmake",
@@ -352,18 +369,13 @@ async function buildProject<T extends string>(
352369
);
353370
}
354371

355-
type CmakeTypedDefinition = { value: string; type: string };
356-
357-
function toDefineArguments(
358-
declarations: Record<string, string | CmakeTypedDefinition>,
359-
) {
360-
return Object.entries(declarations).flatMap(([key, definition]) => {
361-
if (typeof definition === "string") {
362-
return ["-D", `${key}=${definition}`];
363-
} else {
364-
return ["-D", `${key}:${definition.type}=${definition.value}`];
365-
}
366-
});
372+
function toDefineArguments(declarations: Array<Record<string, string>>) {
373+
return declarations.flatMap((values) =>
374+
Object.entries(values).flatMap(([key, definition]) => [
375+
"-D",
376+
`${key}=${definition}`,
377+
]),
378+
);
367379
}
368380

369381
export { program };

packages/cmake-rn/src/weak-node-api.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,36 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
4141
}
4242
}
4343

44-
export function getWeakNodeApiVariables(triplet: SupportedTriplet) {
44+
function getNodeApiIncludePaths() {
4545
const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()];
4646
for (const includePath of includePaths) {
4747
assert(
4848
!includePath.includes(";"),
4949
`Include path with a ';' is not supported: ${includePath}`,
5050
);
5151
}
52+
return includePaths;
53+
}
54+
55+
export function getWeakNodeApiVariables(
56+
triplet: SupportedTriplet,
57+
): Record<string, string> {
58+
return {
59+
// Expose an includable CMake config file declaring the weak-node-api target
60+
WEAK_NODE_API_CONFIG: path.join(weakNodeApiPath, "weak-node-api.cmake"),
61+
WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"),
62+
WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet),
63+
};
64+
}
65+
66+
/**
67+
* For compatibility with cmake-js
68+
*/
69+
export function getCmakeJSVariables(
70+
triplet: SupportedTriplet,
71+
): Record<string, string> {
5272
return {
53-
CMAKE_JS_INC: includePaths.join(";"),
73+
CMAKE_JS_INC: getNodeApiIncludePaths().join(";"),
5474
CMAKE_JS_LIB: getWeakNodeApiPath(triplet),
5575
};
5676
}

packages/gyp-to-cmake/src/cli.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,38 @@ export const program = new Command("gyp-to-cmake")
6262
"--no-path-transforms",
6363
"Don't transform output from command expansions (replacing '\\' with '/')",
6464
)
65+
.option("--weak-node-api", "Link against the weak-node-api library", false)
66+
.option("--define-napi-version", "Define NAPI_VERSION for all targets", false)
67+
.option("--cpp <version>", "C++ standard version", "17")
6568
.argument(
6669
"[path]",
6770
"Path to the binding.gyp file or directory to traverse recursively",
6871
process.cwd(),
6972
)
7073
.action(
71-
wrapAction((targetPath: string, { pathTransforms }) => {
72-
const options: TransformOptions = {
73-
unsupportedBehaviour: "throw",
74-
disallowUnknownProperties: false,
75-
transformWinPathsToPosix: pathTransforms,
76-
};
77-
const stat = fs.statSync(targetPath);
78-
if (stat.isFile()) {
79-
transformBindingGypFile(targetPath, options);
80-
} else if (stat.isDirectory()) {
81-
transformBindingGypsRecursively(targetPath, options);
82-
} else {
83-
throw new Error(`Expected either a file or a directory: ${targetPath}`);
84-
}
85-
}),
74+
wrapAction(
75+
(
76+
targetPath: string,
77+
{ pathTransforms, cpp, defineNapiVersion, weakNodeApi },
78+
) => {
79+
const options: TransformOptions = {
80+
unsupportedBehaviour: "throw",
81+
disallowUnknownProperties: false,
82+
transformWinPathsToPosix: pathTransforms,
83+
compileFeatures: cpp ? [`cxx_std_${cpp}`] : [],
84+
defineNapiVersion,
85+
weakNodeApi,
86+
};
87+
const stat = fs.statSync(targetPath);
88+
if (stat.isFile()) {
89+
transformBindingGypFile(targetPath, options);
90+
} else if (stat.isDirectory()) {
91+
transformBindingGypsRecursively(targetPath, options);
92+
} else {
93+
throw new Error(
94+
`Expected either a file or a directory: ${targetPath}`,
95+
);
96+
}
97+
},
98+
),
8699
);

0 commit comments

Comments
 (0)