Skip to content

Adopt Hermes' first-party Node-API (static_h)#372

Draft
kraenhansen wants to merge 9 commits into
mainfrom
kh/adopt-static-h-node-api
Draft

Adopt Hermes' first-party Node-API (static_h)#372
kraenhansen wants to merge 9 commits into
mainfrom
kh/adopt-static-h-node-api

Conversation

@kraenhansen

@kraenhansen kraenhansen commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Warning

Draft / work in progress. iOS simulator only so far. Opening early for visibility on the approach.

Migrates from the Microsoft/kraenhansen/hermes-fork Hermes patch flow to Hermes' first-party Node-API, merged on facebook/hermes branch static_h under API/napi/ (target hermesNapi, NAPI v10, built by default). RN has not pulled this in yet, so Hermes is built from source at a pinned static_h SHA.

What works now

  • Real Node-API env on iOS sim. CxxNodeApiHostModule casts the JSI runtime to IHermes, reads the low-level vm::Runtime* via getVMRuntimeUnsafe(), and creates the env with hermes_napi_create_env(vm, nullptr). The env is owned by the runtime and cached on the module.
  • MOCHA_REMOTE_CONTEXT=allTests14 passing on iOS sim: all node-addon-examples getting-started addons (napi + node-addon-api + Rust ferric-example), plus buffers, async, and a js-native-api node-test.

Linking hermesNapi (no pod-side surgery needed)

The RN hermesvm framework already -force_loads hermesNapi, so all napi_* runtime symbols are exported and hermes_napi_create_env's code is present. The public hermes_napi_* entry points are exported from the framework as long as Hermes is built from a checkout that includes facebook/hermes#2044 "Export public hermes_napi entry points with NAPI macros" — which the pinned SHA already contains. No force_load, no second VM copy, no source patching in this repo. (During bring-up a stale, pre-#2044 vendored checkout briefly stripped the symbol under -fvisibility=hidden; the fix is simply to ensure the vendored checkout is actually at the pinned SHA.)

Clean break

Drops RN 0.79–0.81 support, the kraenhansen/hermes fork tags, JSI-header copying, and rt.createNodeApiEnv(). Targets RN 0.87.0-nightly. Old npm releases keep serving old RN versions.

Remaining before ready for review

  • Bump NAPI_VERSION 8 → 10 and regenerate weak-node-api + WeakNodeApiInjector.cpp (drop the engine/runtime split)
  • --force re-vendor at the pinned SHA on the next cold build so the pod matches the pin
  • Reference hermes_napi_host (CallInvoker + worker pool) for async work / thread-safe functions
  • Android, then macOS
  • Docs + CI cleanup

Test plan

  • npm run build && npm run prettier:check
  • Workspace unit tests (react-native-node-api, cmake-rn, gyp-to-cmake, cmake-file-api)
  • npm run lint
  • iOS sim e2e (MOCHA_REMOTE_CONTEXT=allTests) — 14 passing
  • Android emulator e2e
  • macOS

🤖 Generated with Claude Code

@kraenhansen
kraenhansen force-pushed the kh/adopt-static-h-node-api branch from b101c95 to e9dc7e6 Compare July 13, 2026 20:31
@kraenhansen kraenhansen self-assigned this Jul 13, 2026
@kraenhansen kraenhansen added C++ Apple 🍎 Anything related to the Apple platform (iOS, macOS, Cocoapods, Xcode, XCFrameworks, etc.) Android 🤖 Anything related to the Android platform (Gradle, NDK, Android SDK) Host 🏡 Our `react-native-node-api-modules` package Linking 🔗 Discovering and copying prebuilds from packages into the host MacOS 💻 Anything related to the Apple MacOS platform or React Native MacOS support labels Jul 13, 2026
@kraenhansen
kraenhansen force-pushed the kh/adopt-static-h-node-api branch from 6317f7c to 2338534 Compare July 16, 2026 18:43
kraenhansen and others added 9 commits July 20, 2026 07:52
Begin migrating off the kraenhansen/hermes fork + JSI-patching path toward
Hermes' first-party Node-API (the static_h branch).

