Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/node-addon-examples/scripts/build-examples.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ const projectDirectories = findCMakeProjects();

for (const projectDirectory of projectDirectories) {
console.log(`Running "react-native-node-api-cmake" in ${projectDirectory}`);
execSync("react-native-node-api-cmake --triplet arm64-apple-ios-sim", {
cwd: projectDirectory,
stdio: "inherit",
});
execSync(
"react-native-node-api-cmake --triplet aarch64-linux-android --triplet arm64-apple-ios-sim",
{
cwd: projectDirectory,
stdio: "inherit",
}
);
console.log();
}
2 changes: 1 addition & 1 deletion packages/react-native-node-api-cmake/src/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export async function createAndroidLibsDirectory({
const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet];
const archOutputPath = path.join(outputPath, arch);
await fs.promises.mkdir(archOutputPath, { recursive: true });
const libraryName = path.basename(libraryPath, path.extname(libraryPath));
const libraryName = path.basename(libraryPath);
const libraryOutputPath = path.join(archOutputPath, libraryName);
await fs.promises.copyFile(libraryPath, libraryOutputPath);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/react-native-node-api-cmake/src/weak-node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
);
assert(fs.existsSync(pathname), "Weak Node API path does not exist");
if (isAppleTriplet(triplet)) {
const xcframeworkPath = path.join(pathname, "weak-node-api.xcframework");
const xcframeworkPath = path.join(pathname, "libweak-node-api.xcframework");
assert(
fs.existsSync(xcframeworkPath),
`Expected an XCFramework at ${xcframeworkPath}`
Expand All @@ -25,9 +25,9 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
} else if (isAndroidTriplet(triplet)) {
const libraryPath = path.join(
pathname,
"weak-node-api.android.node",
"libweak-node-api.android.node",
ANDROID_ARCHITECTURES[triplet],
"weak-node-api"
"libweak-node-api.so"
);
assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`);
return libraryPath;
Expand Down
10 changes: 5 additions & 5 deletions packages/react-native-node-api-modules/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ include/
**/android/build/

# iOS build artifacts
xcframeworks/
/auto-linked/

# Android build artifacts
android/.cxx/
android/build/

# Everything in weak-node-api is generated, except for the configurations
weak-node-api/build/
weak-node-api/weak-node-api.xcframework
weak-node-api/weak-node-api.android.node
weak-node-api.cpp
/weak-node-api/build/
/weak-node-api/*.xcframework
/weak-node-api/*.android.node
/weak-node-api/weak-node-api.cpp
13 changes: 12 additions & 1 deletion packages/react-native-node-api-modules/android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@ target_include_directories(node-api-host PRIVATE
find_package(ReactAndroid REQUIRED CONFIG)
find_package(hermes-engine REQUIRED CONFIG)


target_link_libraries(node-api-host
# android
ReactAndroid::reactnative
ReactAndroid::jsi
hermes-engine::libhermes
# react_codegen_NodeApiHostSpec
)

add_subdirectory(../weak-node-api weak-node-api)

target_compile_definitions(weak-node-api
PRIVATE
# NAPI_VERSION=8
NODE_API_REEXPORT=1
)
target_link_libraries(weak-node-api
node-api-host
hermes-engine::libhermes
)
35 changes: 20 additions & 15 deletions packages/react-native-node-api-modules/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import java.nio.file.Paths
import groovy.json.JsonSlurper

buildscript {
ext.getExtOrDefault = {name ->
Expand Down Expand Up @@ -64,7 +65,7 @@ android {

externalNativeBuild {
cmake {
targets "node-api-host"
targets "node-api-host", "weak-node-api"
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
arguments "-DANDROID_STL=c++_shared"
abiFilters (*reactNativeArchitectures())
Expand Down Expand Up @@ -122,20 +123,6 @@ android {
doNotStrip "**/libhermes.so"
}

// sourceSets {
// main {
// jniLibs.srcDirs = [ 'src/main/jniLibs' ]
// }
// }

// sourceSets {
// main {
// java.srcDirs += [
// "generated/java",
// "generated/jni"
// ]
// }
// }
}

repositories {
Expand All @@ -157,3 +144,21 @@ react {
codegenJavaPackageName = "com.callstack.node_api_modules"
}

// Custom task to fetch jniLibs paths via CLI
task linkNodeApiModules {
doLast {
exec {
// TODO: Support --strip-path-suffix
commandLine 'npx', 'react-native-node-api-modules', 'link', '--android', rootProject.rootDir.absolutePath
standardOutput = System.out
errorOutput = System.err
// Enable color output
environment "FORCE_COLOR", "1"
}

android.sourceSets.main.jniLibs.srcDirs += file("../auto-linked/android").listFiles()
}
}

preBuild.dependsOn linkNodeApiModules

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import java.util.HashMap

class NodeApiModulesPackage : BaseReactPackage() {
init {
// SoLoader.loadLibrary("node-api-host-bootstrap")
SoLoader.loadLibrary("node-api-host")
SoLoader.loadLibrary("weak-node-api")
}

override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ bool CxxNodeApiHostModule::loadNodeAddon(NodeAddon &addon,
std::string libraryPath =
"@rpath/" + libraryName + ".framework/" + libraryName;
#elif defined(__ANDROID__)
std::string libraryPath = libraryName
std::string libraryPath = "lib" + libraryName + ".so";
#else
abort()
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require_relative "./scripts/patch-hermes"
NODE_PATH ||= `which node`.strip
CLI_COMMAND ||= "'#{NODE_PATH}' '#{File.join(__dir__, "dist/node/cli/run.js")}'"
STRIP_PATH_SUFFIX ||= ENV['NODE_API_MODULES_STRIP_PATH_SUFFIX'] === "true"
COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} xcframeworks copy --podfile '#{Pod::Config.instance.installation_root}' #{STRIP_PATH_SUFFIX ? '--strip-path-suffix' : ''}"
COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} link --apple '#{Pod::Config.instance.installation_root}' #{STRIP_PATH_SUFFIX ? '--strip-path-suffix' : ''}"

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

s.vendored_frameworks = "xcframeworks/*.xcframework"
s.vendored_frameworks = "auto-linked/xcframeworks/*.xcframework"
s.script_phase = {
:name => 'Copy Node-API xcframeworks',
:execution_position => :before_compile,
Expand Down
120 changes: 77 additions & 43 deletions packages/react-native-node-api-modules/scripts/build-weak-node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,6 @@ import { z } from "zod";

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

export function getNodeApiSymbols(
version: NodeApiVersion,
filter?: "js_native_api_symbols" | "node_api_symbols"
) {
const symbolsPerInterface = symbols[version];
if (filter === "js_native_api_symbols") {
return symbolsPerInterface.js_native_api_symbols;
} else if (filter === "node_api_symbols") {
return symbolsPerInterface.node_api_symbols;
} else {
return [
...symbolsPerInterface.js_native_api_symbols,
...symbolsPerInterface.node_api_symbols,
];
}
}

export function generateVersionScript(
libraryName: string,
globalSymbols: string[]
Expand Down Expand Up @@ -88,28 +71,82 @@ export function getNodeApiHeaderAST(version: NodeApiVersion) {
return clangAstDump.parse(parsed);
}

type FunctionDecl = {
name: string;
returnType: string;
argumentTypes: string[];
libraryPath: string;
};

export function generateNodeApiFunction({
name,
returnType,
argumentTypes,
libraryPath,
}: FunctionDecl) {
const stubbedReturnStatement =
returnType === "void"
? "abort();"
: "return napi_status::napi_generic_failure;";
return `
typedef ${returnType} (*${name}_t)(${argumentTypes.join(", ")});

${returnType} ${name}(${argumentTypes
.map((type, index) => `${type} arg${index}`)
.join(", ")}) {
#ifdef NODE_API_REEXPORT
static ${name}_t real_func = NULL;

if (!real_func) {
void* handle = dlopen("${libraryPath}", RTLD_LAZY | RTLD_GLOBAL);
if (!handle) {
fprintf(stderr, "Failed to load ${libraryPath}: %s\\n", dlerror());
${stubbedReturnStatement}
}

real_func = (${name}_t)dlsym(handle, "${name}");
if (!real_func) {
fprintf(stderr, "Failed to find symbol: %s\\n", dlerror());
${stubbedReturnStatement}
}
}

${returnType === "void" ? "" : "return "}real_func(${argumentTypes
.map((t, index) => `arg${index}`)
.join(", ")}); // Call the real function
#else
${stubbedReturnStatement}
#endif
}`;
}

/**
* Generates source code for a version script for the given Node API version.
* @param version
*/
export function generateFakeNodeApiSource(version: NodeApiVersion) {
const lines = [
"// This file is generated by react-native-node-api-modules",
"#include <node_api.h>",
"#include <node_api.h>", // Node-API
"#include <dlfcn.h>", // dlopen(), dlsym()
"#include <stdio.h>", // fprintf()
"#include <stdlib.h>", // abort()
];
const root = getNodeApiHeaderAST(version);
assert.equal(root.kind, "TranslationUnitDecl");
assert(Array.isArray(root.inner));
const foundSymbols = new Set();
const nodeApiSymbols = new Set(getNodeApiSymbols(version));
for (const node of root.inner) {
if (
node.kind === "FunctionDecl" &&
node.name &&
nodeApiSymbols.has(node.name)
) {
foundSymbols.add(node.name);

const symbolsPerInterface = symbols[version];
const engineSymbols = new Set(symbolsPerInterface.js_native_api_symbols);
const runtimeSymbols = new Set(symbolsPerInterface.node_api_symbols);
const allSymbols = new Set([...engineSymbols, ...runtimeSymbols]);

for (const node of root.inner) {
const { name, kind } = node;
if (kind === "FunctionDecl" && name && allSymbols.has(name)) {
assert(name, "Expected a name");
foundSymbols.add(name);
assert(node.type, `Expected type for ${node.name}`);

const match = node.type.qualType.match(
Expand All @@ -126,20 +163,27 @@ export function generateFakeNodeApiSource(version: NodeApiVersion) {
);
assert(
argumentTypes,
`Failed to get argument types from ${node.type.qualType}`
`Failed to get argument types from ${argumentTypes}`
);
assert(
returnType === "napi_status" || returnType === "void",
`Expected return type to be napi_status, got ${returnType}`
);

lines.push(
`__attribute__((weak)) ${returnType} ${node.name}(${argumentTypes}) {`,
returnType === "void" ? "" : " napi_status::napi_generic_failure;",
"}"
generateNodeApiFunction({
name,
returnType,
argumentTypes: argumentTypes.split(",").map((arg) => arg.trim()),
// Defer to the right library
libraryPath: engineSymbols.has(name)
? "libhermes.so"
: "libnode-api-host.so",
})
);
}
}
for (const knownSymbol of nodeApiSymbols) {
for (const knownSymbol of allSymbols) {
if (!foundSymbols.has(knownSymbol)) {
throw new Error(
`Missing symbol '${knownSymbol}' in the AST for Node API ${version}`
Expand All @@ -149,18 +193,6 @@ export function generateFakeNodeApiSource(version: NodeApiVersion) {
return lines.join("\n");
}

export async function ensureNodeApiVersionScript(version: NodeApiVersion) {
const outputPath = path.join(WEAK_NODE_API_PATH, `fakenode-${version}.map`);
if (!fs.existsSync(outputPath)) {
// Make sure the output directory exists
fs.mkdirSync(WEAK_NODE_API_PATH, { recursive: true });
const symbols = getNodeApiSymbols(version);
const content = generateVersionScript("libfakenode", symbols);
fs.writeFileSync(outputPath, content, "utf-8");
}
return outputPath;
}

async function run() {
const sourceCode = generateFakeNodeApiSource("v10");
await fs.promises.mkdir(WEAK_NODE_API_PATH, { recursive: true });
Expand All @@ -177,6 +209,8 @@ async function run() {
"--apple",
"--no-auto-link",
"--no-weak-node-api-linkage",
// TODO: Add support for passing variables through to CMake
// "-D NODE_API_REEXPORT=1",
"--source",
WEAK_NODE_API_PATH,
],
Expand Down
Loading