Repository navigation
How should addons get Node-API from the app's Hermes? #493
kraenhansen
started this conversation in
Ideas
Replies: 1 comment
Spike resultsAn addon that only knows the name
What changed from the plan
Hacks a real version must replace
Not tested
Other things found
🤖 Generated with Claude Code |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
How should an addon get Node-API from the Hermes an app actually uses, instead of through
weak-node-api? Background for the RFC in #447, where this extends the "link the Hermes/React Native framework directly" option.The work is tracked in:
node-apifacade for published addons. Deferred.Findings were checked against
nextand upstream on 2026-10-31.Why
weak-node-apiout completely. The shim then becomes this project's distribution tooling, on our side of the seam alongsidecmake-rnandferric.The original reason for the shim has also weakened. Hermes now implements the Node-API surface itself and the host no longer has to complete it (#428), with
napi_fatal_erroras the one remaining override, which #492 removes.Linkage modes
The eventual shape is a single, mutually exclusive choice when building an addon:
weak-node-apihermesvmlibrary of a concrete React Native, recorded by its real name--no-weak-node-api-linkage, which the "none" mode could replacenode-api, a neutral library name provided by a small prebuilt library the host embeds in the appModes 2 and 5 both reach Hermes without a shim. The difference is what the addon records: mode 2 names Hermes, while mode 5 only names Node-API and leaves the choice of backend to the app.
Scope
Mode 2 is simplest when the build knows the app's exact Hermes. Published prebuilds are possible too, because an addon is only tied to a library name and not a Hermes version (see below):
Tradeoffs
The main concern: Hermes as a private detail behind JSI
Two different couplings get mixed up here:
libhermes.sotolibhermesvm.so, the prefab module tohermes-engine::hermesvm, andhermes.xcframeworktohermesvm.xcframework.getVMRuntimeUnsafe, documented as unstable).Direct linking does work against the abstraction, but only by adding a second engine-level contract next to JSI. An RFC should ask for exactly that: Node-API as a stable public ABI of the engine, and a stable way to link it. This is narrower than making Hermes public.
I found no React Native documentation that either declares Hermes private or supports third-party linking. In practice it happens:
react-native-workletslinkshermes-engine::hermesvm, as does our host. Searchingreact/react-nativeissues and the proposals repo found no policy either way, and no Node-API proposal. That search wasn't exhaustive.Benefits
Costs
napi_fatal_erroritself isn't lost, since Hermes implements it. Reporting its reason depends on React Native installing a fatal handler (see Blockers).hermesvmcan't load under other engines. JSC has moved to community support andreact-native-v8looks inactive, butweak-node-apiis engine-neutral today and that has real value.DT_NEEDEDboth bake it in, and another rename would break such builds. This is the question for the RFC, and an acceptable risk while adoption is small.What an addon records
An addon doesn't store any code, offsets or hash from the library it was linked against. It only stores a library name and symbol names, which is the same mechanism it uses with
weak-node-apitoday:DT_NEEDEDtaken from the library'sSONAMEExperiments with dummy libraries, run on macOS and natively on an arm64 API 37 emulator. On Android they ran from a shell process, not inside an app's linker namespace:
dlopen, and so does a renamed library.What this means across Hermes versions:
napi_envandnapi_valueare opaque, and public structs are covered by Node-API's stability promise. That promise doesn't extend tonode_api_*experimental functions orhermes_napi_*.NAPI_VERSIONcome from thenode_api.hthe addon compiles against, not from the linked library..acopies the code into the addon, which would give it a second VM.Getting a library to link against
The name recorded comes from inside the library, not its file name, so a plain symlink or copy of today's shim doesn't work: the addon still records
libweak-node-api.soor@rpath/weak-node-api.framework/weak-node-api, with no error. On Android, linking a library with noSONAMEby absolute path records the build machine's path, also silently. CMake'sIMPORTED_NO_SONAMEavoids that.-undefined dynamic_lookup,-flat_namespaceGenerated stub. Generate a stub from
getNodeApiFunctions(), the listweak-node-apialready has, with the library name as a parameter. On Android, compile a small C file with the NDK clang and-soname <name>(llvm-ifsis in the NDK and might avoid compiling; not tried). On Apple, write a.tbd, which needs no compiler. It lives in a temp directory, so it can't be bundled into the addon. Bothcargo(-L,-l) and CMake (an imported target) can take the name at build time. Thereact-nativepackage reference then only determines the library name. Every symbol in the stub should be checked against the real Hermes, or a mismatch only appears at load time.Donor app. Using a library's example app as the source of Hermes works, since the binary's contents don't matter, but it's heavy for what it provides: a list of names and one library name. Dev builds often produce one ABI or simulator slice, the example app depends on the addon that would need its output, the files sit in hashed Gradle caches or
Pods/, and the donor's Hermes must never be bundled with the addon.Rewrite at app build. Publish against a generic stub with no injection support, then stamp in the real name in the host's
linkstep, which already runsinstall_name_toolandcodesignon Apple. On Android, a long reserved name can be replaced in place with nopatchelf, which isn't in the NDK. Addons must be prepared at build time, with that reserved name on Android and-headerpad_max_install_nameson Apple, since a longer name otherwise fails. Tested on dummy binaries, and a rewritten ELF loads and runs on the emulator. It doesn't help a host with a static engine or a split implementation, and an addon that skips the step fails at load withlibrary not found(confirmed on the emulator).Prebuilt facade. The addon links a stub named
node-api, and never mentions Hermes. A facade with that name is an ordinary CMake project built bycmake-rn --no-weak-node-api-linkage, andlinkembeds it like any other module, so CocoaPods and Gradle only drive the existing step. The facade links a stub of Hermes rather than Hermes itself, so it can be compiled once and shipped, with no Hermes at build time. The spike did this on both platforms:libnode-api.so@rpath/node-api.framework/node-apiEvery published addon records this name, so it's effectively a stable ABI contract and can't easily change once prebuilds ship.
node-apinames the interface rather than an engine or this project, which is also what core would need to adopt. It's close to the host's existinglibnode-api-host.so, but nothing on the Android or iOS system paths checked uses it, and Node's own shared library islibnode.weak-node-apican also be built under the neutral name, one addon binary could work with either backend, and the choice moves to the app. Mode 5 could then replace mode 1 for addons, leavingweak-node-apias one backend the app can choose.Unlike
weak-node-api, a facade has no forwarding functions, no table and no injection step. The loader resolves each symbol once, and the addon holds the engine's own address:-reexported_symbols_listdoes that with no code: in a test the library had an empty__textsection, the addon received the same function address as Hermes, and a symbol outside the list failed to link. The list comes fromgetNodeApiFunctions(), so only Node-API is exposed. An empty library at that name fails withSymbol not found, and a missing one withLibrary not loaded. A symlink to Hermes also ran on macOS, but whether it loads as a single image or survives iOS signing and App Store validation is unknown. A copy must not be used, as it would likely mean two VMs.DT_NEEDEDon Hermes, because bionic's breadth-first local group makes Hermes reachable. ELF has no per-symbol re-export, so the stub is what limits an addon to Node-API at link time. Removing theDT_NEEDEDmakes the load fail.The spike ran this in the test app on the iOS simulator and an Android emulator. See its findings in the comments.
Flat lookup.
-undefined dynamic_lookupand-flat_namespacework on macOS with no stub or library, but both are rejected. They don't constrain addons to the Node-API surface, so a typo or a non-Node-API symbol compiles and only fails on a device, which is a worse developer experience. They're also deprecated on iOS. In the case inspected, binding was lazy, so a missing symbol probably fails at call time. That's why they're rejected for iOS and Android. Mode 3 stays as an option for Node.js targets (#217), where the runtime is the host and this is the usual way to build addons.Blockers
Stock React Native can't do this yet.
static_hhasAPI/napiand enables it by default, but it is on neithermainnor any release. Hermes260318099.0.5(used by React Native 0.88) exports nonapi_*,node_api_*orhermes_napi*symbols on arm64 Android or iOS. So until #412 clears, mode 2 only works with our vendored Hermes. Shipping Node-API is one of the RFC's asks.A
react-nativeinstall doesn't contain Hermes.react-native@0.88.1on npm has no binaries, only podspecs, build scripts andsdks/hermes-engine/version.properties. The library comes from Maven on Android (com.facebook.hermes:hermes-android) and from a tarball fetched bypod installon Apple, or from our vendored build. Pointing at a package identifies which Hermes, but doesn't supply it. A generated stub removes this blocker for linking, though the app still needs a Hermes with Node-API at runtime.Addons compile against
node-api-headers, which defines the interface.hermes_napi.hdeclares embedder API and isn't meant for addons, so it stays internal to whatever provides the napi host.React Native drops the reason for Hermes fatal errors on the New Architecture. We want to delete the host's
napi_fatal_error(#492), so that shim and direct-linked addons behave the same, but that's blocked upstream by react/react-native#59728, with a proposed fix in react/react-native#59729. Mode 2 and 5 addons bypass the host's override, so they would hit this too.Open questions
-headerpad_max_install_namesfrom the start, to keep the rewrite option open?weak-node-apihere and possibly by node-api: use v-table to reverse module dependencies nodejs/node#60916 later. That PR is still open and unreleased.Related
#447, #494, #495, #496, #491, #492, react/react-native#59728, #412, #428, #217, #4
All reactions