关于在electron(Nodejs)中使用 Napi 的简单记录
当我们使用
electron
想要集成一个C++ SDK实现很底层的算法逻辑就有可能与C++ SDK进行数据通信。
Napi 应该是比较好的选择,因为C++本身的运行速度很快,使用Napi
也能很大程度上保证软件的兼容性、又不会阻塞C++线程、还可以很简单的与C++ 实现数据传递。
开始使用
- 安装 C++ 运行环境 直接装一个 VS2022 (c++桌面端开发)
- 安装 python 3.8.2 (如果开发electron win32 最好装32位环境)
npm install -g node-gyp
- 新建一个文件夹
addon
(存放demo文件) - 在
addon
内执行npm init -y
- 在
addon
内执行npm install node-addon-api
- 新建
src/addon.cpp
(相当于Napi的入口文件)(在下方) - 新建
binding.gyp
并配置(在下方)
构建好后只需要 build/Release/addon.node 其他的可以删除(如果没有其他 dll 依赖)
我的Demo目录结构
app.js源码 (也就是Nodejs 集成C++ 插件的起始文件)
const addon = require("./build/Release/addon");
function callback(data) {
console.log("接收到C++传出: ", data);
}
console.log(addon.sayHello("Hello C++"));
// 传入回调函数
console.log("传入回调函数结果: ", addon.startSendingData(callback));
addon.cpp 源码 实现接收JS 传入的参数和回调函数 并使用JS传入的回调函数给JS传递数据
#include <napi.h>
#include <thread>
#include "helper.h"
using namespace std;
// 线程安全的回调
Napi::ThreadSafeFunction tsfn;
Napi::Function jsCallback;
// 这个函数用于在后台线程中运行
void BackgroundThread(Napi::ThreadSafeFunction tsfn) {
// 在 JS 线程调用回调
tsfn.BlockingCall([](Napi::Env env, Napi::Function jsCallback) {
jsCallback.Call({Napi::String::New(env, "Hello Nodejs")});
});
// 释放线程安全函数
tsfn.Release();
}
// 启动数据发送的函数
Napi::Value StartSendingData(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// 检查参数,确保传入的是一个回调函数
if (!info[0].IsFunction()) {
Napi::TypeError::New(env, "C++ StartSendingData 回调异常").ThrowAsJavaScriptException();
return env.Undefined();
}
jsCallback = info[0].As<Napi::Function>();
// 创建线程安全的 JS 回调
tsfn = Napi::ThreadSafeFunction::New(env, jsCallback, "ThreadSafeFunction", 0, 1);
// 启动后台线程,传递线程安全的回调
thread(BackgroundThread, tsfn).detach();
return Napi::Boolean::New(env, true);
}
// 测试传参输出的函数
Napi::String SayHello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// 获取传入的名称参数
string name = info[0].As<Napi::String>().Utf8Value();
// 调用外部的 greet 函数
string message = greet(name);
return Napi::String::New(env, message);
}
// 定义模块并导出方法
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("startSendingData", Napi::Function::New(env, StartSendingData));
exports.Set("sayHello", Napi::Function::New(env, SayHello));
return exports;
}
// 绑定模块
NODE_API_MODULE(addon, Init);
helper.h 源码
#ifndef HELPER_H
#define HELPER_H
#include <string>
// 声明一个辅助函数
std::string greet(const std::string& name);
#endif
helper.cpp 源码
#include "helper.h"
std::string greet(const std::string& name) {
return "接收到NodeJs传入: " + name;
}
binding.gyp 基础配置
{
"variables": {
"product_dir": "<(module_root_dir)/build/Release"
},
"targets": [
{
"target_name": "addon",
"sources": [
"src/addon.cpp",
"src/helper.cpp"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"include"
],
"libraries": [
#"<(module_root_dir)/lib/agan_engine.lib"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"defines": ["NAPI_CPP_EXCEPTIONS"],
}
]
}
环境、目录和源码都配置粘贴好之后
- 执行
node-gyp rebuild
- 运行
node ./app.js
注意:当nodejs
调用build/Release/addon.node
文件时传入的字符串在addon.cpp内接收时要转换为UTF8格式
string name = info[0].As<Napi::String>().Utf8Value();
上述Demo只是实现了一个很简单的Nodejs与C++实现数据交互的逻辑,在实际运用中可能会引入头文件(放到 include内)
还有dll、llib文件(放到lib目录内)
打包后如果缺失dll文件就会报错。又或是在C++线程回调中执行Nodejs传入的回调函数并传入C++执行结果,所以还需要在实践中一点一点积累经验。
void onCallStateChange(CALL_STATUS status) override {
int* data = new int(status);
tsfn.BlockingCall(data, [](Napi::Env env, Napi::Function callback, int* data) {
callback.Call({Napi::Number::New(env, *data)});
delete data;
});
}
我们看上述代码 重写了基类的虚函数,并使用 status
参数初始化了一块堆内存,然后将这个指针传给了线程安全函数,为什么不用栈内存? 是因为 onCallStateChange
执行结束后会销毁栈内存 导致空指针,所以使用堆内存是比较简单的做法。
结束!!!
做electron时间长了会发现我们要面对计算机的各种各样的功能和场景,而不仅仅是写前端页面和单纯的去了解electron,有时还要借助AI 使用C++、Python、Go等等实现一些前端触及不到的功能逻辑,使用第三方封装好的C++工具等等,当我们解决了一个又一个自己认为很难的问题就说明我们一直在成长,作为一个计算机软件爱好者,也希望我和大家的路越走越远。