c++node.jselectronelectron-forgenode-addon-api

Node Addon not loadable in Electron


I try to build my own node addon written in C++. It works well when I load them in a javascript executed in node (node test.js).

As soon as I load them in the Electron environment, it fails wit the Exception:

Uncaught Exception: Error: A dynamic link library (DLL) initialization routine failed.

I know, that Electron use its own implementation of the node.lib library. So, I link to this library instead of the node.js library. But something seams to be wrong.

My project is based on cmake and I link directly to the node.lib. Bellow is a minimal working example to reproduce the issue. I build the addon in visual studio which creates the helloJS.node file. I packed the Electron application with npm run package and copy the binary manually to out/test-win32-x64/resources/app/electron. When I try to start the out/test-win32-x64/test.exe I got the error.

What do I miss? Is there some additional compilation flags required, or do I use the wron node library? It is from https://artifacts.electronjs.org/

./CMakeLists.txt

cmake_minimum_required (VERSION 3.15)
cmake_policy(SET CMP0091 NEW)
cmake_policy(SET CMP0042 NEW)

project(hello
    VERSION 0.0.1 
    DESCRIPTION "hello world for node-addon in pure cmake"
)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

# download Node library and create a cMake target including headers and linking information
include(FetchNode)
FetchNode("Electron" 22.0.0 67af8ac8af0db4ed1fe7af1a8583f1dae9f459be0a37ae9279381a2996cb8426 190aa8035756ea448870547c8123b959031657f739843e6950013df04c0dd119)

# source files
file(GLOB ADDON_SRC_FILES
    "*.cpp"
    "*.hpp"
)

add_definitions(-DNAPI_VERSION=6) 
add_library(helloJS SHARED 
    ${ADDON_SRC_FILES}
)
set_target_properties(helloJS PROPERTIES 
    PREFIX "" 
    SUFFIX ".node"
)
target_link_libraries(helloJS Node)

./hello.hpp

#include <napi.h>

class HelloJS : public Napi::Addon<HelloJS> {
public:
    HelloJS(Napi::Env env, Napi::Object exports);
private:
    Napi::Value sayHello(const Napi::CallbackInfo& info);
};

./hello.cpp

#include "hello.hpp"
#include <iostream>

HelloJS::HelloJS(Napi::Env env, Napi::Object exports) {
    std::cout << "HelloJS::HelloJS called" << std::endl;
    DefineAddon(exports, {
      InstanceMethod("sayHello", &HelloJS::sayHello)
    });
}

Napi::Value HelloJS::sayHello(const Napi::CallbackInfo& info) {
    std::cout << "HelloJS::sayHello called" << std::endl;
    std::string hey = "hello world!";
    return Napi::String::New(info.Env(), hey);
}

NODE_API_ADDON(HelloJS)

./test/test.js

var coreJS = require('./HelloJS');
console.log(coreJS.sayHello());

works well, when it is executed with node test.js

./test/helloJS.node

this file is the build output used in test.js

./electron/package.json

{
  "name": "test",
  "version": "0.1.0",
  "description": "test Application",
  "main": "electron/electron.js",
  "homepage": "./",
  "author": "your-name",
  "license": "MIT",
  "private": true,
  "devDependencies": {
    "@electron-forge/cli": "^6.0.4",
    "@electron-forge/maker-deb": "^6.0.4",
    "@electron-forge/maker-rpm": "^6.0.4",
    "@electron-forge/maker-squirrel": "^6.0.4",
    "@electron-forge/maker-zip": "^6.0.4",
    "electron": "^22.0.0"
  },
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "bindings": "^1.5.0",
    "electron-squirrel-startup": "^1.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron": "electron .",
    "package": "react-scripts build && electron-forge package"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

./electron/electron.js

const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url');

const coreJS = require('./HelloJS');

const createWindow = () => {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600
  })
  mainWindow.loadFile(path.join(__dirname, '/../build/index.html'))
  
  // Open the DevTools.
  mainWindow.webContents.openDevTools();
}

