我是如何在electron上调用dll的导出函数的 - node-addon-api

1. 需求

今天是2024年10月21日,昨天我发了个文章,叫《我是如何在electron上调用dll的导出函数的 - koffi库》。然后昨天晚上我将其应用到项目的时候,发现一个个地去构造 结构体、函数原型 真的太浪费时间了;而且我们的dll里边有具体业务代码,代码几乎都用了std标准库的许多功能(比如 std::string、std::vector ),这些在 ffi 上也很难构造。鉴于这些情况,我又不得不重新想个新方法。

现在我想到的方法是:用 c++ 编写出一个 node 模块,这个 node 模块链接了业务dll的导出函数,然后再将此模块导入进 electron 中。
想象一下,就是 electron 和 业务dll 有了联系,而在此中间搭建这种联系的桥梁就是这个 node 模块。(也就是 中介、枢纽、媒婆)

最后我实践了下,发现这个想法确实可以!因此我打算把其中的经过过程都写下来分享。(因为这是我所想到的一种思路,以后出现了什么坑或者问题啥的,就还会回来补充的)


额外说明:把业务代码直接迁移到 node 模块上其实也是可以的,但是在这篇文章中,我们的设想是为一个已有项目 的c++代码与 electron 进行连接。如果这个 已有项目 本身足够庞大,那么直接把代码迁移到 node 模块上就会显得不太现实,而且没有必要。(至少对于我们的项目来讲,就不会这样去做)

2. 参考

node官方文档:
[C++ addons] https://nodejs.org/docs/latest/api/addons.html
---- 对应中文文档:[C++ 插件] https://nodejs.cn/api/addons.html

[C/C++ addons with Node-API] https://nodejs.org/docs/latest/api/n-api.html
---- 对应中文文档:[C/C++插件(使用 Node-API)] https://nodejs.cn/api/n-api.html

node-addon-api github仓库: https://github.com/nodejs/node-addon-api 【看这个仓库的readme】
node-addon-examples github仓库: https://github.com/nodejs/node-addon-examples

3. 思路

我们先试着调用一个MessageBox

首先在 npm 上全局安装 node-gyp(需要安装 Visual Studio IDEPython):

npm install node-gyp -g

在这里插入图片描述

接着创建一个文件夹作为整体项目文件夹,我这里取名为"addon"

在这里插入图片描述

在此文件夹下用 管理员权限 打开一个cmd,输入npm init,根据它的提示输入相应的信息,最后yes确认,在文件夹下成功生成 package.json

在这里插入图片描述
在这里插入图片描述

接着输入 npm install node-addon-api,将 node-addon-api 安装到当前目录下

在这里插入图片描述
在这里插入图片描述

ok,现在我们分别创建源文件 module.cppbinding.gyp(注意:后者文件名必须只能叫binding.gyp!!!)

//module.cpp

#include <napi.h>
#include <Windows.h>
#include <string>

Napi::Number Method(const Napi::CallbackInfo& info) {
	std::wstring str = L"Hello,World";
	MessageBoxW(NULL, str.c_str(), L"From addon.node", MB_OK);
	Napi::Env env = info.Env();
	return Napi::Number::New(env, 0);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
	exports.Set(Napi::String::New(env, "msg"), Napi::Function::New(env, Method));
	return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

binding.gyp 的内容为:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["module.cpp"],
	  'dependencies': [
		"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
	  ]
	}
  ]
}

其中,"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except", 里边的 node_addon_api_except 要看自身情况使用下面的三者之一:

  1. node_addon_api
  2. node_addon_api_except
  3. node_addon_api_maybe
    在这里插入图片描述

