Skip to content

Commit 5b47314

Browse files
kraenhansenclaude
andcommitted
vendor-hermes: export public hermes_napi_* entry points
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>
1 parent 98869f5 commit 5b47314

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default tseslint.config(
1010
globalIgnores([
1111
"**/dist/**",
1212
"**/build/**",
13+
"**/build-tests/**",
1314
"apps/test-app/ios/**",
1415
"apps/macos-test-app/**",
1516
"packages/host/hermes/**",

packages/host/src/node/cli/hermes.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,41 @@ const platformOption = new Option(
2828
"The React Native package to vendor Hermes into",
2929
).default("react-native");
3030

31+
// The public `hermes_napi_*` entry points (e.g. `hermes_napi_create_env`) are
32+
// declared in `hermes_napi.h` with `NAPI_EXTERN` (visibility "default") but —
33+
// unlike the sibling `js_native_api.h` / `node_api.h` headers — without any
34+
// `extern "C"` wrapping. So they get C++ linkage: the symbols are name-mangled
35+
// and, because Hermes builds with a global `-fvisibility=hidden`, stay out of
36+
// the shared framework's export table. We reach `hermes_napi_create_env` from
37+
// the host module, so consumers linking the framework hit "Undefined symbol".
38+
//
39+
// Wrapping the declarations in `EXTERN_C_START` / `EXTERN_C_END` (both already
40+
// available via the `node_api.h` include) gives them C linkage, so they export
41+
// under their unmangled C names. This mirrors facebook/hermes#2106.
42+
const HERMES_NAPI_HEADER = "API/napi/hermes_napi.h";
43+
const HERMES_NAPI_INCLUDE = '#include "hermes/napi/node_api.h"';
44+
const HERMES_NAPI_ENDIF = "#endif // HERMES_NAPI_HERMES_NAPI_H";
45+
46+
async function patchHermesNapiVisibility(hermesPath: string) {
47+
const headerPath = path.join(hermesPath, HERMES_NAPI_HEADER);
48+
const contents = await fs.promises.readFile(headerPath, "utf8");
49+
if (contents.includes("EXTERN_C_START")) {
50+
return;
51+
}
52+
assert(
53+
contents.includes(HERMES_NAPI_INCLUDE) &&
54+
contents.includes(HERMES_NAPI_ENDIF),
55+
`Cannot patch ${HERMES_NAPI_HEADER}: expected anchors not found (did the pinned Hermes commit change?)`,
56+
);
57+
const patched = contents
58+
.replace(
59+
HERMES_NAPI_INCLUDE,
60+
`${HERMES_NAPI_INCLUDE}\n\nEXTERN_C_START`,
61+
)
62+
.replace(HERMES_NAPI_ENDIF, `EXTERN_C_END\n\n${HERMES_NAPI_ENDIF}`);
63+
await fs.promises.writeFile(headerPath, patched);
64+
}
65+
3166
export const command = new Command("vendor-hermes")
3267
.argument("[from]", "Path to a file inside the app package", process.cwd())
3368
.option("--silent", "Don't print anything except the final path", false)
@@ -115,6 +150,9 @@ export const command = new Command("vendor-hermes")
115150
});
116151
}
117152
}
153+
// Applied unconditionally (idempotent) so an existing checkout from
154+
// before this patch also gets fixed.
155+
await patchHermesNapiVisibility(hermesPath);
118156
console.log(hermesPath);
119157
}),
120158
);

0 commit comments

Comments
 (0)