Skip to content

build: delay-load node.exe imports so addons work in any Node-API host on Windows - #83

Open
hexbinoct wants to merge 1 commit into
nodejs:mainfrom
hexbinoct:windows-delayload-hook
Open

build: delay-load node.exe imports so addons work in any Node-API host on Windows#83
hexbinoct wants to merge 1 commit into
nodejs:mainfrom
hexbinoct:windows-delayload-hook

Conversation

@hexbinoct

Copy link
Copy Markdown
Contributor

On Windows the test addons are linked against an import library generated from the
.def files, whose module name is NODE.EXE. Every .node file therefore carries an
import table that binds the napi_* symbols to a module literally named NODE.EXE.
That works when the host process is node.exe, but any Node-API runtime with a
different executable name cannot load the addons at all: the loader has no module
named NODE.EXE to resolve against. In my testing, process.dlopen of a CTS addon
makes Bun 1.3.14 panic with a segfault and makes Deno 2.9.5 crash with 0xC0000005.
The only workaround was renaming the runtime's exe to node.exe, which is not
something a conformance suite should require. Linux and macOS are unaffected because
their loaders resolve undefined symbols against the host executable directly.

This is the same problem node-gyp solved years ago, and this PR applies the same
standard fix: the node.exe imports become delay-loaded (/DELAYLOAD:NODE.EXE plus
delayimp.lib), and a small delay-load hook (src/win_delay_load_hook.cc, modeled on
node-gyp's win_delay_load_hook.cc) resolves the node.exe module to the current
process image via GetModuleHandle(NULL) at runtime, whatever the executable is
called. The hook also prefers libnode.dll when present, matching node-gyp, so a
shared-library Node build works too. The hook and flags are added inside
add_node_api_cts_addon under if(MSVC), so every addon target gets them and
non-Windows builds are untouched.

Verified on Windows 11 with VS 2022:

  • Without the hook: js-native-api/2_function_arguments/test.js under bun.exe
    (1.3.14) panics in process.dlopen (crash report names NODE.EXE), and under
    deno.exe (2.9.5) segfaults with 0xC0000005. Node passes.
  • With the hook: the same test passes under bun.exe and deno.exe running under
    their own names, no rename. A deliberate failing assertion added to the test makes
    both exit non-zero, so the addon code is genuinely executing.
  • Node is unaffected: npm run node:test still passes 48/48.
  • Full sweep under the real-name runtimes matches what the renamed-exe workaround
    produced before: bun 40 pass / 7 fail / 1 timeout, deno 32 pass / 14 fail / 2
    crashes, with the same per-test verdicts. The remaining failures are runtime
    conformance and harness-portability issues unrelated to this change.
  • Linux is a no-op: the Docker build (node:26-bookworm, GCC) plus
    npm run node:test and npm run lint all pass on this branch.

One behavior note: with delay-loading, an addon that references a symbol the host
does not export now fails when the symbol is first called rather than at load time.
For Node itself nothing changes, since all suite symbols come from the .def files
that Node exports.

Claude found this, wrote the fix, ran the verification, and drafted this text;
I reviewed both the fix and the text.

The MSVC-built addons bind their napi_* imports to a module literally
named NODE.EXE, so only a host process named node.exe can load them.
Delay-load those imports and resolve them to the current process image
with a delay-load hook, the same approach node-gyp uses, so any
Node-API host executable can load the addons regardless of its name.

Signed-off-by: hexbinoct <abubakarm@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Need Triage

Development

Successfully merging this pull request may close these issues.

1 participant