Skip to content

Commit 5d8d37c

Browse files
authored
Windows Electron: NodeAPI.dll fails to load due to missing delay hook (#3)
### Description When using node-swift to build native modules for Electron on Windows, the module fails to load because `NodeAPI.dll` imports from `node.exe` using delay-load, but the delay-load hook that redirects `node.exe` to the actual host executable is not linked into `NodeAPI.dll`. ### Root Cause The delay-load hook (`win_delay_load_hook.cc`) is part of the `NodeModuleSupport` target, which gets linked into the final native module (e.g., `NativeRecording.node`). However, when `NodeAPI` is built as a dynamic library (`NodeAPI.dll`), it's this DLL that actually imports from `node.exe` - not the final module. Since `NodeAPI.dll` doesn't include the delay-load hook, when the DLL loader tries to resolve `node.exe` imports, it fails because: 1. The host executable is named differently (e.g., `electron.exe`) 2. There's no hook to redirect the import to the current process ### Technical Details - `NodeAPI.dll` has `node.exe` in its delay import table (correctly configured with `/DELAYLOAD:node.exe`) - `__pfnDliNotifyHook2` in `NodeAPI.dll` is `NULL` (no hook registered) - The hook exists in `NativeRecording.node` but that doesn't help since `NodeAPI.dll` is the one importing from `node.exe` ### Proposed Fix Create a separate target `CNodeDelayLoadHook` containing only the delay-load hook code, and make `NodeAPI` depend on it (Windows only): ```swift // In Package.swift targets array: .target( name: "CNodeDelayLoadHook", path: "Sources/CNodeDelayLoadHook", sources: ["win_delay_load_hook.cc"], publicHeadersPath: "include", linkerSettings: [ .linkedLibrary("delayimp", .when(platforms: [.windows])), ] ), // Modify NodeAPI target: .target( name: "NodeAPI", dependencies: [ "CNodeAPI", "CNodeAPISupport", "NodeAPIMacros", .target(name: "CNodeDelayLoadHook", condition: .when(platforms: [.windows])), ], linkerSettings: [ .unsafeFlags(["-Xlinker", "/DELAYLOAD:node.exe"], .when(platforms: [.windows])), ] ), ``` Note: We cannot simply make `NodeAPI` depend on `NodeModuleSupport` because that creates a circular dependency (`NodeModuleSupport/register.c` calls `node_swift_register` which is defined in `NodeAPI`). ### Additional Notes The existing `win_delay_load_hook.cc` uses `#ifdef _MSC_VER` which excludes Clang. Since Swift on Windows uses Clang (targeting MSVC ABI), the guard should be changed to `#if defined(_WIN32)` or the separate hook file should omit this guard entirely.
2 parents 267d7dc + 7c74799 commit 5d8d37c

7 files changed

Lines changed: 90 additions & 5 deletions

File tree

Package.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ let package = Package(
4646
]
4747
),
4848
.target(name: "CNodeAPISupport"),
49+
.target(
50+
name: "CNodeDelayLoadHook",
51+
path: "Sources/CNodeDelayLoadHook",
52+
sources: ["win_delay_load_hook.cc"],
53+
publicHeadersPath: "include",
54+
linkerSettings: [
55+
.linkedLibrary("delayimp", .when(platforms: [.windows])),
56+
]
57+
),
4958
.macro(
5059
name: "NodeAPIMacros",
5160
dependencies: [
@@ -55,7 +64,15 @@ let package = Package(
5564
),
5665
.target(
5766
name: "NodeAPI",
58-
dependencies: ["CNodeAPI", "CNodeAPISupport", "NodeAPIMacros"]
67+
dependencies: [
68+
"CNodeAPI",
69+
"CNodeAPISupport",
70+
"NodeAPIMacros",
71+
.target(name: "CNodeDelayLoadHook", condition: .when(platforms: [.windows])),
72+
],
73+
linkerSettings: [
74+
.unsafeFlags(["-Xlinker", "/DELAYLOAD:node.exe"], .when(platforms: [.windows])),
75+
]
5976
),
6077
.target(
6178
name: "NodeModuleSupport",

Sources/CNodeDelayLoadHook/include/.keep

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
(The MIT License)
2+
3+
Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net>
4+
5+
Permission is hereby granted, free of charge, to any person
6+
obtaining a copy of this software and associated documentation
7+
files (the "Software"), to deal in the Software without
8+
restriction, including without limitation the rights to use,
9+
copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following
12+
conditions:
13+
14+
The above copyright notice and this permission notice shall be
15+
included in all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24+
OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// from node-gyp (see node_gyp_LICENSE)
2+
3+
/*
4+
* When this file is linked to a DLL, it sets up a delay-load hook that
5+
* intervenes when the DLL is trying to load the host executable
6+
* dynamically. Instead of trying to locate the .exe file it'll just
7+
* return a handle to the process image.
8+
*
9+
* This allows compiled addons to work when the host executable is renamed.
10+
*/
11+
12+
#if defined(_WIN32)
13+
14+
#pragma managed(push, off)
15+
16+
#ifndef WIN32_LEAN_AND_MEAN
17+
#define WIN32_LEAN_AND_MEAN
18+
#endif
19+
20+
#include <windows.h>
21+
22+
#include <delayimp.h>
23+
#include <string.h>
24+
25+
static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) {
26+
HMODULE m;
27+
if (event != dliNotePreLoadLibrary)
28+
return NULL;
29+
30+
if (_stricmp(info->szDll, "node.exe") != 0)
31+
return NULL;
32+
33+
m = GetModuleHandle(NULL);
34+
return (FARPROC) m;
35+
}
36+
37+
decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = load_exe_hook;
38+
39+
#pragma managed(pop)
40+
41+
#endif

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{
2-
"name": "node-swift",
3-
"version": "1.4.0",
2+
"name": "@circleback/node-swift",
3+
"version": "1.4.1",
44
"repository": "kabiroberai/node-swift",
5+
"publishConfig": {
6+
"access": "restricted"
7+
},
58
"description": "Support for creating Node modules in Swift",
69
"bin": "./lib/cli.js",
710
"main": "./lib/builder.js",

0 commit comments

Comments
 (0)