(就我的了解,如果 node模块 里边抛出了 javascript错误,那么仅仅使用node_addon_api的话,抛出错误后并不会终止代码运行,而是会继续执行下去。这时想要终止代码运行,就必须手动返回,即

	if ( ...... ) {
		Napi::Error::New(info.Env(), "node模块报错了").ThrowAsJavaScriptException();
		return;
	}
	......

而使用了node_addon_api_except的话,抛出错误后,代码随即将终止运行,即

	if ( ...... ) {
		Napi::Error::New(info.Env(), "node模块报错了").ThrowAsJavaScriptException();
	}
	......

想具体了解的,可直接看文档说明:https://github.com/nodejs/node-addon-api/blob/main/doc/setup.md

在这里插入图片描述
此时,addon 文件夹应该就是这个样子的了,我们回到cmd窗口,输入 node-gyp configure 并执行
在这里插入图片描述
执行完成后,会发现此时 addon 目录下多出了一个叫 build 的文件夹
在这里插入图片描述
我们进入到 build 文件夹中,打开这个sln文件,并从中找到 module.cpp

在这里插入图片描述
在这里插入图片描述
这就是我们刚刚所输入的模块的cpp代码,我们直接以Release x64的配置下编译

在这里插入图片描述
可以看到,我们的node模块 addon.node 编译成功了(其实这个node模块本质上就是dll,是一个pe文件)
在这里插入图片描述
在这里插入图片描述

接着,我们会把 addon.node 复制到 electron的工程文件夹下,并在electron的入口文件处调用模块的方法

在入口文件顶部导入addon.node,并取名作为addon

import addon from '../../addon.node'

然后在electron app()的准备方法中,调用模块里的msg()并将内容输出到控制台上。我们的预期结果是electron会如期弹出一个系统信息框,并在控制台中输入一个 0

console.log(addon.msg());

在这里插入图片描述
在这里插入图片描述
开始 F5 调试运行:
在这里插入图片描述
在这里插入图片描述
结果正确!我们的node模块运行成功了

4. 从node模块调用外部dll导出函数

我们简单写一个导出dll,项目名就叫 dllTest

//main.cpp

#include <Windows.h>
#include <string>

int AddFunc(int a, int b);
std::string StrAddFunc(std::string a, std::string b);

BOOL WINAPI DllMain(
	HANDLE hinstDLL,
	DWORD dwReason,
	LPVOID lpvReserved
) {
	return TRUE;
}

int AddFunc(int a, int b) {
	return a + b;
}

std::string StrAddFunc(std::string a, std::string b) {
	return a + b;
}

用模块定义文件来导出函数

Export.def的内容为:

LIBRARY
EXPORTS
	AddFunc
	StrAddFunc

在这里插入图片描述

这个dll将导出两个函数,分别是AddFuncStrAddFunc,前者是整数相加,后者是字符串相加。
在这里插入图片描述
编译出两个文件,分别是 dllTest.dlldllTest.lib

在这里插入图片描述

在 module.cpp 文件中链接这个lib文件,并将AddFuncStrAddFunc的函数原型也写上来(我先把 dllTest.lib 复制到了build目录下)
在这里插入图片描述

//module.cpp

#include <napi.h>
#include <Windows.h>
#include <string>

//dllTest 的函数原型
int AddFunc(int a, int b);
std::string StrAddFunc(std::string a, std::string b);

//链接 dllTest.lib
#pragma comment(lib, "C:\\Users\\90965\\Desktop\\addon\\build\\dllTest.lib")

Napi::Number Method_AddFunc(const Napi::CallbackInfo& info) {
	Napi::Number a = info[0].As<Napi::Number>();
	Napi::Number b = info[1].As<Napi::Number>();

	Napi::Env env = info.Env();
	return Napi::Number::New(env, AddFunc(a, b));
}

Napi::String Method_StrAddFunc(const Napi::CallbackInfo& info) {
	Napi::String a = info[0].As<Napi::String>();
	Napi::String b = info[1].As<Napi::String>();

	Napi::Env env = info.Env();
	return Napi::String::New(env, StrAddFunc(a, b));
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
	exports.Set(Napi::String::New(env, "AddFunc"), Napi::Function::New(env, Method_AddFunc));
	exports.Set(Napi::String::New(env, "StrAddFunc"), Napi::Function::New(env, Method_StrAddFunc));
	
	return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

在这里插入图片描述

执行编译,成功得到了addon.node(dllTest.dll是我后面再复制进来的)。此时这个addon.node已经链接了dllTest.dll
在这里插入图片描述这两个文件我们都要复制到electron工程文件夹下,而且 dllTest.dlladdon.node 必须在同一个目录下。
在这里插入图片描述

接着我们更新electron入口代码,从addon调用我们的AddFuncStrAddFunc函数

console.log(addon.AddFunc(6, 8));
console.log(addon.StrAddFunc("Hello", ", World"));

开始 F5 调试运行:
在这里插入图片描述
运行成功了,而且控制台也成功输出了

14
Hello, World

5. vite打包问题

还是以上面的源代码为例。

如果此时我们在electron工程文件夹下直接用 npm run build:win 进行打包的话,会发现此时运行 nodeaddon.exe 程序会弹出错误,错误原因是找不到 dll 文件在哪(也就是上文的 dllTest.dll
在这里插入图片描述
在这里插入图片描述

根据我的排查,addon.node 是能够成功加载进去的,也就是说问题并不出在addon.node,而是出在 dllTest.dll。electron 不知道 dllTest.dll在哪里,因此无法加载它,所以才爆出了这个错误。

目前我只找到了一种只针对已解包的解决办法(也就是不使用那个 setup 程序):在已解包目录下,直接将 dllTest.dll 复制进去即可。
在这里插入图片描述
这时候,程序就能正常运行了

在这里插入图片描述
我们用 x64dbg 调试工具来看一下此时electron程序已加载模块的情况
在这里插入图片描述
调用 dll 代码的可不是渲染器进程,而是 electron 的主进程,也就是这张图片pid为30036的那个进程。
我们附加调试进去,查看模块列表并过滤搜索是否有 dllTest.dll
在这里插入图片描述
找到了,这说明 dllTest.dll 此时确实已经加载到electron程序中了

6. 调试(2025年3月1日 21:44补充)

6.1. 把工程文件夹搞好看点

首先说点题外的hhh,如果你感觉生成node模块的工程文件不好看(比如下面那些..C:等等奇怪的名字)
在这里插入图片描述
现在可以删掉上边那个文件夹 (node_modules)
在这里插入图片描述
工程里的C:不要删,可以改个名字,按照里边的代码功能 我将其改名为_delay
在这里插入图片描述

..是我们的主代码文件夹,肯定不要删,可以改个名字,比如src或者source,这里我改成src
在这里插入图片描述

6.2. 怎么调试node模块

因为node模块(addon.node)的本质是dll,因此怎么调试的dll,就怎么调试的node模块
在将生成的debug版node模块复制到electron项目文件夹下并运行时,直接在Visual Studio下进行附加调试
在这里插入图片描述
在这里插入图片描述
(可以用左边的那个选择窗口,快速找到electron的进程),找到electron的进程之后,就单击右下角的"附加"即可
之后就可以随便使用断点和单步等等调试功能啦

注意!!!!因为我的node模块是在主进程上加载的,所以我是可以使用“选择窗口”来快速找到进程的(因为"选择窗口"找到的就是主进程)。而如果你的node模块是通过预加载脚本加载到渲染器进程上的,自然就用不了"选择窗口"来快速找到进程调试,只能通过一个个调试,看有没有加载调试符号来判断 是不是加载了node模块的那个进程

在这里插入图片描述

6.3. 编译后实时复制

还有一点,如果你vs编译出来的新的addon.node跟electron加载的addon.node不一样,可能断点会不给你下,比如下面这张图
在这里插入图片描述
这时候你一定要记得在vs新编译出来的addon.node要覆盖electron的addon.node,保证两者版本是一致的。

为了方便点,你可以利用生成后事件,编写一句copy的批处理语句并添加到生成后事件里边

copy "D:\electron-cpp-template\addon\build\Debug\addon.node" "D:\electron-cpp-template\electron-app\addon.node" /y

上边,"D:\electron-cpp-template\addon\build\Debug\addon.node"是vs编译出来的路径
"D:\electron-cpp-template\electron-app\addon.node"是electron加载的路径

在这里插入图片描述
应用确定后,之后无论怎么编译,vs都会自动帮你把新编译出来的node模块更新到electron里边啦

7. 打包问题解决!2025/3/18 23:49

感谢 imcholl 大佬的指导:https://blog.csdn.net/yi_zongjishi/article/details/146036125

在上文中,我曾经提到过一个问题,那就是打包electron应用的时候,会存在随行的dll无法一并打包的问题,现在说说解决办法:

在你的electron目录里,应该有electron-builder.yml这个文件
在这里插入图片描述
我们在其后边加上以下内容,即可完成随行文件或目录(不只是dll)的打包:

extraFiles:
	- aaa.dll
	- bbb.dll
	- asdasasdas.txt
	- dir/
	- asd/

就像我这样:
在这里插入图片描述

最后打包即可完成啦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值