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()
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.