- vendor-hermes: shallow-fetch facebook/hermes at pinned static_h SHA
  0ae42446d1ae669508368b0a18e60c789f76735d; drop the JSI-header copy step
- patch-hermes.rb: rely on REACT_NATIVE_OVERRIDE_HERMES_DIR alone to trigger
  build-from-source; drop the no-op BUILD_FROM_SOURCE var and the obsolete
  RCT_USE_PREBUILT_RNCORE / JSI-patch guard
- CxxNodeApiHostModule: stub env=nullptr (real env arrives in Phase 2 via
  hermes_napi_create_env)
- bump react-native to 0.87.0-nightly-20260529-88857d22f (+ test-app deps,
  react-native-test-app 5.x); regenerate lockfile
- RN 0.87 fallout: add @types/babel__core, fix test-app tsconfig extends for
  the tightened @react-native/typescript-config exports map, delete the
  podspec test asserting the removed guard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
react-native-test-app 5.x generates the app's ReactTestApp.xcodeproj under
the nearest node_modules, which in a workspace is the app-local
node_modules (apps/test-app/node_modules/.generated), not the hoisted root.
The workspace can also accumulate stale references to a project under a
different node_modules.

findXcodeProject took the first fileRef unconditionally, which could be the
stale (non-existent) reference or the Pods project. Resolve every app
project reference and pick the first whose project.pbxproj exists on disk,
ignoring Pods.xcodeproj.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bump @rnx-kit/metro-config to ^2.2.4: 2.1.1 called metro-config's
  exclusionList as a bare function, but Metro 0.84 changed that module to a
  { default } export, breaking `react-native start`.
