Closed stoically closed 4 years ago
Did some digging, tl;dr is it builds but crashes without error when trying to run.
nodejs-mobile-react-native
with Android Studio Emulatorneon-cli
through npm i -g neon-cli
neon new neon-test
neon projects don't have a bindings.gyp
, so it's needed to put a BUILD_NATIVE_MODULES.txt
with content 1
into the nodejs-assets
directory. That will trigger the neon build if the neon project has additionally an "install": "neon build"
scripts entry in the package.json
.
react-native run-android
will successfully build, put the compiled neon native module into build/nodejs-native-assets/nodejs-native-assets-{abi}/node_modules/neon-test/native/index.node
and then packages the native/index.node
into the apk and deploys it to the emulator.
neon build
calls rustc build
internally in the neon-test
crate located at neon-test/native
neon-test/native
crate depends on the neon
crate which in turn has the neon-runtime
crate as build-dependencyrustc build
triggers the build for the crate neon-runtime
as defined in its build.rs
neon-runtime
s build runs npm run build-{debug|release}
, which is a call to node-gyp build
as defined in its package.json
env::vars
in the build.rs
and the npm_configs_
, like e.g. npm_config_node_gyp
, from nodejs-mobile
are set correctlyRequiring the neon-test
project inside the nodejs-assets/nodejs-project/main.js
and nodejs.start
ing from react-native
will crash the app without an error message. Not sure how to debug further. Any pointers would be appreciated.
Trying to build the apk with ./gradlew assembleRelease
will fail if there's already a native/index.node
and native/target
in the neon-test
node_modules directory, which is the case if npm install
is executed in the nodejs-assets/nodejs-project
with the "install": "neon build"
script in the package.json
. It's needed to remove native/index.node
and native/target
first to have a successful build.
So I realized that I can check the Android Studio Logcat and there's actually an error:
11909-11956/com.neontest E/NODEJS-MOBILE: internal/modules/cjs/loader.js:717
return process.dlopen(module, path.toNamespacedPath(filename));
^
Error: dlopen failed: cannot locate symbol "node_module_register" referenced by "/data/data/com.neontest/files/nodejs-project/node_modules/neon-test/native/index.node"...
To actually get neon to build successfully, it's needed to set some rust cargo build environment variables and have the neon-cli respect the CARGO_BUILD_TARGET
. I've opened PRs regarding that here:
(I've pushed the neon change to a fork, so it's possible to add npx neon-cli@stoically/neon#neon-cli-dist build
as install script)
Also, rust needs the targets installed, on linux these are:
rustup target add i686-linux-android
rustup target add arm-linux-androideabi
rustup target add armv7-linux-androideabi
rustup target add aarch64-linux-android
rustup target add x86_64-linux-android
There's also the matter of having to link the final binary with nodejs-mobile for it to load successfully at runtime (this might be why the dlopen failed
error appeared) : https://github.com/janeasystems/nodejs-mobile-gyp/issues/4
It's working now, then?
Thanks for the hint. I've tried to let neon use the nodejs-mobile
fork of node-gyp
by putting /path/to/nodejs-mobile-gyp/bin/nody-gyp.js build --nodedir=/path/to/nodejs-mobile-react-native/android/libnode
into the neon-runtime package.json where currently node-gyp is called. It again builds successfully, but unfortunately still seeing the dlopen failed
error. Maybe you got an idea on how to debug this further?
Here's the output from nodejs-mobile-gyp configure and build (run by neon-runtime):
It says gyp verb get node dir compiling against specified --nodedir dev files: /path/to/nodejs-mobile-react-native/android/libnode/
, so it seems to use the correct npm_config_
env from the gradle task. (I've removed the command-line --nodedir=
mentioned in the last comment)
Hi @stoically ,
The resulting binary files need to be linked to the nodejs-mobile shared library : https://github.com/janeasystems/nodejs-mobile/blob/2101f2096d59d2a081f64da2f10fd7385222ac8a/common.gypi#L517-L538
One way of debugging this would be to verify if the resulting ELF binary (the .node
file) has a reference to the library. Here's an example, where I use the NDK's readelf tool for arm64 binaries to check it in a macOS machine:
$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf -d android/build/nodejs-native-assets/nodejs-native-assets-arm64-v8a/node_modules/sha3/build/Release/sha3.node | grep "NEEDED"
This gives the following output:
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libnode.so]
0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
The line for [libnode.so]
is needed in order for the runtime dynamic loader in Android to find the symbols at runtime.
The neon.node
binary which is generated by the neon-runtime
from the node-gyp
call internally indeed links to the libnode.so
$ANDROID_NDK_HOME/toolchains/x86_64-4.9/prebuilt/linux-x86_64/bin/x86_64-linux-android-readelf -d /home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/neon-runtime-0.2.0/build/Debug/obj.target/neon.node | grep "NEEDED"
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libnode.so]
0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
But that binary is not used. The neon-runtime proceeds to compile a libneon.a
based on the neon.o
(build from node-gyp
). That libneon.a
is linked into the neon-runtime crate. Finally the resulting dylib (lib<neon-project-name>.so
) is copied to native/index.node
.
And the resulting index.node
binary, obviously, ends up not being linked against libnode.so
.
$ANDROID_NDK_HOME/toolchains/x86_64-4.9/prebuilt/linux-x86_64/bin/x86_64-linux-android-readelf -d android/build/nodejs-native-assets-temp-build/nodejs-native-assets-x86_64/nodejs-project/node_modules/neon-test/native/index.node | grep "NEEDED"
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
So my best guess at the moment is that it's needed to let the neon-runtime Cargo.toml point to the nodejs-mobile lib files in its link section.
Linking the libnode.so
worked by putting links = "node"
in the neon project native/Cargo.toml
and
[target.x86_64-linux-android.node]
rustc-link-search = ["/path/to/nodejs-mobile-react-native/android/libnode/bin/x86_64"]
rustc-link-lib = ["node"]
in the native/.cargo/config
Now adb logcat
tells me the following when starting the app:
05-16 20:06:45.983 16152 16200 E NODEJS-MOBILE: internal/modules/cjs/loader.js:717
05-16 20:06:45.983 16152 16200 E NODEJS-MOBILE: return process.dlopen(module, path.toNamespacedPath(filename));
05-16 20:06:45.983 16152 16200 E NODEJS-MOBILE: ^
05-16 20:06:45.983 16152 16200 E NODEJS-MOBILE:
05-16 20:06:45.983 16152 16200 E NODEJS-MOBILE: Error: Module did not self-register.
So my next best guess was that libc++_shared.so
and liblog.so
are needed too. I've tried linking the /path/to/android/build/standalone-toolchains/x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so
, but that results in
error: linking with `/path/to/android/build/standalone-toolchains/x86_64/bin/x86_64-linux-android-clang++` failed: exit code: 1
= note: /path/to/android/build/standalone-toolchains/x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: /path/to/android/build/standalone-toolchains/x86_64/sysroot/usr/lib/x86_64-linux-android/libc.a(sse2-memset-slm.o):
requires dynamic R_X86_64_PC32 reloc against '__memset_chk_fail' which may overflow at runtime;
recompile with -fPIC
I'm not sure what exactly needs to compiled with -fPIC
and if using the libc++_shared.so
from the toolchain is even the right approach. Maybe you got another helpful hint?
Hi @stoically ,
The error here is Error: Module did not self-register.
Node Modules register themselves by declaring themselves through NODE_MODULE
, which is a macro that defines a function that's supposed to run when you dlopen
the module. The function is marked as a __attribute__((constructor))
for this behavior, so that the function is registered in the .ctor
section of the ELF binary.
Here's an example of such a function being declared: https://stackoverflow.com/a/41283828/4594761
Some build systems might not include these symbols when building something from a static library, since these symbols were not referenced anywhere in the code that's using the static library. You mentioned the module is first built as a static library and then into a shared library, so it's possible there's some issues there.
There's also some changes to nodejs-mobile headers that might be related to this, in order to properly build the nodejs-mobile library for iOS (builds into a static library that is used by a Framework
project. For Rust, the function might need to be static, so this change might need to be reverted in the node headers:
https://github.com/janeasystems/nodejs-mobile/commit/7ee73e96c2a89914d025f4d217faef32b370fbf6
The node.h
file is inside the --nodedir
.
There might be some way to use the NDK's readelf
(or similar) tool to see if the registered initialization function for the module you are building is properly exported in the final shared library, in order to debug this.
I hope this is helpful :)
Hey @jaimecbernardo - thanks again for another, indeed helpful, hint!
Checked the registered constructors in the final shared library with objdump -Dr -j .init_array
. The index.node
compiled from neon itself contains
00000000024d8468 <_ZN4node18__LOAD_NEON_MODULE17h3645e986fe67a8c5E>:
which is the responsible code for calling node_register_module
.
The index.node
compiled from nodejs-mobile doesn't contain an .init_array
section as you suspected. I've tried changing the node.h
as you suggested and did a full rebuild, but that unfortunately didn't help either. So I guess I have to figure out why the ctor is not included in the final library. I have no experience regarding compilation of static/shared libraries, so yet another suggestion on where I could start looking would be much appreciated!
Hi @stoically ,
I'm not familiar enough with Rust's toolset to understand what could be going on, but my first step would be comparing the commands being run when compiling to run in the Desktop, if the symbols are being correctly exported to the final .so
there. Whatever flag is being passed to include those symbols might not be supported in the NDK toolchain, perhaps?
Added the missing npm_config environment variables to build-settings in neon/src/build-settings.ts and neon/lib/build-settings.js and now the module registers
Forgot to add that you need to also add in the android/iOS target here like so:
#[cfg_attr(target_os = "android", link_section = ".ctors")]
https://github.com/neon-bindings/neon/blob/37191e316e12bdbd8f84d3cd42739263d3312455/src/lib.rs#L87
@cgdusek Cool stuff, thanks! I guess we can close this issue then. Would you be interested in opening an PR against Neon with those changes?
Yeah, I will do it when I get a little bit of time. The neon runtime also needs to have the nodejs-mobile-nodegyp dependency like this. The Neon team is currently working on a different methodology for the Node bindings so I have been hesitant to PR waiting for the outcome of that work.
I've been following this thread closely, and the tips you've put @stoically @cgdusek @jaimecbernardo have been very helpful.
I managed to get my Neon-built npm package working with nodejs-mobile, and for reference I'll list here the right configurations and changes that made it possible:
These changes to the neon
repo are necessary (I will submit a PR):
──────────────────────────────────────────────────────────────────────────────────────────────────────────
modified: cli/src/build-settings.ts
──────────────────────────────────────────────────────────────────────────────────────────────────────────
@ cli/src/build-settings.ts:60 @ export default class BuildSettings {
npm_config_disturl: process.env.npm_config_disturl || null,
npm_config_runtime: process.env.npm_config_runtime || null,
npm_config_build_from_source: process.env.npm_config_build_from_source || null,
- npm_config_devdir: process.env.npm_config_devdir || null
+ npm_config_devdir: process.env.npm_config_devdir || null,
+ npm_config_node_engine: process.env.npm_config_node_engine || null,
+ npm_config_nodedir: process.env.npm_config_nodedir || null,
+ npm_config_node_gyp: process.env.npm_config_node_gyp || null,
+ npm_config_platform: process.env.npm_config_platform || null
});
}
──────────────────────────────────────────────────────────────────────────────────────────────────────────
modified: crates/neon-sys/build.rs
──────────────────────────────────────────────────────────────────────────────────────────────────────────
@ crates/neon-sys/build.rs:97 @ mod build {
//
// gyp verb architecture ia32
fn parse_node_arch(node_gyp_output: &str) -> String {
- let version_regex = Regex::new(r"gyp verb architecture (?P<arch>ia32|x64)").unwrap();
+ let version_regex = Regex::new(r"gyp verb architecture (?P<arch>ia32|x64|arm|arm64)").unwrap();
let captures = version_regex.captures(&node_gyp_output).unwrap();
String::from(&captures["arch"])
}
──────────────────────────────────────────────────────────────────────────────────────────────────────────
modified: src/lib.rs
──────────────────────────────────────────────────────────────────────────────────────────────────────────
@ src/lib.rs:108 @ macro_rules! register_module {
// Mark this function as a global constructor (like C++).
#[allow(improper_ctypes)]
#[cfg_attr(target_os = "linux", link_section = ".ctors")]
+ #[cfg_attr(target_os = "android", link_section = ".ctors")]
#[cfg_attr(target_os = "macos", link_section = "__DATA,__mod_init_func")]
#[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
#[used]
These rustup targets need to be installed:
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add arm-linux-androideabi
The Cargo.toml of the Neon-built npm package needs this:
diff --git a/node_modules/ssb-neon-keys/native/Cargo.toml b/node_modules/ssb-neon-keys/native/Cargo.toml
index 4e72059..6fc3dbb 100644
--- a/node_modules/ssb-neon-keys/native/Cargo.toml
+++ b/node_modules/ssb-neon-keys/native/Cargo.toml
@@ -4,6 +4,7 @@ version = "8.0.0"
authors = ["Andre Staltz <andre@staltz.com>"]
license = "AGPL-3.0"
build = "build.rs"
+links = "node"
exclude = ["artifacts.json", "index.node"]
edition = "2018"
The Neon-built npm package needs this additional config file (it uses absolute paths, so this file should be created locally via a script):
node_modules/ssb-neon-keys/native/.cargo/config.toml
[target.aarch64-linux-android.node]
rustc-link-search = ["/home/me/absolute/path/to/project/node_modules/nodejs-mobile-react-native/android/libnode/bin/arm64-v8a"]
rustc-link-lib = ["node"]
[target.armv7-linux-androideabi.node]
rustc-link-search = ["/home/me/absolute/path/to/project/node_modules/nodejs-mobile-react-native/android/libnode/bin/armeabi-v7a"]
rustc-link-lib = ["node"]
[target.arm-linux-androideabi.node]
rustc-link-search = ["/home/me/absolute/path/to/project/node_modules/nodejs-mobile-react-native/android/libnode/bin/armeabi-v7a"]
rustc-link-lib = ["node"]
The project compiled correctly and ran successfully in runtime. Only on Android. I'm still trying to figure out iOS and would I appreciate a lot if anyone has made progress on iOS.
I just figured out iOS support! Compiled and tested on an iPad. You should assume that the following changes build on top of my previous comment, I can't guarantee that this will work if you don't apply the changes from the previous comment.
These changes to the neon
repo are necessary (I will submit a PR):
──────────────────────────────────────────────────────────────────────────────────────────────────────────
modified: src/lib.rs
──────────────────────────────────────────────────────────────────────────────────────────────────────────
@ src/lib.rs:108 @ macro_rules! register_module {
// Mark this function as a global constructor (like C++).
#[allow(improper_ctypes)]
#[cfg_attr(target_os = "linux", link_section = ".ctors")]
#[cfg_attr(target_os = "android", link_section = ".ctors")]
#[cfg_attr(target_os = "macos", link_section = "__DATA,__mod_init_func")]
+ #[cfg_attr(target_os = "ios", link_section = "__DATA,__mod_init_func")]
#[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
#[used]
These rustup targets need to be installed:
rustup target add aarch64-apple-ios
rustup target add x86_64-apple-ios
nodejs-mobile-react-native/scripts/ios-build-native-modules.sh needs these changes to add CARGO_BUILD_TARGET
(I will submit a PR):
pushd $CODESIGNING_FOLDER_PATH/nodejs-project/
if [ "$PLATFORM_NAME" == "iphoneos" ]
then
- GYP_DEFINES="OS=ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_format="make-ios" npm_config_node_engine="chakracore" npm_config_arch="arm64" npm --verbose rebuild --build-from-source
+ GYP_DEFINES="OS=ios" CARGO_BUILD_TARGET="aarch64-apple-ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_format="make-ios" npm_config_node_engine="chakracore" npm_config_arch="arm64" npm --verbose rebuild --build-from-source
else
- GYP_DEFINES="OS=ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_format="make-ios" npm_config_node_engine="chakracore" npm_config_arch="x64" npm --verbose rebuild --build-from-source
+ GYP_DEFINES="OS=ios" CARGO_BUILD_TARGET="x86_64-apple-ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_format="make-ios" npm_config_node_engine="chakracore" npm_config_arch="x64" npm --verbose rebuild --build-from-source
fi
popd
Finally, the index.node
files need to be converted to folders, this was important so that nodejs-mobile scripts create .framework
files for these Cargo-built (not gyp-built) .node
files. I did this by patching the package.json
of the my neon npm packages, to add a postinstall
script which runs after Cargo has built the index.node
. I wrote a script in my project to automatically apply this patch after npm install. Maybe this should be part of Neon? I'm not sure. Anyway, here it is, a simple hack:
{
"name": "my-neon-package",
...
"scripts": {
+ "postinstall": "mv native/index.node native/index && mkdir native/index.node && mv native/index native/index.node/index"
}
}
neon compiles Rust code to native nodejs modules and provides bindings to work with those native modules. Would be awesome if nodejs-mobile could support neon compiled native modules.
Notes
"Related" issues over at neon:
Also, maybe it's already possible by manually building https://github.com/janeasystems/nodejs-mobile/issues/173 the neon module and providing it to nodejs-mobile? I've just naively tried to copy over my neon-build module into the nodejs-mobile node_modules folder, but that makes
react-native run-android
fail with':app:packageDebug'. > org.gradle.tooling.BuildException (no error message)
.