Repository navigation
Add a Node-API host for Hermes - #59894
Draft
kraenhansen wants to merge 13 commits into
Draft
kraenhansen wants to merge 13 commits into
kraenhansen wants to merge 13 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?
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>.HERMES_ENABLE_NAPI=OFF. SetdisableNodeApi=true(Gradle) orRCT_DISABLE_NODE_API=1(CocoaPods, SwiftPM). This definesRCT_DISABLE_NODE_API, which drops the module and its registration, and builds Hermes withHERMES_ENABLE_NAPI=OFF. The RFC covers the naming.Fatal Node-API errors are reported through #59729 and #59861, which this branch is based on.
Binary size: Node-API in Hermes plus the module add 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.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-apiTest 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 newisNodeApiDisabledonesCocoaPods:
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_DISABLE_NODE_API=1. With it, the module's target andReact-Runtime's dependency on it are dropped and the define is added. Not builtRNTester on Android, built with
./gradlew :packages:rn-tester:android:app:installDebug -PreactNativeArchitectures=arm64-v8a(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
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. Hermes was built for the simulator at the pinned commit, with the Node-API headers added to its
destroot. 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 without Node-API on Android (
assembleDebug -PdisableNodeApi=true) and iOS (RCT_DISABLE_NODE_API=1 RCT_USE_PREBUILT_RNCORE=0 HERMES_COMMIT=… pod install, Debug, simulator).hermesvmexports nonapi_*functions, neitherlibhermestooling.sonor the iOS app hasNodeApisymbols, the example addon isn't built, and the example showsNode-API is not available: the JavaScript engine does not implement it, or React Native was built without it. Built again with the defaults, the Maestro flow passes on Android and iOS (CocoaPods)The Maestro flow passes in all three:
maestro test -e APP_ID=<app id> .maestro/node-api.yml(Maestro 2.6.1, as in CI)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