Repository navigation
Add a Node-API host for Hermes - #59894
Draft
kraenhansen wants to merge 15 commits into
Draft
kraenhansen wants to merge 15 commits into
kraenhansen wants to merge 15 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves the handler from the Android-only JHermesInstance to the cross-platform HermesInstance, and logs through react_native_log_fatal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reactnativejni, which initializes glog, is only loaded lazily on the New Architecture, so glog output during startup never reached logcat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Temporary, for the Node-API spike: pins Hermes to a static_h commit that has API/napi, builds it from source on Android, and ships the vendored Node-API headers (include/hermes/napi) in the Hermes prefab headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`dlopen(module, filename)` from `react-native/node-api` loads a Node-API addon like Node's `process.dlopen`. It's backed by the NativeNodeApiCxx TurboModule, which hosts async work and thread-safe functions for the engine and leaves env creation and addon loading to a NodeApiEngine. Hermes provides one and registers the module from createJSRuntime, so it's only available when Hermes is the engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A small C addon, built as its own shared library and loaded with `dlopen` from `react-native/node-api`. The example covers a sync call, async work, a thread-safe function, and env cleanup across a reload. Android only for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a React-nodeapinativemodule pod, installed and depended on by React-RuntimeHermes. When Hermes is built from source, its podspec now exposes hermes_napi.h and the Node-API headers in include/hermes/napi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A build phase on the RNTester target builds the example addon into nodeapiexample.framework in the app's Frameworks folder, linking hermesvm, so it can be loaded with dlopen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the sync call, async work, the thread-safe function, and env cleanup across a reload. The example's buttons get plain testIDs, since Maestro matches ids as regular expressions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hand-written declaration, like react-native/unstable-internals-do-not-use, with a type test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a React-nodeapinativemodule target, depended on by React-Runtime, and lets the RNTester example find the Node-API headers in the Hermes artifacts when it isn't built with CocoaPods. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`disableNodeApi` (Gradle) and `RCT_DISABLE_NODE_API=1` (CocoaPods, SwiftPM) leave the Node-API module out of React Native and build Hermes with `HERMES_ENABLE_NAPI=OFF`, when building from source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
javache
reviewed
Nov 12, 2026
| if (hermesHost_ == nullptr) { | ||
| // Leaked: Hermes reads the struct during runtime teardown, after this | ||
| // engine is gone. The host is retained for the same reason. | ||
| hermesHost_ = new hermes_napi_host{ |
Contributor
There was a problem hiding this comment.
Can we fix this in Hermes? Leaking this memory will lead to lot of false signals.
javache
reviewed
Nov 12, 2026
Comment on lines
+75
to
+81
| std::call_once(fatalHandlerFlag, []() { | ||
| auto* fatalHandlerInterface = | ||
| jsi::castInterface<ISetFatalHandler>(makeHermesRootAPI()); | ||
| if (fatalHandlerInterface) { | ||
| fatalHandlerInterface->setFatalHandler(hermesFatalHandler); | ||
| } | ||
| }); |
Contributor
There was a problem hiding this comment.
This is already in another PR, right?
javache
reviewed
Nov 12, 2026
Comment on lines
+84
to
+91
| std::call_once(nodeApiFlag, []() { | ||
| registerCxxModuleToGlobalModuleMap( | ||
| std::string(NativeNodeApi::kModuleName), | ||
| [](std::shared_ptr<CallInvoker> jsInvoker) { | ||
| return std::make_shared<NativeNodeApi>( | ||
| std::make_shared<HermesNodeApiEngine>(), std::move(jsInvoker)); | ||
| }); | ||
| }); |
Contributor
There was a problem hiding this comment.
Can this go in DefaultTurboModules?
Node-API is now off by default. `nodeApiEnabled` (Gradle) and `RCT_NODE_API_ENABLED=1` (CocoaPods, SwiftPM) compile in the module and build Hermes from source with `HERMES_ENABLE_NAPI` on. `dlopen` says how to enable it when it's missing, and the Gradle plugin warns when it's set with the prebuilt AAR, as `pod install` does with the prebuilt `React.xcframework`. RNTester builds without it, so its Maestro flow moves out of the folder CI runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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:
Lets a React Native app load Node-API addons, using the Node-API implementation Hermes already has. This is the working diff for the RFC in react-native-community/discussions-and-proposals#1030, and isn't meant to merge as is: it depends on a Hermes that exports Node-API, which no Hermes release does yet.
dlopen:import {dlopen} from 'react-native/node-api'loads an addon into the calling runtime. It behaves like Node'sprocess.dlopen(module, filename): it setsmodule.exportsand caches nothing.filenameis passed to the system'sdlopen, so core doesn't decide how addons are named, laid out or cached.NativeNodeApiCxxmodule: creates anapi_envperdlopencall and hosts async work and thread-safe functions, dispatched through the runtime'sCallInvoker. Creating the env and loading the addon go through a smallNodeApiEngineinterface, which Hermes implements with its embedder API (hermes_napi.h).HermesInstanceregisters the module, so it's only available with Hermes.hermes-enginepod (built from source) and the SwiftPM build, so addons can#include <node_api.h>.nodeApiEnabled=true(Gradle) orRCT_NODE_API_ENABLED=1(CocoaPods, SwiftPM), and build React Native from source, since the prebuilt artifacts don't include it. This definesRCT_NODE_API_ENABLED, which compiles in the module and its registration, and a Hermes built from source getsHERMES_ENABLE_NAPIto match. When it's missing,dlopen's error says how to enable it, and the Gradle plugin andpod installwarn when it's set with prebuilt React Native. The RFC covers the naming.Fatal Node-API errors are reported through #59729 and #59861, which this branch is based on.
Binary size: enabling Node-API in Hermes and the module adds 131,072 bytes (+0.62%) to a release arm64 RNTester APK and 113,304 bytes (+0.80%) to the iOS app. The RFC has the breakdown and how it was measured.
Not in this PR yet
Addons should link against a
node-apilibrary rather than the engine, so that they don't record Hermes' library name. The plan is for Hermes to ship it next tohermesvm, proposed in meta/hermes#1074 (comment). Addons would then use the same CMake in an app as in a prebuild:Until then, the RNTester example links
hermesvmdirectly, marked with TODOs. The module keeps linkinghermesvm: it's the embedder, and useshermes_napi.h, whichnode-apiwon't export.Not tested in CI
CI builds React Native and RNTester with the defaults, so without Node-API. The Maestro flow is in
packages/rn-tester/NodeApiExample/.maestro/, outside the folder CI's E2E jobs run, and is only run locally with Node-API enabled, as described inNodeApiExample/README.md.RCT_REMOVE_LEGACY_MODULE_INTEROPandRCT_REMOVE_LEGACY_COMPONENT_INTEROP, the other opt-ins that compile React Native's core differently, aren't tested in CI either.Alternatively, CI could enable Node-API in its PR (
dry-run) builds only, and the flow could move back into.maestro/. That meansORG_GRADLE_PROJECT_nodeApiEnabled=trueinbuild-android'sdry-runbranch, andRCT_NODE_API_ENABLED=1inprebuild-ios-coreandtest-ios-rntesterfor non-release runs, with the switch added to the iOS core prebuild's cache keys so nightly and release builds never reuse a cache built with Node-API. Nightly and release artifacts would stay without it, but PR E2E jobs would test a build that differs from the default, and no other opt-in is enabled this way today.Temporary
be8b598661andce3d9c9648are Report Hermes fatal errors on the New Architecture #59729, and3b2c5849acis Initialize glog from rninstance on Android #59861. They drop out once those land.0cbdb0eb11pins Hermes to astatic_hcommit with Node-API and builds it from source on Android (react.internal.useHermesNightly=false).pod installneedsHERMES_COMMIT=$(cat sdks/.hermesv1version).destroot/include, which Hermes'build-apple-framework.shdoesn't copy yet.Open questions
node-apiframework hasn't been tested.hermes_napi.h, Hermes' embedder API, is public in the Hermes artifacts: the Android prefab and thehermes-enginepod'sNodeApiEmbeddersubspec. Only React Native's host needs it, so Hermes might want to ship it separately from the Node-API headers addons use.module.exports, becausehermes_napi_load_moduledoesn't take one: NAPI: hermes_napi_load_module can't take the caller's exports object, unlike Node's process.dlopen meta/hermes#2307.Changelog:
[GENERAL] [ADDED] - Load Node-API addons with
dlopenfromreact-native/node-api, opt-in withnodeApiEnabled(Android) orRCT_NODE_API_ENABLED(iOS)Test Plan:
yarn flow-checkyarn lintyarn build-typesandyarn test-generated-typescript, including a type test forreact-native/node-apiyarn test: no Jest tests were added, and the suite wasn't runGradle plugin:
./gradlew :react-native-gradle-plugin:test --tests '*ProjectUtilsTest*', 39 tests passed, including the newisNodeApiEnabledones. The whole plugin suite: 250 of 251 pass.PreparePrefabHeadersTaskTest.preparePrefabHeadersTask_declaresTheHeadersItCopiesAsInputsfails comparing/var/…with/private/var/…temp paths on macOS, in a task this PR doesn't touchCocoaPods:
utils-test.rb(run on its own), 81 tests. The newset_node_api_flagstests pass.test_SetGCCPreprocessorDefinitionForHermes_itSetsThePreprocessorForDebugfails, and fails on the base commit tooSwiftPM:
swift package dump-packagewith and withoutRCT_NODE_API_ENABLED=1. Without it, the module's target andReact-Runtime's dependency on it are left out; with it, they're included and the define is addedRNTester on Android, built with
./gradlew :packages:rn-tester:android:app:assembleDebug -PreactNativeArchitectures=arm64-v8a -PnodeApiEnabled=true(Hermes from source at the pinned commit) and run on an arm64 API 37 emulator:hello() → "Hello from Node-API",add(2, 3) → 5squareAsync(7) → 49pthreadtick 1,2and3 from threadenvStats() → {"created":1,"cleanedUp":0}DevSettings.reload()envStats() → {"created":2,"cleanedUp":1}, and async work still completesRNTester on iOS (CocoaPods): same results. Installed with
RCT_NODE_API_ENABLED=1 HERMES_COMMIT=$(cat ../react-native/sdks/.hermesv1version) pod install, built in Debug, and run on an iPhone 17 Pro simulator (iOS 26.2).RNTester on iOS (SwiftPM): same results, on the same simulator, before Node-API became opt-in. Not run again since: it now needs
RCT_NODE_API_ENABLED=1when prebuilding React Native. Hermes was built for the simulator at the pinned commit, with the Node-API headers added to itsdestroot. React Native was prebuilt against it withHERMES_ENGINE_TARBALL_PATH=… RN_DEP_VERSION=nightly node scripts/ios-prebuild, and RNTester was converted withnpx react-native spm add --deintegratefor the run only, as the SwiftPM CI job does.Built with the defaults on Android (
assembleDebug) and iOS (HERMES_COMMIT=… pod install, Debug, simulator): Hermes is built withHERMES_ENABLE_NAPIoff,hermesvmexports nonapi_*functions (155 with Node-API enabled), neitherlibhermestooling.sonor the iOS app hasNodeApisymbols, the example addon isn't built, and the example showsNode-API is not available: React Native was built without it. Enable it with the Gradle property `nodeApiEnabled=true` (Android) or `RCT_NODE_API_ENABLED=1` (iOS), and build React Native from sourceGradle plugin warning: with
-PnodeApiEnabled=true, helloworld (prebuilt AAR) printsWARNING: 'nodeApiEnabled' has no effect on the prebuilt React Native AAR…, and RNTester (React Native built from source) doesn'tWith Node-API enabled, the Maestro flow passes on Android and iOS (CocoaPods):
maestro test -e APP_ID=<app id> NodeApiExample/.maestro/node-api.yml(Maestro 2.6.1)Release build on a physical iPhone
node-api-rntester-android.mp4
node-api-rntester-ios.mp4
node-api-rntester-ios-spm.mp4
🤖 Generated with Claude Code