From cd581b4b14df3491fdec7d69439a3c38437e64a8 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 16 Sep 2026 15:28:21 -0700 Subject: [PATCH 1/2] Node-API: declare the source-URL napi_run_script as a C++ overload; implement it for Hermes The shared header replaced Node's three-argument napi_run_script with the Babylon four-argument form (source URL) inside extern "C". Hermes ships its own Node-API and exports the standard three-argument symbol, so Env::RunScript(script, url) bound to it and passed the URL where Hermes expected the result pointer: an access violation on Win32 and a SIGSEGV on Android for any node-addon-api RunScript call. Restore the standard declaration in the C block and declare the source-URL form as a C++ overload after it. The other engines already define both (their three-argument definitions get C linkage, the four-argument ones C++ linkage; nothing else changes), and the Hermes bridge now defines the overload through hermes_run_script, so Env::RunScript works there too. NodeApi.RunScriptWithSourceUrl covers every engine that has Env::RunScript. --- .../Node-API/Include/Engine/Hermes/napi/env.h | 10 ++--- .../Include/Shared/napi/js_native_api.h | 15 ++++++- Core/Node-API/Source/env_hermes.cc | 45 ++++++++++++++++--- Tests/UnitTests/Shared/Shared.cpp | 18 ++++++++ 4 files changed, 75 insertions(+), 13 deletions(-) diff --git a/Core/Node-API/Include/Engine/Hermes/napi/env.h b/Core/Node-API/Include/Engine/Hermes/napi/env.h index ac806a4b..f0a429b1 100644 --- a/Core/Node-API/Include/Engine/Hermes/napi/env.h +++ b/Core/Node-API/Include/Engine/Hermes/napi/env.h @@ -15,12 +15,10 @@ namespace Napi void Detach(Napi::Env env); // Compile and execute UTF-8 source on the current Hermes runtime. - // `sourceUrl` is attached to stack traces. Unlike the other engines - // we don't go through `Env::RunScript` because Hermes's standard - // `napi_run_script` is the canonical 3-argument signature, while the - // shared header carries a Babylon-specific 4-argument variant with a - // `source_url` parameter that Hermes doesn't provide. Instead we call - // Hermes's `hermes_run_script` directly inside the engine TU. + // `sourceUrl` is attached to stack traces. This calls Hermes's + // `hermes_run_script` directly with a zero-copy buffer; `Env::RunScript` + // works too, through the source-URL overload of `napi_run_script` that + // env_hermes.cc defines. Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl); // Pump Hermes's job queue (drains microtasks and pending finalizers). diff --git a/Core/Node-API/Include/Shared/napi/js_native_api.h b/Core/Node-API/Include/Shared/napi/js_native_api.h index 961b30f2..7a607235 100644 --- a/Core/Node-API/Include/Shared/napi/js_native_api.h +++ b/Core/Node-API/Include/Shared/napi/js_native_api.h @@ -490,10 +490,8 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_is_promise(napi_env env, bool* is_promise); // Running a script -// [BABYLON-NATIVE-ADDITION] NAPI_EXTERN napi_status NAPI_CDECL napi_run_script(napi_env env, napi_value script, - const char* source_url, napi_value* result); // Memory management @@ -631,4 +629,17 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_object_seal(napi_env env, EXTERN_C_END +#ifdef __cplusplus +// [BABYLON-NATIVE-ADDITION] +// Runs a script with a source URL for stack traces and diagnostics. This is a C++ overload on +// purpose: engines that ship their own Node-API implementation (Hermes) export the standard +// three-argument napi_run_script above with C linkage, and declaring this four-argument form +// with C linkage made Env::RunScript bind to that symbol and pass the URL where the engine +// expected the result pointer. +NAPI_EXTERN napi_status NAPI_CDECL napi_run_script(napi_env env, + napi_value script, + const char* source_url, + napi_value* result); +#endif + #endif // SRC_JS_NATIVE_API_H_ diff --git a/Core/Node-API/Source/env_hermes.cc b/Core/Node-API/Source/env_hermes.cc index c3a97db0..62461ebe 100644 --- a/Core/Node-API/Source/env_hermes.cc +++ b/Core/Node-API/Source/env_hermes.cc @@ -20,11 +20,9 @@ // `` chain FIRST so: // * the Napi:: C++ wrappers (`Napi::Env`, `Napi::Value`, `Napi::Error`) // line up with the rest of the project, -// * the Babylon-extended 4-arg `napi_run_script` declaration matches the -// inline `Env::RunScript` body in napi-inl.h (which we never actually -// call in this engine — Napi::Eval below routes through -// `hermes_run_script` directly — so the linker is never asked to find -// the 4-arg symbol). +// * the Babylon source-URL overload of `napi_run_script` (a C++ overload in +// the shared header) resolves to the definition below, which runs through +// `hermes_run_script`; the standard 3-arg C entry point stays Hermes's own. // // We keep `NAPI_VERSION` at the shared default (5) so the inline wrappers // in napi-inl.h that target newer NAPI revisions (e.g. @@ -97,6 +95,43 @@ namespace } } +// The source-URL overload of napi_run_script (a C++ overload, see js_native_api.h). Hermes +// exports only the standard three-argument entry point, so Env::RunScript(script, url) lands here +// and runs through hermes_run_script like Napi::Eval below; the standard form binds to Hermes's own +// symbol. Before the overload had C++ linkage, that call bound to the three-argument symbol and +// passed the URL where Hermes expected the result pointer. +napi_status napi_run_script(napi_env env, napi_value script, const char* source_url, napi_value* result) +{ + if (env == nullptr || script == nullptr || result == nullptr) + { + return napi_invalid_arg; + } + + size_t length = 0; + napi_status status = napi_get_value_string_utf8(env, script, nullptr, 0, &length); + if (status != napi_ok) + { + return status; + } + + // hermes_run_script takes ownership of the buffer and wants the terminator counted in `size`. + const size_t size = length + 1; + auto* source = new uint8_t[size]; + status = napi_get_value_string_utf8(env, script, reinterpret_cast(source), size, &length); + if (status != napi_ok) + { + delete[] source; + return status; + } + + hermes_run_script_flags flags{}; + flags.struct_size = sizeof(flags); + const auto finalize = [](const uint8_t* data, size_t /*size*/, void* /*hint*/) { + delete[] data; + }; + return hermes_run_script(env, source, size, finalize, /*finalize_hint=*/nullptr, source_url, &flags, result); +} + namespace Napi { Napi::Env Attach() diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 1c7e9ff7..69ebb624 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -829,6 +829,24 @@ TEST(NodeApi, AdjacentEscapableScopesEscapeIndependently) #endif +#if !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI) +TEST(NodeApi, RunScriptWithSourceUrl) +{ + // Regression: Env::RunScript(script, url) calls the source-URL overload of napi_run_script. + // On Hermes that call used to bind to the engine's standard three-argument C symbol and hand + // it the URL where it expected the result pointer (an access violation on every host). + Babylon::AppRuntime runtime{}; + + std::promise evaluated; + runtime.Dispatch([&evaluated](Napi::Env env) { + const auto sum = env.RunScript("40 + 2", "run-script-with-url.js"); + evaluated.set_value(sum.IsNumber() && sum.As().Int32Value() == 42); + }); + + EXPECT_TRUE(evaluated.get_future().get()); +} +#endif + int RunTests() { testing::InitGoogleTest(); From 84f8836ea43c8e61f37ac7c1e1c56c995bdec555 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 17 Sep 2026 09:42:50 -0700 Subject: [PATCH 2/2] Correct Hermes script buffer ownership comment --- Core/Node-API/Include/Engine/Hermes/napi/env.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/Node-API/Include/Engine/Hermes/napi/env.h b/Core/Node-API/Include/Engine/Hermes/napi/env.h index f0a429b1..a964d164 100644 --- a/Core/Node-API/Include/Engine/Hermes/napi/env.h +++ b/Core/Node-API/Include/Engine/Hermes/napi/env.h @@ -15,9 +15,9 @@ namespace Napi void Detach(Napi::Env env); // Compile and execute UTF-8 source on the current Hermes runtime. - // `sourceUrl` is attached to stack traces. This calls Hermes's - // `hermes_run_script` directly with a zero-copy buffer; `Env::RunScript` - // works too, through the source-URL overload of `napi_run_script` that + // `sourceUrl` is attached to stack traces. This calls Hermes's + // `hermes_run_script` with an owned source copy; `Env::RunScript` works + // too, through the source-URL overload of `napi_run_script` that // env_hermes.cc defines. Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl);