I am out of thoughts on the cause of the problem after 3 days of investigating and researching. Basically I am loading a hello world Node JS addon compiled with MinGW64 and linked against C node-api.
The code is as follows:
// hello.c
#include <node/node_api.h>
napi_value Method(napi_env env, napi_callback_info args)
{
napi_value greeting;
napi_status status = napi_create_string_utf8(env, "hello, asshole", NAPI_AUTO_LENGTH, &greeting);
return status == napi_ok ? greeting : (napi_value)0;
}
napi_value init(napi_env env, napi_value exports)
{
napi_value function;
napi_status status = napi_create_function(env, 0, 0, &Method, 0, &function);
if (status != napi_ok)
return (napi_value)0;
status = napi_set_named_property(env, exports, "hello", function);
return status == napi_ok ? exports : (napi_value)0;
}
// static void _register_hello(void)__attribute((constructor));
// calls napi_module_register
NAPI_MODULE(hello, init)
I have downloaded dist headers and precompiled node.lib
which I link against. This is my CMakeLists.txt
file:
cmake_minimum_required(VERSION 3.11.4 FATAL_ERROR)
project(hello-node-api LANGUAGES C CXX)
add_library(hello SHARED hello.c)
# TODO: download dist and make an imported target
target_include_directories(hello PRIVATE node-v16.2.0/include)
target_link_libraries(hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/node-v16.2.0/node.lib)
set_target_properties(hello PROPERTIES
SUFFIX ".node"
PREFIX ""
)
target_compile_definitions(hello PUBLIC BUILDING_NODE_EXTENSION)
Node.exe is compiled with exported symbols, that are provided within the node.lib
.
Node.exe loads an addon and a dll entry point (on Windows it is __DllMainCRTStart
) is called.
Either of the dll versions (compiled either with MSVC or MinGW) are loaded just fine: the entry point is called, and all the user-defined functions are callable without errors. But an attempt to invoke an imported napi_
function results in access violation with MinGW.
Exception thrown at 0x0000000000009238 in node.exe: 0xC0000005: Access violation executing location 0x0000000000009238.
With MSVC api functions are entered normally and the addon gets registered etc.
First I thought that the problem is caused by the compiler, but a dummy exe(MSVC)->addon(MinGW)->exe_symbols(MSVC) works just fine regardless of the compiler used to build an exe: extern "C"
is used to export symbols:
Export header:
// esport.h
#pragma once
#ifndef BUILDING
#define EXPORT_API __declspec(dllimport)
#else
#define EXPORT_API __declspec(dllexport)
#endif
EXPORT_API void hello_addon();
EXPORT_API int sum(int a, int b);
Addon:
// addon.c
#include <export.h>
static void print_hello_from_export(void)__attribute((constructor));
void print_hello_from_export(void)
{
int res = sum(4, 15);
hello_addon();
}
exe:
extern "C" {
#include "export.h"
}
#include <iostream>
void hello_addon()
{
std::cout << "hello addon" << std::endl;
}
int sum(int a, int b)
{
return a + b;
}
#include <windows.h>
int main()
{
HMODULE handle = LoadLibraryA("addon.dll");
return 0;
}
So, the library is loaded and a message is printed, regardless of the compiler used to build the executable. This behavior is expected when using the C ABI.
I tried to look through the symbols within the hello.node
binary but I don't know what to do with this info.
...
[894](sec 1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000001410 __CTOR_LIST__
[895](sec 8)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000000014 _head_lib64_libkernel32_a
[896](sec 8)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000000150 __imp_napi_module_register
...
Here is the Ninja script generated by cmake: MSVC build
build CMakeFiles\hello.dir\hello.c.obj: C_COMPILER__hello_Debug C$:\dev\repos\hello-node-api\hello.c || cmake_object_order_depends_target_hello
DEFINES = -DBUILDING_NODE_EXTENSION -Dhello_EXPORTS
FLAGS = /DWIN32 /D_WINDOWS /W3 /MDd /Zi /Ob0 /Od /RTC1
INCLUDES = -IC:\dev\repos\hello-node-api\node-v16.2.0\include
OBJECT_DIR = CMakeFiles\hello.dir
OBJECT_FILE_DIR = CMakeFiles\hello.dir
TARGET_COMPILE_PDB = CMakeFiles\hello.dir\
TARGET_PDB = hello.pdb
# =============================================================================
# Link build statements for SHARED_LIBRARY target hello
#############################################
# Link the shared library hello.node
build hello.node hello.lib: C_SHARED_LIBRARY_LINKER__hello_Debug CMakeFiles\hello.dir\hello.c.obj | C$:\dev\repos\hello-node-api\node-v16.2.0\node.lib
LANGUAGE_COMPILE_FLAGS = /DWIN32 /D_WINDOWS /W3 /MDd /Zi /Ob0 /Od /RTC1
LINK_FLAGS = /machine:x64 /debug /INCREMENTAL
LINK_LIBRARIES = C:\dev\repos\hello-node-api\node-v16.2.0\node.lib kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib
OBJECT_DIR = CMakeFiles\hello.dir
POST_BUILD = cd .
PRE_LINK = cd .
RESTAT = 1
TARGET_COMPILE_PDB = CMakeFiles\hello.dir\
TARGET_FILE = hello.node
TARGET_IMPLIB = hello.lib
TARGET_PDB = hello.pdb
MinGW build
#############################################
# Order-only phony target for hello
build cmake_object_order_depends_target_hello: phony || CMakeFiles/hello.dir
build CMakeFiles/hello.dir/hello.c.obj: C_COMPILER__hello_Debug C$:/dev/repos/hello-node-api/hello.c || cmake_object_order_depends_target_hello
DEFINES = -DBUILDING_NODE_EXTENSION -Dhello_EXPORTS
DEP_FILE = CMakeFiles\hello.dir\hello.c.obj.d
FLAGS = -g
INCLUDES = -IC:/dev/repos/hello-node-api/node-v16.2.0/include
OBJECT_DIR = CMakeFiles\hello.dir
OBJECT_FILE_DIR = CMakeFiles\hello.dir
# =============================================================================
# Link build statements for SHARED_LIBRARY target hello
#############################################
# Link the shared library hello.node
build hello.node libhello.dll.a: C_SHARED_LIBRARY_LINKER__hello_Debug CMakeFiles/hello.dir/hello.c.obj | C$:/dev/repos/hello-node-api/node-v16.2.0/node.lib
LANGUAGE_COMPILE_FLAGS = -g
LINK_LIBRARIES = C:/dev/repos/hello-node-api/node-v16.2.0/node.lib -lkernel32 -luser32 -lgdi32 -lwinspool -lshell32 -lole32 -loleaut32 -luuid -lcomdlg32 -ladvapi32
OBJECT_DIR = CMakeFiles\hello.dir
POST_BUILD = cd .
PRE_LINK = cd .
RESTAT = 1
TARGET_FILE = hello.node
TARGET_IMPLIB = libhello.dll.a
TARGET_PDB = hello.node.dbg
I can't spot any significant differences in compilation and linking.
I also noticed that visual studio project generated to build Node has a distinct target to build node.lib
. I don't know if it matters, but here is the command line args from .vsproj
/Yu"node_pch.h" /MP /GS /W3 /wd"4351" /wd"4355" /wd"4800" /wd"4251" /wd"4275" /wd"4267" /Zc:wchar_t /I"src" /I"out\Debug\obj\global_intermediate" /I"out\Debug\obj\global_intermediate\include" /I"out\Debug\obj\global_intermediate\src" /I"tools\msvs\genfiles" /I"deps\histogram\src" /I"deps\uvwasi\include" /I"deps\v8\include" /I"deps\icu-small\source\i18n" /I"deps\icu-small\source\common" /I"deps\zlib" /I"deps\llhttp\include" /I"deps\cares\include" /I"deps\uv\include" /I"deps\nghttp2\lib\includes" /I"deps\brotli\c\include" /I"deps\openssl\openssl\include" /I"deps\ngtcp2" /I"deps\ngtcp2\ngtcp2\lib\includes" /I"deps\ngtcp2\ngtcp2\crypto\includes" /I"deps\ngtcp2\nghttp3\lib\includes" /Z7 /Gm- /Od /Fd"out\Debug\obj\libnode\libnode.pdb" /FI"node_pch.h" /Zc:inline /fp:precise /D "V8_DEPRECATION_WARNINGS" /D "V8_IMMINENT_DEPRECATION_WARNINGS" /D "_GLIBCXX_USE_CXX11_ABI=1" /D "WIN32" /D "_CRT_SECURE_NO_DEPRECATE" /D "_CRT_NONSTDC_NO_DEPRECATE" /D "_HAS_EXCEPTIONS=0" /D "BUILDING_V8_SHARED=1" /D "BUILDING_UV_SHARED=1" /D "OPENSSL_NO_PINSHARED" /D "OPENSSL_THREADS" /D "OPENSSL_NO_ASM" /D "NODE_ARCH=\"x64\"" /D "NODE_WANT_INTERNALS=1" /D "V8_DEPRECATION_WARNINGS=1" /D "NODE_OPENSSL_SYSTEM_CERT_PATH=\"\"" /D "HAVE_INSPECTOR=1" /D "HAVE_ETW=1" /D "FD_SETSIZE=1024" /D "NODE_PLATFORM=\"win32\"" /D "NOMINMAX" /D "_UNICODE=1" /D "NODE_USE_V8_PLATFORM=1" /D "NODE_HAVE_I18N_SUPPORT=1" /D "HAVE_OPENSSL=1" /D "UCONFIG_NO_SERVICE=1" /D "U_ENABLE_DYLOAD=0" /D "U_STATIC_IMPLEMENTATION=1" /D "U_HAVE_STD_STRING=1" /D "UCONFIG_NO_BREAK_ITERATION=0" /D "NGHTTP2_STATICLIB" /D "NGTCP2_STATICLIB" /D "NGHTTP3_STATICLIB" /D "DEBUG" /D "_DEBUG" /D "V8_ENABLE_CHECKS" /errorReport:prompt /GF /WX- /Zc:forScope /RTC1 /Gd /Oy- /MTd /FC /Fa"out\Debug\obj\libnode\" /nologo /Fo"out\Debug\obj\libnode\" /Fp"out\Debug\obj\libnode\libnode.pch" /diagnostics:column
I am out of ideas, on the web there are some fruitless attempts to load a mingw-compiled addon. Some of them are from several years ago and still no result. So I ask the community for help in order to solve this issue or at least understand why it can't be solved.
So it boils down to:
node.exe
?I decided to look into the possibility that the loaded (who exactly loads it? Is it __DllMainRTCStartup
?) addresses differ from the addresses within the node.exe
While inside the node.exe
:
0x00007ff629d198e0 {node.exe!napi_module_register(napi_module *)}
Disassembly:
// Registers a NAPI module.
void napi_module_register(napi_module* mod) {
00007FF629D198E0 mov qword ptr [rsp+8],rcx
00007FF629D198E5 push rsi
00007FF629D198E6 push rdi
00007FF629D198E7 sub rsp,0C8h
00007FF629D198EE mov rdi,rsp
00007FF629D198F1 mov ecx,32h
00007FF629D198F6 mov eax,0CCCCCCCCh
00007FF629D198FB rep stos dword ptr [rdi]
00007FF629D198FD mov rcx,qword ptr [mod]
...
But when inside the hello.node
:
identifier "napi_module_register" is undefined
- I don't know if it is expected.
Disassembly:
static void _register_hello(void) __attribute((constructor));
static void _register_hello(void)
{
00007FFC02F81479 push rbp
00007FFC02F8147A mov rbp,rsp
00007FFC02F8147D sub rsp,20h
napi_module_register(&_module);
00007FFC02F81481 lea rcx,[7FFC02F83020h]
00007FFC02F81488 mov rax,qword ptr [7FFC02F89150h]
00007FFC02F8148F call rax
}
00007FFC02F81491 nop
00007FFC02F81492 add rsp,20h
00007FFC02F81496 pop rbp
00007FFC02F81497 ret
call rax
leads to 0000000000009238 ?? ??????
and then access violation is thrown.
It looks like the import table for node.exe
is empty:
There is an import table in .idata at 0xb32a9000
The Import Tables (interpreted .idata section contents)
vma: Hint Time Forward DLL First
Table Stamp Chain Name Thunk
00009000 00009084 00000000 00000000 000092a0 00009170
DLL Name: node.exe
vma: Hint/Ord Member-Name Bound-To
And for dll compiled with MSVC it is NOT EMPTY:
Dump of file .\hello.node
File Type: DLL
Section contains the following imports:
node.exe
18000E1F8 Import Address Table
18000E5E8 Import Name Table
0 time date stamp
0 Index of first forwarder reference
6707 napi_set_named_property
66A4 napi_create_function
66F3 napi_module_register
66AD napi_create_string_utf8
Looks like it mught be the reasin after all.
Can an empty import address table be the reason?
This is a legit workaround, that provides some "temporary" solution. It involves explicit loading of symbols from the calling application, which has to be node.exe
.
Though it does work, I am still looking for a conventional solution to correctly load imported symbols implicitly.
#include <node/node_api.h>
#include <windows.h>
static void (*pRegisterModule)(napi_module*);
static napi_status (*pCreateStringUtf8)(napi_env, const char*, size_t, napi_value *);
static napi_status (*pCreateFunction)(napi_env, const char*, size_t, napi_callback, void*, napi_value*);
static napi_status (*pSetNamedProperty)(napi_env, napi_value, const char*, napi_value);
napi_value Method(napi_env env, napi_callback_info args)
{
napi_value greeting;
napi_status status = // napi_create_string_utf8(env, "hello workaround", NAPI_AUTO_LENGTH, &greeting);
pCreateStringUtf8(env, "hello workaround", NAPI_AUTO_LENGTH, &greeting);
return status == napi_ok ? greeting : (napi_value)0;
}
napi_value init(napi_env env, napi_value exports)
{
napi_value function;
napi_status status = // napi_create_function(env, 0, 0, &Method, 0, &function);
pCreateFunction(env, 0, 0, &Method, 0, &function);
if (status != napi_ok)
return (napi_value)0;
status = // napi_set_named_property(env, exports, "hello", function);
pSetNamedProperty(env, exports, "hello", function);
return status == napi_ok ? exports : (napi_value)0;
}
// static void _register_hello(void)__attribute((constructor));
// calls napi_module_register
//NAPI_MODULE(hello, init)
static napi_module _module = {NAPI_MODULE_VERSION,
0,
__FILE__,
init,
"hello",
(void*)0,
{0}};
typedef void(WINAPI *void_func_ptr_t)(void);
static void _register_hello(void) __attribute((constructor));
static void _register_hello(void)
{
// TODO: load pointers from calling exe
*(void**)&pRegisterModule = GetProcAddress(GetModuleHandleW(0),
"napi_module_register");
*(void**)&pCreateStringUtf8 = GetProcAddress(GetModuleHandleW(0),
"napi_create_string_utf8");
*(void**)&pCreateFunction = GetProcAddress(GetModuleHandleW(0),
"napi_create_function");
*(void**)&pSetNamedProperty = GetProcAddress(GetModuleHandleW(0),
"napi_set_named_property");
pRegisterModule(&_module);
// napi_module_register(&_module);
}
User contributions licensed under CC BY-SA 3.0