Repository navigation
Description
Prove the RFC in #447: an app with no react-native-node-api native host can load an addon, with core providing the napi host and addon loading, and Hermes providing a node-api library to link against.
Steps
- On a branch of a
meta/hermesfork, add thenode-apilibrary to the Hermes build. - On a branch of a
react/react-nativefork, pin that Hermes commit and move the napi host and addon loading into core. Android first. - Build an app from that source, without our native host, with an addon linked against
node-api. - Check that the addon loads, its calls land in Hermes, and the relevant tests pass.
- Publish both branches as draft PRs on the forks, so the RFC has concrete diffs to point at.
The node-api library
A library that forwards to Hermes' Node-API symbols, which addons link against so they never record the engine's library name. At runtime it has no code of its own. It takes over #496's facade, and its names become the stable names that prebuilds made with the node-api stub (#494) record.
- Android: two builds of
libnode-api.sowith the same soname, for the same ABIs aslibhermesvm.so. A stub whose functions all abort goes in the AAR's prefab module, which is only used for linking. An empty library with aDT_NEEDEDonlibhermesvm.sogoes injni/<abi>/, which is packaged into the APK. One library can't do both: the NDK links with--no-undefinedand lld doesn't resolve through a dependency'sDT_NEEDED, while at runtime bionic would pick definitions inlibnode-api.sooverlibhermesvm.so. Prebuilds don't need Hermes' stub, sincecmake-rnandferricbuildnode-api-stub. - Apple:
node-api.xcframework, with the same slices ashermesvm.xcframework(iOS, simulator, Mac Catalyst, tvOS, visionOS) and built by the samecreate_universal_frameworkstep. Each slice re-exports the Node-API symbols of the matchinghermesvmslice. ld64 follows re-exports when linking, so unlike Android, one build works for both linking and runtime. The install name is@rpath/node-api.framework/node-api, and on macOS, where frameworks are versioned bundles,@rpath/node-api.framework/Versions/A/node-api. The prebuild stub has to be an xcframework with the same slices and install names. Each slice holds one framework with one binary, sonode-apican't sharehermesvm's xcframework; the two are built together and shipped side by side in the same Hermes artifact. - Headers: Hermes already vendors the Node.js headers in
include/hermes/napi/(see itsVENDORED.md). The library puts that directory on the include path, so addons can#include <node_api.h>. - Linking: on Apple, addons depend on a
node-apipod or SwiftPM product. On Android, the CMake target name depends on which package provides it (see below).
The Android target name
An addon's CMakeLists.txt is often built both ways: as a prebuild with cmake-rn or ferric, and from source inside an app. If the target has the same name in both, the author writes one target_link_libraries line. If it doesn't, the author needs a conditional, or keeps two configs. Prefab always names targets <package>::<module>, so the name depends on which package the module is in:
| Option | Target | For | Against |
|---|---|---|---|
A module in the Hermes AAR, next to hermesvm |
hermes-engine::node-api |
No extra artifact. Says where it comes from, and matches how React Native links Hermes (hermes-engine::hermesvm). |
Names the engine in every addon. Differs from cmake-rn's node-api target unless cmake-rn adopts the same name. |
A separate node-api AAR published by Hermes |
node-api::node-api |
The same name everywhere, and doesn't name the engine. | One more artifact for Hermes to publish and apps to depend on. |
A node-api package published by React Native, wrapping Hermes' libraries |
node-api::node-api |
The same, and Hermes only adds a module. | React Native re-packages Hermes' output. |
The prefab package name is the Gradle root project name. For the Hermes AAR that's hermes-engine, even though its Maven artifact is hermes-android.
With the first option, addons can still use one name. An addon could alias hermes-engine::node-api to node-api::node-api itself, but React Native provides node-api::node-api by default instead, the same way cmake-rn provides weak-node-api today: by passing node-api_DIR, so that find_package(node-api REQUIRED CONFIG) loads a config file React Native wrote. An addon's CMake is then the same in an app as in a cmake-rn prebuild:
find_package(node-api REQUIRED CONFIG)
target_link_libraries(my_addon PRIVATE node-api::node-api)- Where the files come from: the React Native Gradle plugin takes
hermes-androidfrom the app's dependencies and extracts itsnode-apistub andinclude/hermes/napiinto the app's build directory. It writes anode-api-config.cmakethere that definesnode-api::node-apias an imported shared library, using${ANDROID_ABI}to pick the stub and the extracted headers as its include directory. - How it reaches every build: the plugin passes
-Dnode-api_DIR=<dir>to the app's native build and to every Android library's. It already goes through all libraries from the app (ReactPlugin.kt), including ones that don't apply its plugin, and adds-DRN_STRICT_APIto their native builds (configureStrictCxxApiForLibrary). So this covers libraries with their ownexternalNativeBuild, and their CMake configure tasks have to run after the extraction. - No Hermes dependency in libraries: the files don't come from AGP's prefab, so an addon's library module doesn't need to depend on
hermes-android. Hermes only has to ship the stub and the headers in its AAR, not necessarily as a prefab module.
If this doesn't work out, a CMake dependency provider that handles find_package(node-api) is the alternative (CMake 3.24 and later), at the cost of allowing only one provider per build.
The alias shouldn't go the other way, with cmake-rn defining hermes-engine::node-api for its stub: a prebuild's CMake shouldn't name an engine, since the same addon could be built for another engine or for an Android app that embeds Node-API without React Native.
Who ships it
The plan is that Hermes ships it, since the headers and the symbols it forwards to are both in Hermes. This was proposed to the Hermes team in meta/hermes#1074 (comment). The alternative is React Native shipping it from ReactAndroid and its pods. The RFC presents both:
| Hermes | React Native | |
|---|---|---|
| Owns the stable name | Hermes, which renamed libhermes.so to libhermesvm.so in 0.82 |
React Native, whatever the engine is |
| Headers and export list | Built from the same source as the symbols, so they can't drift apart | Re-packaged from Hermes, and checked on every bump |
| Who benefits | Any Hermes embedder | React Native apps |
| Changes | One Hermes PR and one React Native PR | One React Native PR |
Moving from one to the other only moves the facade's build. Addons don't change, as long as the target they link keeps its name (see "The Android target name").
Neither the stub nor the facade is specific to Hermes. They're still built with Hermes, because each one is tied to a particular hermesvm: the facade names its soname or install name, and the stub has to declare exactly what it exports, for the same ABIs and slices. What can be shared is their generator, which could be maintained outside both projects, for example next to the .def files and symbol lists nodejs/node-api-headers already ships for Windows import libraries. node-api-stub (#501) is a starting point for that, and Hermes, React Native and cmake-rn would all depend on it.
Hermes branch
- The
node-apitargets in Hermes' CMake build: the facade, and on Android the link-time stub, installed next tohermesvmwithinclude/hermes/napi. Inhermes-android, the prefab module points at the stub andjni/<abi>/gets the facade. It's built on top ofstatic_h(51ae2a0 when proposed). If that doesn't work with React Native's JSI, the fallback is the host'sHERMES_GIT_SHA(5a795c9), which has napi: give hermes_napi.h public API C linkage meta/hermes#2106 andJSI_UNSTABLEoff.
React Native branch
The branch is napi-host on kraenhansen/react-native. It's based on react/react-native#59729 and react/react-native#59861, so fatal errors are reported without the host's napi_fatal_error override (#492). Those commits are dropped once they land.
- Pin the Hermes branch's commit in
sdks/.hermesv1version, which the Android source build already downloads asmeta/hermes/tarball/<ref>. On iOS,.hermesv1versionis treated as a tag, so a commit needsHERMES_COMMIT. GitHub probably serves the fork's commit throughmeta/hermes, but that's unverified; if not, useREACT_NATIVE_OVERRIDE_HERMES_DIR. This commit is temporary until a Hermes release has Node-API (Remove Hermes vendoring once React Native ships a Hermes with Node-API #412). - Packaging for
node-api, wrapping the library Hermes builds:- Android: the Gradle plugin extracts the stub and headers from
hermes-android, writesnode-api-config.cmake, and passesnode-api_DIRto every native build (see "The Android target name"). - CocoaPods: a separate
node-apipod. Its headers aren't namespaced under the pod name, so they're added toHEADER_SEARCH_PATHS. Apps get them throughuser_target_xcconfig, asReact-Coredoes. Library pods don't inherit that, so they call adepend_on_node_api(spec)helper, likedepend_on_js_engine, which adds the dependency and the search path. - SwiftPM: a headers-only, library-type binary target like
ReactNativeHeadersserves itsHeaders/without a prefix to whatever depends on thenode-apiproduct. The facade itself is a framework-type xcframework, which can't serve headers without a prefix, so the product has both.
- Android: the Gradle plugin extracts the stub and headers from
- The napi host.
HermesNapiHost(async work, thread-safe functions,CallInvokerdispatch) ported from the host, withoutRuntimeNodeApi.hermes_napi.hstays a private include and isn't exported in prefab or pod headers. dlopen.import { dlopen } from "react-native/node-api", withdlopen(module, filename)working like Node'sprocess.dlopen, including settingmodule.exports, with no cache (Build addon paths in TypeScript and expose dlopen(module, filename) #498). It's a TurboModule spec insrc/private, areact-native/node-apisubpath export, and the regenerated type snapshots. Unflagged, since it's opt-in and its surface follows a de facto standard.- An RNTester example. An addon built in RNTester that links
node-api, covering a sync call, async work, a thread-safe function and a reload. Its JS buildsfilenameitself, which shows path construction stays out of core. To check teardown, the addon counts environments it created and ones whose cleanup hook ran, in process-global counters that surviveDevSettings.reload(). __docs__for the subsystem.
Changes needed here
cmake-rn(Link addons against a generated Hermes stub via--node-api-linkage#501) defines the plainnode-apitarget. It also needsadd_library(node-api::node-api ALIAS node-api), so addons link the same name in a prebuild as in an app.
Left out
requireNodeAddonand its cache, the Babel plugin, the naming scheme and autolinking: these stay in this project.- The
node-apistub for prebuilds (Link addons against a generated Hermes stub instead of weak-node-api #494): it only has to match the names above. - Addon-declared Node-API versions (Support Node-API modules declaring their "Node-API version" #4), which need a Hermes change.
weak-node-api.
Open questions for reviewers
- Should Hermes or React Native ship
node-api? (See above.) - On Android, should
node-api::node-apicome from a separatenode-apiAAR, or from a config file the Gradle plugin writes and passes asnode-api_DIR? (See above.) - Should the React Native PR include Fantom coverage, given it needs a built addon?
- App Store validation of a re-exporting
node-apiframework can't be tested in RNTester (see Prebuilt node-api facade for published addons #496). - Do cleanup hooks run when the runtime is torn down? The reload test answers this.
What this should settle
- What has to move. Something must load the addon as well as host it. Today that path is
require("./addon.node")→ Babel transform →requireNodeAddon(). Core needs one primitive for this, while the Babel plugin,linkand the naming scheme may be able to stay here. - Whether
hermes_napi.hcan stay internal. It declares embedder API such ashermes_napi_create_envandhermes_run_script, so it should be a private dependency of core's napi host, not a public header. Addons get the Node-API headers throughnode-apiin an app, and fromnode-api-headerswhen prebuilding.
What the RFC asks for
- Hermes builds used by React Native export Node-API and ship the
node-apilibrary and the headers ininclude/hermes/napi(Remove Hermes vendoring once React Native ships a Hermes with Node-API #412).static_halready hasAPI/napi, but it isn't onmainor in any release. This spike builds on a pinned commit in the meantime. The library exporting it keeps a stable name, or React Native declares a supported one.Thenode-apilibrary has a fixed name, so a Hermes rename doesn't break published addons.
The headers define the interface, so a mismatch with them is a Hermes bug and needs no separate ask.
The RNTester example links node-api directly, so this no longer depends on #494.
Activity