Skip to content

Commit 0a638a9

Browse files
authored
Injecting Node-API functions into weak-node-api (#49)
* Update weak_node_api to use injection * Improve the search for Android .so files * Injecting when loading in iOS * Injecting on Android * Consuming the weak-node-api prebuild * Link Android libs from Ferric with weak-node-api * Strip lib from xcframework and android library directories * Strip "lib" prefix from prebuild directory names * Add cross-platform logging * Load weak-node-api dynamically when injecting * Remove extraneous doNotStrip * Escape weakNodeApiPath * More async fs I/O in cmake CLI * Rename inject_host_t to InjectHostFunction * Rename inject_host to inject_weak_node_api_host, mark it extern "C" and move it out of namespace * Error on warnings when building weak-node-api * Propagating __attribute__((noreturn)) from headers * Generate the injector on CI
1 parent 74ab040 commit 0a638a9

File tree

31 files changed

+583
-360
lines changed

31 files changed

+583
-360
lines changed

.github/workflows/check.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ jobs:
5050
- run: npm run build
5151
- run: npm run copy-node-api-headers --workspace react-native-node-api-modules
5252
- run: npm run build-weak-node-api --workspace react-native-node-api-modules
53+
- run: npm run generate-weak-node-api-injector --workspace react-native-node-api-modules
5354
- run: npm test --workspace react-native-node-addon-examples

packages/ferric-example/.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ Cargo.lock
55
/*.android.node/
66

77
# Generated files
8-
/libferric_example.d.ts
9-
/libferric_example.js
8+
/ferric_example.d.ts
9+
/ferric_example.js

packages/ferric-example/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"url": "git+https://github.com/callstackincubator/react-native-node-api-modules.git",
99
"directory": "packages/ferric-example"
1010
},
11-
"main": "libferric_example.js",
12-
"types": "libferric_example.d.ts",
11+
"main": "ferric_example.js",
12+
"types": "ferric_example.d.ts",
1313
"scripts": {
1414
"build": "ferric build --android --apple"
1515
},

packages/ferric/src/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
determineXCFrameworkFilename,
1515
createXCframework,
1616
createUniversalAppleLibrary,
17-
determineLibraryFilename,
17+
determineLibraryBasename,
1818
prettyPath,
1919
} from "react-native-node-api-modules";
2020

@@ -146,7 +146,7 @@ export const buildCommand = new Command("build")
146146
}
147147
);
148148

149-
const libraryName = determineLibraryFilename([
149+
const libraryName = determineLibraryBasename([
150150
...androidLibraries.map(([, outputPath]) => outputPath),
151151
]);
152152

packages/ferric/src/cargo.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
isAppleTarget,
1515
} from "./targets.js";
1616

17+
const WEAK_NODE_API_PATH = new URL(
18+
import.meta.resolve("react-native-node-api-modules/weak-node-api")
19+
).pathname;
20+
1721
const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
1822
"aarch64-apple-darwin": "macos-arm64_x86_64", // Universal
1923
"x86_64-apple-darwin": "macos-arm64_x86_64", // Universal
@@ -28,6 +32,13 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
2832
// "aarch64-apple-visionos-sim": "xros-arm64-simulator",
2933
};
3034

35+
const ANDROID_ARCH_PR_TARGET: Record<AndroidTargetName, string> = {
36+
"aarch64-linux-android": "arm64-v8a",
37+
"armv7-linux-androideabi": "armeabi-v7a",
38+
"i686-linux-android": "x86",
39+
"x86_64-linux-android": "x86_64",
40+
};
41+
3142
export function joinPathAndAssertExistence(...pathSegments: string[]) {
3243
const joinedPath = path.join(...pathSegments);
3344
assert(fs.existsSync(joinedPath), `Expected ${joinedPath} to exist`);
@@ -114,17 +125,23 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) {
114125
}
115126

116127
export function getWeakNodeApiFrameworkPath(target: AppleTargetName) {
117-
const weakNodeApiPath = new URL(
118-
import.meta.resolve("react-native-node-api-modules/weak-node-api")
119-
).pathname;
120-
assert(fs.existsSync(weakNodeApiPath), "Expected weak-node-api to exist");
128+
assert(fs.existsSync(WEAK_NODE_API_PATH), "Expected weak-node-api to exist");
121129
return joinPathAndAssertExistence(
122-
weakNodeApiPath,
123-
"libweak-node-api.xcframework",
130+
WEAK_NODE_API_PATH,
131+
"weak-node-api.xcframework",
124132
APPLE_XCFRAMEWORK_CHILDS_PER_TARGET[target]
125133
);
126134
}
127135

136+
export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) {
137+
assert(fs.existsSync(WEAK_NODE_API_PATH), "Expected weak-node-api to exist");
138+
return joinPathAndAssertExistence(
139+
WEAK_NODE_API_PATH,
140+
"weak-node-api.android.node",
141+
ANDROID_ARCH_PR_TARGET[target]
142+
);
143+
}
144+
128145
export function getTargetEnvironmentVariables({
129146
target,
130147
ndkVersion,
@@ -149,8 +166,15 @@ export function getTargetEnvironmentVariables({
149166
const toolchainBinPath = getLLVMToolchainBinPath(ndkPath);
150167
const targetArch = getTargetAndroidArch(target);
151168
const targetPlatform = getTargetAndroidPlatform(target);
169+
const weakNodeApiPath = getWeakNodeApiAndroidLibraryPath(target);
152170

153171
return {
172+
CARGO_ENCODED_RUSTFLAGS: [
173+
"-L",
174+
weakNodeApiPath,
175+
"-l",
176+
"weak-node-api",
177+
].join(String.fromCharCode(0x1f)),
154178
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: joinPathAndAssertExistence(
155179
toolchainBinPath,
156180
`aarch64-linux-android${androidApiLevel}-clang`
@@ -186,7 +210,12 @@ export function getTargetEnvironmentVariables({
186210
} else if (isAppleTarget(target)) {
187211
const weakNodeApiFrameworkPath = getWeakNodeApiFrameworkPath(target);
188212
return {
189-
RUSTFLAGS: `-L framework=${weakNodeApiFrameworkPath} -l framework=libweak-node-api`,
213+
CARGO_ENCODED_RUSTFLAGS: [
214+
"-L",
215+
`framework=${weakNodeApiFrameworkPath}`,
216+
"-l",
217+
"framework=weak-node-api",
218+
].join(String.fromCharCode(0x1f)),
190219
};
191220
} else {
192221
throw new Error(`Unexpected target: ${target}`);

packages/react-native-node-api-cmake/src/cli.ts

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import assert from "node:assert/strict";
22
import path from "node:path";
3-
import fs from "node:fs/promises";
4-
import { existsSync, readdirSync, renameSync } from "node:fs";
3+
import fs from "node:fs";
54
import { EventEmitter } from "node:events";
65

76
import { Command, Option } from "@commander-js/extra-typings";
@@ -109,7 +108,7 @@ export const program = new Command("react-native-node-api-cmake")
109108
try {
110109
const buildPath = getBuildPath(globalContext);
111110
if (globalContext.clean) {
112-
await fs.rm(buildPath, { recursive: true, force: true });
111+
await fs.promises.rm(buildPath, { recursive: true, force: true });
113112
}
114113
const triplets = new Set<SupportedTriplet>(tripletValues);
115114
if (globalContext.apple) {
@@ -162,7 +161,7 @@ export const program = new Command("react-native-node-api-cmake")
162161
tripletContext.map(async (context) => {
163162
// Delete any stale build artifacts before building
164163
// This is important, since we might rename the output files
165-
await fs.rm(context.tripletOutputPath, {
164+
await fs.promises.rm(context.tripletOutputPath, {
166165
recursive: true,
167166
force: true,
168167
});
@@ -181,32 +180,37 @@ export const program = new Command("react-native-node-api-cmake")
181180
isAppleTriplet(triplet)
182181
);
183182
if (appleTriplets.length > 0) {
184-
const libraryPaths = appleTriplets.flatMap(({ tripletOutputPath }) => {
185-
const configSpecificPath = path.join(
186-
tripletOutputPath,
187-
globalContext.configuration
188-
);
189-
assert(
190-
existsSync(configSpecificPath),
191-
`Expected a directory at ${configSpecificPath}`
192-
);
193-
// Expect binary file(s), either .node or .dylib
194-
return readdirSync(configSpecificPath).map((file) => {
195-
const filePath = path.join(configSpecificPath, file);
196-
if (filePath.endsWith(".dylib")) {
197-
return filePath;
198-
} else if (file.endsWith(".node")) {
199-
// Rename the file to .dylib for xcodebuild to accept it
200-
const newFilePath = filePath.replace(/\.node$/, ".dylib");
201-
renameSync(filePath, newFilePath);
202-
return newFilePath;
203-
} else {
204-
throw new Error(
205-
`Expected a .node or .dylib file, but found ${file}`
206-
);
207-
}
208-
});
209-
});
183+
const libraryPaths = await Promise.all(
184+
appleTriplets.map(async ({ tripletOutputPath }) => {
185+
const configSpecificPath = path.join(
186+
tripletOutputPath,
187+
globalContext.configuration
188+
);
189+
assert(
190+
fs.existsSync(configSpecificPath),
191+
`Expected a directory at ${configSpecificPath}`
192+
);
193+
// Expect binary file(s), either .node or .dylib
194+
const files = await fs.promises.readdir(configSpecificPath);
195+
const result = files.map(async (file) => {
196+
const filePath = path.join(configSpecificPath, file);
197+
if (filePath.endsWith(".dylib")) {
198+
return filePath;
199+
} else if (file.endsWith(".node")) {
200+
// Rename the file to .dylib for xcodebuild to accept it
201+
const newFilePath = filePath.replace(/\.node$/, ".dylib");
202+
await fs.promises.rename(filePath, newFilePath);
203+
return newFilePath;
204+
} else {
205+
throw new Error(
206+
`Expected a .node or .dylib file, but found ${file}`
207+
);
208+
}
209+
});
210+
assert.equal(result.length, 1, "Expected exactly one library file");
211+
return await result[0];
212+
})
213+
);
210214
const frameworkPaths = libraryPaths.map(createAppleFramework);
211215
const xcframeworkFilename =
212216
determineXCFrameworkFilename(frameworkPaths);
@@ -239,25 +243,32 @@ export const program = new Command("react-native-node-api-cmake")
239243
);
240244
if (androidTriplets.length > 0) {
241245
const libraryPathByTriplet = Object.fromEntries(
242-
androidTriplets.map(({ tripletOutputPath, triplet }) => {
243-
assert(
244-
existsSync(tripletOutputPath),
245-
`Expected a directory at ${tripletOutputPath}`
246-
);
247-
// Expect binary file(s), either .node or .so
248-
const result = readdirSync(tripletOutputPath).map((file) => {
249-
const filePath = path.join(tripletOutputPath, file);
250-
if (file.endsWith(".so") || file.endsWith(".node")) {
251-
return filePath;
252-
} else {
253-
throw new Error(
254-
`Expected a .node or .so file, but found ${file}`
255-
);
256-
}
257-
});
258-
assert.equal(result.length, 1, "Expected exactly library file");
259-
return [triplet, result[0]] as const;
260-
})
246+
await Promise.all(
247+
androidTriplets.map(async ({ tripletOutputPath, triplet }) => {
248+
assert(
249+
fs.existsSync(tripletOutputPath),
250+
`Expected a directory at ${tripletOutputPath}`
251+
);
252+
// Expect binary file(s), either .node or .so
253+
const dirents = await fs.promises.readdir(tripletOutputPath, {
254+
withFileTypes: true,
255+
});
256+
const result = dirents
257+
.filter(
258+
(dirent) =>
259+
dirent.isFile() &&
260+
(dirent.name.endsWith(".so") ||
261+
dirent.name.endsWith(".node"))
262+
)
263+
.map((dirent) => path.join(dirent.parentPath, dirent.name));
264+
assert.equal(
265+
result.length,
266+
1,
267+
"Expected exactly one library file"
268+
);
269+
return [triplet, result[0]] as const;
270+
})
271+
)
261272
) as Record<AndroidTriplet, string>;
262273
const androidLibsFilename = determineAndroidLibsFilename(
263274
Object.values(libraryPathByTriplet)

packages/react-native-node-api-cmake/src/weak-node-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
1717
);
1818
assert(fs.existsSync(pathname), "Weak Node API path does not exist");
1919
if (isAppleTriplet(triplet)) {
20-
const xcframeworkPath = path.join(pathname, "libweak-node-api.xcframework");
20+
const xcframeworkPath = path.join(pathname, "weak-node-api.xcframework");
2121
assert(
2222
fs.existsSync(xcframeworkPath),
2323
`Expected an XCFramework at ${xcframeworkPath}`
@@ -26,7 +26,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
2626
} else if (isAndroidTriplet(triplet)) {
2727
const libraryPath = path.join(
2828
pathname,
29-
"libweak-node-api.android.node",
29+
"weak-node-api.android.node",
3030
ANDROID_ARCHITECTURES[triplet],
3131
"libweak-node-api.so"
3232
);

packages/react-native-node-api-modules/.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ android/.cxx/
1717
android/build/
1818

1919
# Everything in weak-node-api is generated, except for the configurations
20+
# Generated and built bia `npm run build-weak-node-api-injector`
2021
/weak-node-api/build/
2122
/weak-node-api/*.xcframework
2223
/weak-node-api/*.android.node
23-
/weak-node-api/weak-node-api.cpp
24+
/weak-node-api/weak_node_api.cpp
25+
/weak-node-api/weak_node_api.hpp
26+
# Generated via `npm run generate-weak-node-api-injector`
27+
/cpp/WeakNodeApiInjector.cpp

packages/react-native-node-api-modules/android/CMakeLists.txt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,35 @@ cmake_minimum_required(VERSION 3.13)
33
project(react-native-node-api-modules)
44
set(CMAKE_CXX_STANDARD 20)
55

6+
find_package(ReactAndroid REQUIRED CONFIG)
7+
find_package(hermes-engine REQUIRED CONFIG)
8+
9+
add_library(weak-node-api SHARED IMPORTED)
10+
set_target_properties(weak-node-api PROPERTIES
11+
IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/../weak-node-api/weak-node-api.android.node/${ANDROID_ABI}/libweak-node-api.so"
12+
)
13+
target_include_directories(weak-node-api INTERFACE
14+
../weak-node-api
15+
../weak-node-api/include
16+
)
17+
618
add_library(node-api-host SHARED
719
src/main/cpp/OnLoad.cpp
20+
../cpp/Logger.cpp
821
../cpp/CxxNodeApiHostModule.cpp
22+
../cpp/WeakNodeApiInjector.cpp
923
)
1024

1125
target_include_directories(node-api-host PRIVATE
1226
../cpp
13-
../include
1427
)
1528

16-
find_package(ReactAndroid REQUIRED CONFIG)
17-
find_package(hermes-engine REQUIRED CONFIG)
18-
1929
target_link_libraries(node-api-host
2030
# android
31+
log
2132
ReactAndroid::reactnative
2233
ReactAndroid::jsi
2334
hermes-engine::libhermes
35+
weak-node-api
2436
# react_codegen_NodeApiHostSpec
2537
)
26-
27-
add_subdirectory(../weak-node-api weak-node-api)
28-
29-
target_compile_definitions(weak-node-api
30-
PRIVATE
31-
# NAPI_VERSION=8
32-
NODE_API_REEXPORT=1
33-
)
34-
target_link_libraries(weak-node-api
35-
node-api-host
36-
hermes-engine::libhermes
37-
)

packages/react-native-node-api-modules/android/build.gradle

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ android {
6565

6666
externalNativeBuild {
6767
cmake {
68-
targets "node-api-host", "weak-node-api"
68+
targets "node-api-host"
6969
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
7070
arguments "-DANDROID_STL=c++_shared"
7171
abiFilters (*reactNativeArchitectures())
@@ -117,12 +117,6 @@ android {
117117
sourceCompatibility JavaVersion.VERSION_1_8
118118
targetCompatibility JavaVersion.VERSION_1_8
119119
}
120-
121-
packagingOptions {
122-
// TODO: I don't think this works - I needed to add this in the react-native package too.
123-
doNotStrip "**/libhermes.so"
124-
}
125-
126120
}
127121

128122
repositories {

0 commit comments

Comments
 (0)