Skip to content

Commit 522421d

Browse files
authored
Auto link on Android (#39)
* Refactor to prepare to Android commands * WIP Add linking of Android modules * Extend weak-node-api to defer calls * Build examples for Android * Update libweak-node-api location * Add weak-node-api to TurboModule * Drive autolink from Gradle * Fix library path when loading on Android
1 parent 4afa6ef commit 522421d

File tree

23 files changed

+1142
-925
lines changed

23 files changed

+1142
-925
lines changed

packages/node-addon-examples/scripts/build-examples.mts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ const projectDirectories = findCMakeProjects();
66

77
for (const projectDirectory of projectDirectories) {
88
console.log(`Running "react-native-node-api-cmake" in ${projectDirectory}`);
9-
execSync("react-native-node-api-cmake --triplet arm64-apple-ios-sim", {
10-
cwd: projectDirectory,
11-
stdio: "inherit",
12-
});
9+
execSync(
10+
"react-native-node-api-cmake --triplet aarch64-linux-android --triplet arm64-apple-ios-sim",
11+
{
12+
cwd: projectDirectory,
13+
stdio: "inherit",
14+
}
15+
);
1316
console.log();
1417
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export async function createAndroidLibsDirectory({
135135
const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet];
136136
const archOutputPath = path.join(outputPath, arch);
137137
await fs.promises.mkdir(archOutputPath, { recursive: true });
138-
const libraryName = path.basename(libraryPath, path.extname(libraryPath));
138+
const libraryName = path.basename(libraryPath);
139139
const libraryOutputPath = path.join(archOutputPath, libraryName);
140140
await fs.promises.copyFile(libraryPath, libraryOutputPath);
141141
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
1616
);
1717
assert(fs.existsSync(pathname), "Weak Node API path does not exist");
1818
if (isAppleTriplet(triplet)) {
19-
const xcframeworkPath = path.join(pathname, "weak-node-api.xcframework");
19+
const xcframeworkPath = path.join(pathname, "libweak-node-api.xcframework");
2020
assert(
2121
fs.existsSync(xcframeworkPath),
2222
`Expected an XCFramework at ${xcframeworkPath}`
@@ -25,9 +25,9 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
2525
} else if (isAndroidTriplet(triplet)) {
2626
const libraryPath = path.join(
2727
pathname,
28-
"weak-node-api.android.node",
28+
"libweak-node-api.android.node",
2929
ANDROID_ARCHITECTURES[triplet],
30-
"weak-node-api"
30+
"libweak-node-api.so"
3131
);
3232
assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`);
3333
return libraryPath;

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ include/
1010
**/android/build/
1111

1212
# iOS build artifacts
13-
xcframeworks/
13+
/auto-linked/
1414

1515
# Android build artifacts
1616
android/.cxx/
1717
android/build/
1818

1919
# Everything in weak-node-api is generated, except for the configurations
20-
weak-node-api/build/
21-
weak-node-api/weak-node-api.xcframework
22-
weak-node-api/weak-node-api.android.node
23-
weak-node-api.cpp
20+
/weak-node-api/build/
21+
/weak-node-api/*.xcframework
22+
/weak-node-api/*.android.node
23+
/weak-node-api/weak-node-api.cpp

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@ target_include_directories(node-api-host PRIVATE
1616
find_package(ReactAndroid REQUIRED CONFIG)
1717
find_package(hermes-engine REQUIRED CONFIG)
1818

19-
2019
target_link_libraries(node-api-host
2120
# android
2221
ReactAndroid::reactnative
2322
ReactAndroid::jsi
2423
hermes-engine::libhermes
2524
# react_codegen_NodeApiHostSpec
2625
)
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: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import java.nio.file.Paths
2+
import groovy.json.JsonSlurper
23

34
buildscript {
45
ext.getExtOrDefault = {name ->
@@ -64,7 +65,7 @@ android {
6465

6566
externalNativeBuild {
6667
cmake {
67-
targets "node-api-host"
68+
targets "node-api-host", "weak-node-api"
6869
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
6970
arguments "-DANDROID_STL=c++_shared"
7071
abiFilters (*reactNativeArchitectures())
@@ -122,20 +123,6 @@ android {
122123
doNotStrip "**/libhermes.so"
123124
}
124125

125-
// sourceSets {
126-
// main {
127-
// jniLibs.srcDirs = [ 'src/main/jniLibs' ]
128-
// }
129-
// }
130-
131-
// sourceSets {
132-
// main {
133-
// java.srcDirs += [
134-
// "generated/java",
135-
// "generated/jni"
136-
// ]
137-
// }
138-
// }
139126
}
140127

141128
repositories {
@@ -157,3 +144,21 @@ react {
157144
codegenJavaPackageName = "com.callstack.node_api_modules"
158145
}
159146

147+
// Custom task to fetch jniLibs paths via CLI
148+
task linkNodeApiModules {
149+
doLast {
150+
exec {
151+
// TODO: Support --strip-path-suffix
152+
commandLine 'npx', 'react-native-node-api-modules', 'link', '--android', rootProject.rootDir.absolutePath
153+
standardOutput = System.out
154+
errorOutput = System.err
155+
// Enable color output
156+
environment "FORCE_COLOR", "1"
157+
}
158+
159+
android.sourceSets.main.jniLibs.srcDirs += file("../auto-linked/android").listFiles()
160+
}
161+
}
162+
163+
preBuild.dependsOn linkNodeApiModules
164+

packages/react-native-node-api-modules/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import java.util.HashMap
1212

1313
class NodeApiModulesPackage : BaseReactPackage() {
1414
init {
15-
// SoLoader.loadLibrary("node-api-host-bootstrap")
1615
SoLoader.loadLibrary("node-api-host")
16+
SoLoader.loadLibrary("weak-node-api")
1717
}
1818

1919
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {

packages/react-native-node-api-modules/cpp/CxxNodeApiHostModule.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ bool CxxNodeApiHostModule::loadNodeAddon(NodeAddon &addon,
5353
std::string libraryPath =
5454
"@rpath/" + libraryName + ".framework/" + libraryName;
5555
#elif defined(__ANDROID__)
56-
std::string libraryPath = libraryName
56+
std::string libraryPath = "lib" + libraryName + ".so";
5757
#else
5858
abort()
5959
#endif

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require_relative "./scripts/patch-hermes"
77
NODE_PATH ||= `which node`.strip
88
CLI_COMMAND ||= "'#{NODE_PATH}' '#{File.join(__dir__, "dist/node/cli/run.js")}'"
99
STRIP_PATH_SUFFIX ||= ENV['NODE_API_MODULES_STRIP_PATH_SUFFIX'] === "true"
10-
COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} xcframeworks copy --podfile '#{Pod::Config.instance.installation_root}' #{STRIP_PATH_SUFFIX ? '--strip-path-suffix' : ''}"
10+
COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} link --apple '#{Pod::Config.instance.installation_root}' #{STRIP_PATH_SUFFIX ? '--strip-path-suffix' : ''}"
1111

1212
# We need to run this now to ensure the xcframeworks are copied vendored_frameworks are considered
1313
XCFRAMEWORKS_DIR ||= File.join(__dir__, "xcframeworks")
@@ -32,7 +32,7 @@ Pod::Spec.new do |s|
3232
s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "include/*.h"
3333
s.public_header_files = "include/*.h"
3434

35-
s.vendored_frameworks = "xcframeworks/*.xcframework"
35+
s.vendored_frameworks = "auto-linked/xcframeworks/*.xcframework"
3636
s.script_phase = {
3737
:name => 'Copy Node-API xcframeworks',
3838
:execution_position => :before_compile,

packages/react-native-node-api-modules/scripts/build-weak-node-api.ts

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,6 @@ import { z } from "zod";
1212

1313
export const WEAK_NODE_API_PATH = path.join(__dirname, "../weak-node-api");
1414

15-
export function getNodeApiSymbols(
16-
version: NodeApiVersion,
17-
filter?: "js_native_api_symbols" | "node_api_symbols"
18-
) {
19-
const symbolsPerInterface = symbols[version];
20-
if (filter === "js_native_api_symbols") {
21-
return symbolsPerInterface.js_native_api_symbols;
22-
} else if (filter === "node_api_symbols") {
23-
return symbolsPerInterface.node_api_symbols;
24-
} else {
25-
return [
26-
...symbolsPerInterface.js_native_api_symbols,
27-
...symbolsPerInterface.node_api_symbols,
28-
];
29-
}
30-
}
31-
3215
export function generateVersionScript(
3316
libraryName: string,
3417
globalSymbols: string[]
@@ -88,28 +71,82 @@ export function getNodeApiHeaderAST(version: NodeApiVersion) {
8871
return clangAstDump.parse(parsed);
8972
}
9073

74+
type FunctionDecl = {
75+
name: string;
76+
returnType: string;
77+
argumentTypes: string[];
78+
libraryPath: string;
79+
};
80+
81+
export function generateNodeApiFunction({
82+
name,
83+
returnType,
84+
argumentTypes,
85+
libraryPath,
86+
}: FunctionDecl) {
87+
const stubbedReturnStatement =
88+
returnType === "void"
89+
? "abort();"
90+
: "return napi_status::napi_generic_failure;";
91+
return `
92+
typedef ${returnType} (*${name}_t)(${argumentTypes.join(", ")});
93+
94+
${returnType} ${name}(${argumentTypes
95+
.map((type, index) => `${type} arg${index}`)
96+
.join(", ")}) {
97+
#ifdef NODE_API_REEXPORT
98+
static ${name}_t real_func = NULL;
99+
100+
if (!real_func) {
101+
void* handle = dlopen("${libraryPath}", RTLD_LAZY | RTLD_GLOBAL);
102+
if (!handle) {
103+
fprintf(stderr, "Failed to load ${libraryPath}: %s\\n", dlerror());
104+
${stubbedReturnStatement}
105+
}
106+
107+
real_func = (${name}_t)dlsym(handle, "${name}");
108+
if (!real_func) {
109+
fprintf(stderr, "Failed to find symbol: %s\\n", dlerror());
110+
${stubbedReturnStatement}
111+
}
112+
}
113+
114+
${returnType === "void" ? "" : "return "}real_func(${argumentTypes
115+
.map((t, index) => `arg${index}`)
116+
.join(", ")}); // Call the real function
117+
#else
118+
${stubbedReturnStatement}
119+
#endif
120+
}`;
121+
}
122+
91123
/**
92124
* Generates source code for a version script for the given Node API version.
93125
* @param version
94126
*/
95127
export function generateFakeNodeApiSource(version: NodeApiVersion) {
96128
const lines = [
97129
"// This file is generated by react-native-node-api-modules",
98-
"#include <node_api.h>",
130+
"#include <node_api.h>", // Node-API
131+
"#include <dlfcn.h>", // dlopen(), dlsym()
132+
"#include <stdio.h>", // fprintf()
133+
"#include <stdlib.h>", // abort()
99134
];
100135
const root = getNodeApiHeaderAST(version);
101136
assert.equal(root.kind, "TranslationUnitDecl");
102137
assert(Array.isArray(root.inner));
103138
const foundSymbols = new Set();
104-
const nodeApiSymbols = new Set(getNodeApiSymbols(version));
105-
for (const node of root.inner) {
106-
if (
107-
node.kind === "FunctionDecl" &&
108-
node.name &&
109-
nodeApiSymbols.has(node.name)
110-
) {
111-
foundSymbols.add(node.name);
112139

140+
const symbolsPerInterface = symbols[version];
141+
const engineSymbols = new Set(symbolsPerInterface.js_native_api_symbols);
142+
const runtimeSymbols = new Set(symbolsPerInterface.node_api_symbols);
143+
const allSymbols = new Set([...engineSymbols, ...runtimeSymbols]);
144+
145+
for (const node of root.inner) {
146+
const { name, kind } = node;
147+
if (kind === "FunctionDecl" && name && allSymbols.has(name)) {
148+
assert(name, "Expected a name");
149+
foundSymbols.add(name);
113150
assert(node.type, `Expected type for ${node.name}`);
114151

115152
const match = node.type.qualType.match(
@@ -126,20 +163,27 @@ export function generateFakeNodeApiSource(version: NodeApiVersion) {
126163
);
127164
assert(
128165
argumentTypes,
129-
`Failed to get argument types from ${node.type.qualType}`
166+
`Failed to get argument types from ${argumentTypes}`
130167
);
131168
assert(
132169
returnType === "napi_status" || returnType === "void",
133170
`Expected return type to be napi_status, got ${returnType}`
134171
);
172+
135173
lines.push(
136-
`__attribute__((weak)) ${returnType} ${node.name}(${argumentTypes}) {`,
137-
returnType === "void" ? "" : " napi_status::napi_generic_failure;",
138-
"}"
174+
generateNodeApiFunction({
175+
name,
176+
returnType,
177+
argumentTypes: argumentTypes.split(",").map((arg) => arg.trim()),
178+
// Defer to the right library
179+
libraryPath: engineSymbols.has(name)
180+
? "libhermes.so"
181+
: "libnode-api-host.so",
182+
})
139183
);
140184
}
141185
}
142-
for (const knownSymbol of nodeApiSymbols) {
186+
for (const knownSymbol of allSymbols) {
143187
if (!foundSymbols.has(knownSymbol)) {
144188
throw new Error(
145189
`Missing symbol '${knownSymbol}' in the AST for Node API ${version}`
@@ -149,18 +193,6 @@ export function generateFakeNodeApiSource(version: NodeApiVersion) {
149193
return lines.join("\n");
150194
}
151195

152-
export async function ensureNodeApiVersionScript(version: NodeApiVersion) {
153-
const outputPath = path.join(WEAK_NODE_API_PATH, `fakenode-${version}.map`);
154-
if (!fs.existsSync(outputPath)) {
155-
// Make sure the output directory exists
156-
fs.mkdirSync(WEAK_NODE_API_PATH, { recursive: true });
157-
const symbols = getNodeApiSymbols(version);
158-
const content = generateVersionScript("libfakenode", symbols);
159-
fs.writeFileSync(outputPath, content, "utf-8");
160-
}
161-
return outputPath;
162-
}
163-
164196
async function run() {
165197
const sourceCode = generateFakeNodeApiSource("v10");
166198
await fs.promises.mkdir(WEAK_NODE_API_PATH, { recursive: true });
@@ -177,6 +209,8 @@ async function run() {
177209
"--apple",
178210
"--no-auto-link",
179211
"--no-weak-node-api-linkage",
212+
// TODO: Add support for passing variables through to CMake
213+
// "-D NODE_API_REEXPORT=1",
180214
"--source",
181215
WEAK_NODE_API_PATH,
182216
],

0 commit comments

Comments
 (0)