app.whenReady().then(() => {
  createWindow()
  
  app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

./cmake/FetchNode.cmake

# File: FetchNode.cmake
# FetchNodeJS(<NODE_TYPE> <NODE_VERSION> <LIBRARY_SHA256> <HEADERS_SHA256>)
#       <NODE_TYPE>         in: type of requested node library (NodeJS or Electron)
#       <NODE_VERSION>      in: requested node version (node JS version or electron version) (e.g. 18.12.1)
#       <LIBRARY_SHA256>    in: sha256 hash of the node.lib file
#       <HEADERS_SHA256>    in: sha256 hash of headers tar file
#
# The function downloads the node library, its header files as well as the the node-addon-api headers.
# As output it defines a cMake Target **Node**
cmake_minimum_required (VERSION 3.18)

if(__FETCHNODE_INCLUDED)
  return()
endif()
set(__FETCHNODE_INCLUDED TRUE)

include(FetchContent)

function(FetchNode node_TYPE node_VERSION node_LibHash node_HdrHash)
  string(COMPARE EQUAL "${node_TYPE}" "NodeJS" _cmp_NodeJS)
  string(COMPARE EQUAL "${node_TYPE}" "Electron" _cmp_Electron)
  if (_cmp_NodeJS)
    set(NODE_URL "https://nodejs.org/dist/v${node_VERSION}")
    set(NODE_LIBRARY "${NODE_URL}/win-x64/node.lib")
    set(NODE_HEADER "${NODE_URL}/node-v${node_VERSION}-headers.tar.gz")
  elseif(_cmp_Electron)
    set(NODE_URL "https://artifacts.electronjs.org/headers/dist/v${node_VERSION}")
    set(NODE_LIBRARY "${NODE_URL}/win-x64/node.lib")
    set(NODE_HEADER "${NODE_URL}/node-v${node_VERSION}.tar.gz")
  else()
    message(FATAL_ERROR "Unknown Node Type in FetchNode function!")
  endif()

  FetchContent_Declare(nodeLib
    URL ${NODE_LIBRARY}
    URL_HASH SHA256=${node_LibHash}
    DOWNLOAD_NO_EXTRACT true
  )
  FetchContent_Declare(nodeHdr
    URL ${NODE_HEADER}
    URL_HASH SHA256=${node_HdrHash}    
  )
  FetchContent_Declare(nodeAddonAPI
    GIT_REPOSITORY https://github.com/nodejs/node-addon-api.git
    GIT_TAG v5.0.0
  )

  FetchContent_MakeAvailable(nodeLib nodeHdr nodeAddonAPI)

  add_library(Node STATIC IMPORTED)
  set_target_properties(Node PROPERTIES IMPORTED_LOCATION ${nodelib_SOURCE_DIR}/node.lib)
  target_include_directories(Node INTERFACE ${nodehdr_SOURCE_DIR}/include/node ${nodeaddonapi_SOURCE_DIR})

endfunction()

Solution

  • I got the solution of my question and will post them here, if anyone comes to the same issue.

    For electron, a hook is required as documented here: https://www.electronjs.org/docs/latest/tutorial/using-native-node-modules#a-note-about-win_delay_load_hook

    I added the source file to my project and updated the CMakeLists.txt:

    cmake_minimum_required (VERSION 3.15)
    cmake_policy(SET CMP0091 NEW)
    cmake_policy(SET CMP0042 NEW)
    
    project(hello
        VERSION 0.0.1 
        DESCRIPTION "hello world for node-addon in pure cmake"
    )
    
    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
    
    # download Node library and create a cMake target including headers and linking information
    include(FetchNode)
    FetchNode("Electron" 22.0.0 67af8ac8af0db4ed1fe7af1a8583f1dae9f459be0a37ae9279381a2996cb8426 190aa8035756ea448870547c8123b959031657f739843e6950013df04c0dd119)
    
    # source files
    set(DELAY_LOAD_HOOK "hook/win_delay_load_hook.cc")
    file(GLOB ADDON_SRC_FILES
        "*.cpp"
        "*.hpp"
    )
    
    add_definitions(-DNAPI_VERSION=6) 
    set (CMAKE_SHARED_LINKER_FLAGS "/DELAYLOAD:NODE.EXE")
    add_library(helloJS SHARED 
        ${ADDON_SRC_FILES}
        ${DELAY_LOAD_HOOK}
    )
    set_target_properties(helloJS PROPERTIES 
        PREFIX "" 
        SUFFIX ".node"
    )
    target_link_libraries(helloJS Node delayimp)
    

    After all this changes its possible to build the node-addon directly in the CMake project without additional external dependencies.