Build napi as a shared library on Android#183
Open
matthargett wants to merge 2 commits into
Open
Conversation
Link napi as a shared library (libnapi.so) on Android instead of statically into each consumer, so native addons can be dlopen'd as standalone .node modules and resolve their napi_* imports via a real DT_NEEDED. The host and every addon then share a single napi instance. Other platforms keep static napi.
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adjusts how the napi library is built so Android produces a shared libnapi.so, enabling separately dlopen’d native addons to resolve napi_* symbols via dynamic linking.
Changes:
- Build
napiasSHAREDon Android, otherwise keep existing default library-type behavior. - Add rationale in CMake comments about Android/bionic
dlopenand symbol resolution.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Author
|
cc @vmoroz @kraenhansen , thanks for generalizing your work into the upstream nodejs CTS! |
- Don't hard-force SHARED: add a JSR_NAPI_SHARED CMake option (default ON on Android, OFF elsewhere)
so integrators can keep a static napi via -DJSR_NAPI_SHARED=OFF without patching the project.
- Fix the comment: the non-shared branch keeps add_library(napi ${SOURCES}), which follows the
project's default library type (BUILD_SHARED_LIBS), not necessarily static.
matthargett
added a commit
to rebeckerspecialties/JsRuntimeHost
that referenced
this pull request
Jun 5, 2026
…PI_SHARED option)
This was referenced Jun 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Android, build the
napitarget as a shared library (libnapi.so) instead of statically linking it into each consumer:Other platforms are unchanged —
napistays a static library there. Small, self-contained packaging change to the Node-API build.Why
Aligns the native-addon model with
nodejs/node-api-cts. With a sharedlibnapi.so, a native addon can bedlopen'd as a standalone.nodemodule that resolvesnapi_*through a realDT_NEEDED— exactly what node-api-cts'sadd_node_api_cts_addon()produces. This lets us run that conformance suite's addons directly instead of duplicating/vendoring parts of it inline in this repo.More flexible binary app updates. The engine, the app, and individual addons become separately shippable binaries, so an update can target an individual
.sorather than relinking one monolith.Lazy loading of native modules. Addons can be
dlopen'd on demand rather than all being bound at process start, which can help app launch time.Still installs via
.apkand stays within Google Play binary-analysis rules.libnapi.sois a normal bundledlib/<abi>/*.soshipped inside the APK'snativeLibraryDir— no runtime code download, nodlopenfrom an app-writable directory (both of which Android API 29+ / Play restrict). It is the same packaging mechanism already used forlibv8android.so.Static and dynamic can coexist. If a shared
napiturns out to be an unwanted hard dependency for some embedders, this can be gated behind a CMake option (default could remain static; opt into shared where the dynamic addon model is needed). The change here is a ~6-line diff, so adding that knob later is trivial.Compatibility / embedder impact
Android (primary)
napi— directly or transitively (AppRuntime,JsRuntime,Foundation,ScriptLoader, the polyfills) — now carries aDT_NEEDED libnapi.so, so the final app/AAR must bundlelibnapi.soinlib/<abi>/.externalNativeBuild(and the prefab/AAR distribution) this is automatic — AGP packages every SHARED target it builds..soallow-list (manually enumerating which libraries to bundle) must addlibnapi.so, or the app fails at load withdlopen failed: library "libnapi.so" not found..so. Negligible.libnapi.soexports its default-visibility symbols (allnapi_*plus theNapi::/engine glue). That is the point — addons resolve from it — but it widens what the engine exports vs. a statically-internalizednapi. An embedder who relied onnapi_*being hidden in their final binary would now see them exported. (Unlikely to matter in practice.)libnapi.so→ one napi/engine state, which is the correct behavior for sharing an env across the host and addons. An embedder who had accidentally linked two staticnapicopies converges to one — generally a fix, not a regression.Android app-store deployability
lib/<abi>/*.sois the standard, sanctioned mechanism — no policy issue. This change introduces no runtime code download and nodlopenfrom a writable directory, so it stays clear of the API-29+ / Play native-code restrictions.iOS (unchanged here, but worth stating)
if(ANDROID)-guarded, so iOS keeps staticnapi— intentionally. Apple's app-store rules prohibit loading dynamically-linked code that isn't embedded + code-signed in the app bundle, anddlopenof arbitrary dylibs is restricted. A sharednapion iOS would have to be a properly embedded, signed.framework, and the dynamic-addon model would be a meaningfully bigger lift. No change/impact today — flagged so the dynamic model isn't assumed portable to iOS for free. (On Apple, JsRuntimeHost uses system JavaScriptCore and the existing addon model resolvesnapi_*from the host process via-undefined dynamic_lookup.)Windows (unchanged here)
if(ANDROID)-guarded, so Windows keeps staticnapi. A sharednapion Windows would benapi.dll+ an import lib (NAPI_EXTERNalready maps to__declspec(dllexport/dllimport)under_WIN32), which is feasible but out of scope. No impact today. (Windows additionally carries the Chakra-engine considerations tracked separately.)BabylonNative
libnapi.so(automatic under AGP; a custom.soallow-list must add it). No source/API change —napiheaders and usage are identical; only the link/packaging shape changes.Net
The one concrete hardship is Android packaging discipline: anyone whose Android bundling is not AGP-automatic must add
libnapi.soto their packaged libraries. No API changes, no app-store-rule conflicts, and iOS/Windows are untouched. Static linking remains one CMake conditional away if a sharednapiis undesirable for a given embedder.Verification
Verified on the downstream integration branch this was extracted from (rebased on this
main):libnapi.sois built and packaged into the APK (lib/arm64-v8a/libnapi.so, ~300 KB), every consumer links it viaDT_NEEDED, and on an arm64 Android emulator (API 29, V8) a standalone.nodeconformance addon —DT_NEEDED [libnapi.so], itsnapi_*as undefined imports —dlopens and resolves itsnapi_register_module_v1against it, running in-process. This isolated change is just theadd_libraryconditional; CI here exercises it againstmain.Landing sequence
Inter-related N-API PRs; intended order (✅ done / ⏳ pending):
napi_*). ← this PRnapi_*via the sharedlibnapi.sofrom the step above).Motivating RFCs: rebeckerspecialties#6 (jsc-android → maintained JSC) · rebeckerspecialties#7 (WebWorker via N-API → Factotum into BabylonNative).