Skip to content

Calling napi_ functions from MinGW-compiled addon results in access violation #38782

Closed

Description

@sergio-eld
  • 1.62.0:
  • Windows 10:

What steps will reproduce the bug?

  • Compile a simple hello-world addon dll with MinGW64 linking to C node-api.
  • Launch node.exe and load addon require("path_to_addon/hello.nod")

If the compiled with MSVC, everything is fine, you can then call hello.hello().
With MinGW64 dll is loaded as expected via the system call to LoadLibraryExW. All the user-defined code can be invoked just fine with expected behavior. But an attempt to call an imported napi_ function results in access violation.
If you dynamically load napi_ functions from the process's address (uncommented code in hello.c), everything works. I failed to find the reason why it failed to properly link to node.lib.

This is my CMakeLists.txt

cmake_minimum_required(VERSION 3.11.4 FATAL_ERROR)

project(hello-node-api LANGUAGES C CXX)

add_library(hello SHARED hello.c)

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)

The addon's source. Uncommented code loads napi_ functions dynamically. In case you want to try it, comment out NAPI_MODULE macro.

// hello.c

#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, asshole", NAPI_AUTO_LENGTH, &greeting);
//            pCreateStringUtf8(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);
//            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;
}

NAPI_MODULE(hello, init)
//static napi_module _module = {NAPI_MODULE_VERSION,
//                           0,
//                           __FILE__,
//                           init,
//                           "hello",
//                           (void*)0,
//                           {0}};
//
//static void _register_hello(void) __attribute((constructor));
//static void _register_hello(void)
//{
//    // loading 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);
//}

What is the expected behavior?

Node API exports C functions, so the are expected to be available no matter the compiler was used.

Additional information

I didn't notice any significant differences in compile and linker flags. I tried to reproduce this problem with MSVC by disabling default flags, but everything was fine still.

I also asked a question on SO, but no answers so far.

Activity

  1. sergio-eld commented on May 23, 2021

    @sergio-eld
    Author

    I've compared import address tables for both versions, and here is what I have found:
    MSVC-compiled hello.node

    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
    

    MinGW-compiled hello.node

    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 000092c0 00009180
    
    	DLL Name: node.exe
    	vma:  Hint/Ord Member-Name Bound-To
    

    As can be seen the second one is empty...
    I guess it might actually be the reason. But I am still missing on the difference between linker and compiler flags

  2. NickNaso commented on Jul 21, 2021

    @NickNaso
    Member

    Hi @ElDesalmado,
    in the past I had similar problem and I'm working to make it easy to solve. The first step was to create a repository that contains the Node-API headers here: https://github.com/nodejs/node-api-headers with Node-API you don't need to download all the Node.js include files.

    addon.cc

    #include <assert.h>
    #include <node_api.h>
    
    static napi_value Method(napi_env env, napi_callback_info info) {
        napi_status status;
        napi_value world;
        status = napi_create_string_utf8(env, "world", 5, &world);
        assert(status == napi_ok);
        return world;
    }
    
    #define DECLARE_NAPI_METHOD(name, func)                                        \
      { name, 0, func, 0, 0, 0, napi_default, 0 }
    
    static napi_value Initialize(napi_env env, napi_value exports) {
        napi_status status;
        napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
        status = napi_define_properties(env, exports, 1, &desc);
        assert(status == napi_ok);
        return exports;
    }
    
    extern "C" {
         NAPI_EXTERN  napi_value napi_register_module_v1(napi_env env, napi_value exports) {
            return (napi_value) Initialize(env, exports);
        }
    }

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.16)
    
    project(addon_mingw)
    
    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_NODE_API_LIB libnode_api.a)
    
    include_directories(${CMAKE_SOURCE_DIR}/include)
    
    set(SOURCE_FILES
            ${CMAKE_CURRENT_SOURCE_DIR}/addon.cc
            )
    
    add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES})
    set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
    target_link_libraries(${PROJECT_NAME} ${CMAKE_SOURCE_DIR}/lib/${CMAKE_NODE_API_LIB})
    
    # Define NAPI_VERSION
    add_definitions(-DNAPI_VERSION=3)

    In my CMakeLists.txt file you can see a library called libnode_api.a that I created from the list of symbols exported by Node-API. This is possible thanks to node-api-headers that is also a module (not yet published on npm) that you can install directly from git npm i https://github.com/nodejs/node-api-headers .
    Then you can use node-api-headers to build the node-api lib for example using a script like this:

    'use strict'
    
    const { promisify } = require('util');
    const exec = promisify(require('child_process').exec);
    const { symbols } = require('node-api-headers')
    const { writeFile } = require('fs').promises
    
    async function main() {
        const node_api_symbols = symbols.v8.node_api_symbols
        let node_api_lib = 'LIBRARY node.exe\n'
        node_api_lib += 'EXPORTS\n'
        node_api_lib += node_api_symbols.join('\n')
        await writeFile('./node_api.def', node_api_lib)
        await exec(' dlltool -d ./node_api.def -y libnode_api.a')
    }
    main().catch(err => console.error(err))

    The dlltool is part of MinGW.
    I hope that this info could help you.

  3. sergio-eld commented on Jul 22, 2021

    @sergio-eld
    Author

    @NickNaso The problem is caused by a bug within the MinGW linker in binutils. It corrupts the import table for some reason when there are a lot of mangled C++ symbols in an export lib provided by MSVC. It results in an empty import table.
    So, what you did is a solid workaround - you regenerate the export lib with only C API (n_api) functions. I admire your effort and certainly your solution is helpful.
    However, given that you still use MinGW binutils, it is not necessary to mess with node-api headers etc. All you have to do is to get all the C API symbols exported from node.exe (you could use regex, for instance) and generate an export lib. Simple python script would suffice.
    However "however", there is a third approach. Since MinGW and Clang on Windows can link directly to a binary, there might be no need for an export lib period. But this produces unresolved issues due to node.exe being basically an executable. Also, CMake on Windows does not support direct linking to a binary when using targets with target_link_libraries.

    To sum up: you did a good job.

  4. mhdawson commented on Sep 3, 2021

    @mhdawson
    Member

    Looks like the question has been answered, closing. Please let us know if you think that was not the right thing to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    node-apiIssues and PRs related to Node-API.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions