From 507943b5c8dd3485493c661813e5535241863777 Mon Sep 17 00:00:00 2001 From: hexbinoct Date: Thu, 13 Aug 2026 13:15:59 +0500 Subject: [PATCH 1/3] build: delay-load node.exe imports on Windows 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 --- CMakeLists.txt | 8 ++++++++ src/win_delay_load_hook.cc | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/win_delay_load_hook.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index cf67540..286ab29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,14 @@ function(add_node_api_cts_addon ADDON_NAME) endif() target_include_directories(${ADDON_NAME} PRIVATE ${NODE_API_HEADERS_DIR}) target_link_libraries(${ADDON_NAME} PRIVATE ${NODE_API_LIB}) + if(MSVC) + # Delay-load the node.exe imports and resolve them against the host + # process at runtime, so the addons can be loaded by any Node-API host + # executable regardless of its file name (the node-gyp approach). + target_sources(${ADDON_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/win_delay_load_hook.cc) + target_link_libraries(${ADDON_NAME} PRIVATE delayimp) + target_link_options(${ADDON_NAME} PRIVATE "/DELAYLOAD:NODE.EXE") + endif() target_compile_features(${ADDON_NAME} PRIVATE cxx_std_17) target_compile_definitions(${ADDON_NAME} PRIVATE ADDON_NAME=${ADDON_NAME}) endfunction() diff --git a/src/win_delay_load_hook.cc b/src/win_delay_load_hook.cc new file mode 100644 index 0000000..65a0557 --- /dev/null +++ b/src/win_delay_load_hook.cc @@ -0,0 +1,39 @@ +/* + * When this file is linked to a DLL, it sets up a delay-load hook that + * intervenes when the DLL is trying to load 'node.exe' dynamically. Instead + * of trying to locate the .exe file it'll just return a handle to the + * process image. + * + * This allows the test addons to load into any Node-API host executable, + * regardless of its file name. Same approach as node-gyp's + * win_delay_load_hook.cc. + */ + +#ifdef _MSC_VER + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include + +#include +#include + +static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) { + HMODULE m; + if (event != dliNotePreLoadLibrary) + return NULL; + + if (_stricmp(info->szDll, "node.exe") != 0) + return NULL; + + // Prefer libnode.dll to support a Node.js built as a shared library. + m = GetModuleHandle(TEXT("libnode.dll")); + if (m == NULL) m = GetModuleHandle(NULL); + return (FARPROC) m; +} + +decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = load_exe_hook; + +#endif From a1394efba13dea6da148426d8566be234cc4055a Mon Sep 17 00:00:00 2001 From: hexbinoct Date: Fri, 14 Aug 2026 19:35:04 +0500 Subject: [PATCH 2/3] build: also intercept libnode.dll in the delay-load hook Covers addons linked against a shared-library Node import lib. In this repo's own build the generated import library is NAME NODE.EXE, so the new name is inert here; /ignore:4199 silences LNK4199 for whichever of the two names an import table does not reference (node-gyp does the same). --- CMakeLists.txt | 6 +++++- src/win_delay_load_hook.cc | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 286ab29..df7be0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,9 +44,13 @@ function(add_node_api_cts_addon ADDON_NAME) # Delay-load the node.exe imports and resolve them against the host # process at runtime, so the addons can be loaded by any Node-API host # executable regardless of its file name (the node-gyp approach). + # libnode.dll covers addons linked against a shared-library Node build; + # /ignore:4199 silences the warning for whichever of the two names the + # import library does not reference (node-gyp does the same). target_sources(${ADDON_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/win_delay_load_hook.cc) target_link_libraries(${ADDON_NAME} PRIVATE delayimp) - target_link_options(${ADDON_NAME} PRIVATE "/DELAYLOAD:NODE.EXE") + target_link_options(${ADDON_NAME} PRIVATE + "/DELAYLOAD:NODE.EXE" "/DELAYLOAD:LIBNODE.DLL" "/ignore:4199") endif() target_compile_features(${ADDON_NAME} PRIVATE cxx_std_17) target_compile_definitions(${ADDON_NAME} PRIVATE ADDON_NAME=${ADDON_NAME}) diff --git a/src/win_delay_load_hook.cc b/src/win_delay_load_hook.cc index 65a0557..456556d 100644 --- a/src/win_delay_load_hook.cc +++ b/src/win_delay_load_hook.cc @@ -1,8 +1,8 @@ /* * When this file is linked to a DLL, it sets up a delay-load hook that - * intervenes when the DLL is trying to load 'node.exe' dynamically. Instead - * of trying to locate the .exe file it'll just return a handle to the - * process image. + * intervenes when the DLL is trying to load 'node.exe' or 'libnode.dll' + * dynamically. Instead of trying to locate the file it'll just return a + * handle to the process image (or to libnode.dll if the host loaded one). * * This allows the test addons to load into any Node-API host executable, * regardless of its file name. Same approach as node-gyp's @@ -25,7 +25,8 @@ static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) { if (event != dliNotePreLoadLibrary) return NULL; - if (_stricmp(info->szDll, "node.exe") != 0) + if (_stricmp(info->szDll, "node.exe") != 0 && + _stricmp(info->szDll, "libnode.dll") != 0) return NULL; // Prefer libnode.dll to support a Node.js built as a shared library. From c5b5de0b1b569be9f8f8fd5a077bd0737afcc9ce Mon Sep 17 00:00:00 2001 From: hexbinoct Date: Fri, 14 Aug 2026 20:25:26 +0500 Subject: [PATCH 3/3] build: drop the libnode.dll module preference from the hook GetModuleHandle("libnode.dll") cannot fire for the hosts the suite runs against, none of which load a shared libnode.dll, and node-gyp's own hook resolves every delay-loaded name to the process image the same way. The libnode.dll name match stays, so an addon whose import table names it is still redirected rather than left to the loader. Signed-off-by: hexbinoct --- src/win_delay_load_hook.cc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/win_delay_load_hook.cc b/src/win_delay_load_hook.cc index 456556d..8f1bb99 100644 --- a/src/win_delay_load_hook.cc +++ b/src/win_delay_load_hook.cc @@ -2,7 +2,7 @@ * When this file is linked to a DLL, it sets up a delay-load hook that * intervenes when the DLL is trying to load 'node.exe' or 'libnode.dll' * dynamically. Instead of trying to locate the file it'll just return a - * handle to the process image (or to libnode.dll if the host loaded one). + * handle to the process image. * * This allows the test addons to load into any Node-API host executable, * regardless of its file name. Same approach as node-gyp's @@ -21,7 +21,6 @@ #include static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) { - HMODULE m; if (event != dliNotePreLoadLibrary) return NULL; @@ -29,10 +28,7 @@ static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) { _stricmp(info->szDll, "libnode.dll") != 0) return NULL; - // Prefer libnode.dll to support a Node.js built as a shared library. - m = GetModuleHandle(TEXT("libnode.dll")); - if (m == NULL) m = GetModuleHandle(NULL); - return (FARPROC) m; + return (FARPROC) GetModuleHandle(NULL); } decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = load_exe_hook;