- Gradle wrapper bumped to 9.3.1 by react-native-test-app 5.x's
  configureGradleWrapper during pod install (RN 0.87 alignment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the `env = nullptr` stub in CxxNodeApiHostModule with a real
Node-API environment: cast the JSI runtime to `IHermes`, read the
underlying `vm::Runtime*` via `getVMRuntimeUnsafe()`, and create the env
with `hermes_napi_create_env(vm, nullptr)`. The env is owned by the
runtime and cached on the module (shared across all addons).

This flips the Phase 1 baseline abort (`assert(status == napi_ok)` right
after `napi_create_object(env=nullptr, …)`) green: with
`MOCHA_REMOTE_CONTEXT=allTests` the iOS-sim suite now reports 14 passing
(node-addon-examples getting-started incl. the Rust ferric addon,
buffers, async, and a js-native-api node-test).

Linking note: the RN `hermesvm` framework force-loads `hermesNapi`, and
the public `hermes_napi_*` entry points are exported from it as long as
Hermes is built from a checkout that includes facebook/hermes #2044
("Export public hermes_napi entry points with NAPI macros") — which the
pinned SHA (0ae42446) already contains. No pod-side linker surgery or
source patching is required; just ensure the vendored checkout is
actually at the pinned SHA (a stale pre-#2044 checkout is what stripped
the symbol during bring-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All Node-API symbols are now sourced from Hermes' hermesNapi, so the old
engine (js_native_api → libhermes.so) / runtime (node_api →
libnode-api-host.so) distinction and the hand-maintained
IMPLEMENTED_RUNTIME_FUNCTIONS allow-list are obsolete.

- weak-node-api: getNodeApiFunctions defaults to v10 and no longer computes
  the dead `kind`/`libraryPath` fields; CMake compiles the generated
  weak_node_api.cpp at NAPI_VERSION=10 (145 → 155 symbols, adding the v9/v10
  node_api_* surface).
- generate-injector.mts: bind every symbol (no filter) and emit
  `#include <Versions.hpp>` first so the injector TU also compiles at v10.
- Versions.hpp: guarded bump to NAPI_VERSION 10.

Regenerated (gitignored) WeakNodeApiInjector.cpp + weak-node-api/generated
now expose all 155 symbols incl. TSFN and napi_make_callback. Verified:
build, prettier, lint, workspace unit tests, and the weak-node-api native
build + ctest all pass. iOS e2e pending (rides the cold re-vendor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clean Hermes build at the pinned SHA does NOT export
hermes_napi_create_env (and the other hermes_napi_* entry points). They are
declared in API/napi/hermes_napi.h with NAPI_EXTERN (visibility "default")
but — unlike the sibling js_native_api.h / node_api.h headers — without any
extern "C" wrapping, so they get C++ linkage. The mangled C++ symbols stay
out of the framework's dynamic export table under Hermes' global
-fvisibility=hidden, and a from-scratch build fails at the app link with
"Undefined symbol: hermes_napi_create_env".

vendor-hermes now wraps the hermes_napi.h declarations in
EXTERN_C_START / EXTERN_C_END (both available via the node_api.h include),
giving the entry points C linkage so they export under their unmangled C
names. This mirrors the upstream fix in facebook/hermes#2106. The patch is
idempotent (guarded on EXTERN_C_START) and asserts its anchors exist so a
future Hermes bump fails loudly rather than silently no-op'ing.

Also ignore **/build-tests/** in ESLint (CMake writes compiler_depend.ts
dependency files there that aren't real TypeScript).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RN 0.87 dependency bumps were installed through a private pull-through
registry mirror, which rewrote 61 `resolved` URLs in package-lock.json to
`https://sfw-prod.tail9d0a9e.ts.net/npm/...`. That host is unreachable from
CI, so every `npm ci` failed with ENOTFOUND and took down Lint, all Unit
tests, and all Test app jobs before they could run.

Rewrite the host back to `https://registry.npmjs.org/`. The `integrity`
hashes are content-based and unchanged, so the entries stay valid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the single-argument `.replace()` call in patchHermesNapiVisibility
onto one line to satisfy prettier:check (fixup for the hermes_napi patch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lockfile regeneration in 593373a dropped every non-darwin-arm64
optional platform package for the native-binding families that were
newly added in this PR: `@rolldown/binding-*`, `@napi-rs/lzma-*`,
`@napi-rs/tar-*`, and `@napi-rs/wasm-tools-*`. Only the darwin-arm64
sibling of each survived (the platform the lockfile was regenerated on),
a known npm optional-dependency pruning behaviour.

As a result `npm ci` on Linux and Windows installed no matching binding
and the node-tests bootstrap crashed with:

    Error: Cannot find module '@rolldown/binding-linux-x64-gnu'

which failed the Ubuntu and Windows unit-test jobs (and the npm ci /
bootstrap step of the Linux test-app job).

Re-add the 56 missing optional platform entries at the versions already
pinned in the lockfile, with authoritative resolved/integrity metadata
from the registry. No existing entry is modified and no dependency
version changes. Verified with `npm ci` on Linux: the previously-missing
bindings install and `rolldown` loads its native binding successfully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gg882sXvCmX7ZcJdFh1sGV
@kraenhansen
kraenhansen force-pushed the kh/adopt-static-h-node-api branch from 96ecbea to 1675ecc Compare July 20, 2026 05:52
@owl352

owl352 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Reference hermes_napi_host (CallInvoker + worker pool) for async work / thread-safe functions

I think I can test this part on real module when it's ready. Asynchronous bindings were added in the previous update of our sdk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Android 🤖 Anything related to the Android platform (Gradle, NDK, Android SDK) Apple 🍎 Anything related to the Apple platform (iOS, macOS, Cocoapods, Xcode, XCFrameworks, etc.) C++ Host 🏡 Our `react-native-node-api-modules` package Linking 🔗 Discovering and copying prebuilds from packages into the host MacOS 💻 Anything related to the Apple MacOS platform or React Native MacOS support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants