在Electron中使用C++扩展
系列文章
- 什么是Electron
- 如何使用Electron
- Electron的基础知识
- 在Electron中使用Vue
- 关于Electron性能的讨论
- 在Electron中使用C++扩展【当前文章】
六、在Electron中使用C++扩展
在上一章中,我们讨论Electron的性能相关的话题。其中遗留了使用C++来优化性能的一个选项。其实,不仅仅是因为C++可以优化性能,有些系统底层相关的功能也不得不使用C++来完成。例如,如果你的App需要使用某个读卡器或者特殊的打印机之类的。比如调用一些第三方库,如压缩、加密或者音视频编解码之类的。
总结下来,使用C++扩展会有三个原因:
1、使用C++优化性能,处理计算密集型或大量数据的任务。
2、访问系统底层资源,如调用系统API、硬件驱动等等。
3、调用第三方C++库。
当然,使用C++还可以带来一个好处,那就是降低内存的使用。同样的数据量处理和功能逻辑,C++的内存使用也比JavaScript少很多。由于Electron是建立的Node.js和Chromium基础上的。Chromium本身就是C++开发的,Electron内置的功能模块也是用C++开发的,其实,就是Node.js的C++扩展模块。为Electron写C++扩展,跟为Node.js写C++扩展其实是等价的。硬要说不同,那就是Electron中内置的Node.js的版本与宿主开发环境中的Node.js的版本可能会有些差异。
6.1 基本的编译和使用
6.1.1 编译环境
C/C++是编译型语言,所以,使用C/C++之前,首先需要准备好环境。不过,并不复杂。只需要安装一个node-gyp
这个包就可以了。
npm install -g node-gyp
除了node-gyp,你还需要有一整套C/C++编译工具。
如果是Windows系统,需要提前安装一下Visual Studio
和Python
。这两个都可以手动安装,安装方法可以在网上找一下,直接去官网下载安装包安装一下即可。如果想偷懒也可以使用Chocolatey,这是一个Windows下的软件包管理器,如同Linux下的Apt和yum,可以做到一键安装:
choco install python visualstudio2022-workload-vctools -y
另外,需要特别留意的是,Windows下需要用到Powershell
。
在Linux下是最简单的只需要一个命令就可以完成安装包的依赖,如果是Debian系,就是使用
sudo apt install -y build-essential python3 make
如果是MacOS,则需要使用xcode-select --install
命令安装Xcode Command Line Tools
。
6.1.2 binding.gyp
首先,需要创建一个binding.gyp
的文件,内容如下:
{
"targets": [
{
"target_name": "hello",
"sources": [ "hello.cpp" ]
}
]
}
内容比较简单,就是定义一下最基本的信息。其中可以包含以下信息:
target_name:目标的名称,此名称将用作生成的 Visual Studio 解决方案中的项目名称。
type:可选项:static_library 静态库、executable 可执行文件、shared_library 共享库【默认】。
defines:将在编译命令行中传入的 C 预处理器定义(使用 -D 或 /D 选项)。
include_dirs:C++ 头文件所在的目录。
sources:C++ 源文件。
conditions:适配不同环境配置条件块。
copies:拷贝 dll 动态库到生成目录。
library_dirs: 配置 lib 库目录到 vs 项目中。
libraries:项目依赖的库。
msvs_settings:Visual Studio 中属性设置。
相对比较完整的例子如下:
{
"targets": [
{
"target_name": "addon_name",
"type": "static_library",
'defines': ['DEFINE_FOO', 'DEFINE_A_VALUE=value',],
'include_dirs': [
'./src/include',
'<!(node -e "require(\'nan\')")' # include NAN in your project
],
'sources': [
'file1.cc',
'file2.cc',
],
'conditions': [[
'OS=="win"', {
'copies': [{
'destination': '<(PRODUCT_DIR)',
'files': ['./dll/*']
}],
'defines': ['WINDOWS_SPECIFIC_DEFINE',],
'library_dirs': ['./lib/'],
'link_settings': {
'libraries': ['-lyou_sdk.lib']
},
'msvs_settings': {
'VCCLCompilerTool': {
'AdditionalOptions': ['/utf-8']
}
},
}
]],
},
]
}
当然,在咱们这个例子中由于没有任务依赖,所以,不需要用到这么复杂的配置。
6.1.3 C++源文件
hello.cpp
文件的内容为:
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;
void Hello(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world", NewStringType::kNormal).ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Hello);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}
其中,
NODE_MODULE
:这个用来定义一个Node模块的宏。当Node加载该模块的时候,就会按照这个宏的定义调用Initialize
函数。Initialize
函数的功能是导出符号。可以导出函数和常量两种符号。我们这儿仅仅导出了一个名为hello
的函数,然后,其实现函数为Hello
。- 在
Hello
函数中,所有的入参和出参都是通过args
完成的。在这个例子中,我们仅仅返回了一个字符串world
。
6.1.4 调用C++扩展
我们只能在主进程中调用C++扩展,在渲染进程中,如果想使用C++扩展,就必须通过ipc调用完成。主进程运行的脚本index.js
内容如下:
let { app, BrowserWindow, ipcMain } = require('electron');
let hello_file = "./build/Release/hello";
const hello = require(hello_file);
ipcMain.on("hello", (event) => {
console.log("call hello in main process");
event.reply("hello_reply", hello.hello());
});
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.loadFile("./index.html")
})
在渲染进程中通过ipcRenderer来调用主进程,index.html
的内容如下:
<!DOCTYPE html>
<html>
<head>
<title>demo for c++ in electron</title>
</head>
<body>
<button onclick="sendMsg()">发送消息</button>
<br>
<div id="txt"></div>
<script lang="javascript">
let { ipcRenderer } = require("electron");
ipcRenderer.on("hello_reply", (event, msg) => {
document.querySelector("#txt").innerHTML += msg + "<br>";
})
function sendMsg() {
ipcRenderer.send("hello");
}
</script>
</body>
</html>
6.1.5 编译和使用
有了这两个文件就可以进行编译了。运行下面这个命令即可:
node-gyp configure # 生成配置项,仅需要运行一次。
node-gyp build # 编译
如果此次你运行,就无法发现,编译出来的C++扩展无法使用。报错信息大致如下:
App threw an error during load
Error: The module '/home/allan/Desktop/Electronå
¥é¨ç¯/demo5.1/build/Release/hello.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 93. This version of Node.js requires
NODE_MODULE_VERSION 101. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
at process.func [as dlopen] (node:electron/js2c/asar_bundle:5:1812)
at Object.Module._extensions..node (node:internal/modules/cjs/loader:1199:18)
at Object.func [as .node] (node:electron/js2c/asar_bundle:5:1812)
at Module.load (node:internal/modules/cjs/loader:988:32)
at Module._load (node:internal/modules/cjs/loader:829:12)
at Function.c._load (node:electron/js2c/asar_bundle:5:13343)
at Module.require (node:internal/modules/cjs/loader:1012:19)
at require (node:internal/modules/cjs/helpers:102:18)
at Object.<anonymous> (/home/allan/Desktop/Electron入门篇/demo5.1/index.js:4:15)
at Module._compile (node:internal/modules/cjs/loader:1116:14)
A JavaScript error occurred in the main process
这是因为,我们编译的node.js
与Electron
中集成的node.js
版本不一致导致的。node-gyp
编译时使用的是编译环境中的node.js
,而Electron
中集成的node.js
与编译环境中总是会有一些不同。所以,无法在Electron
中正常加载使用。解决方法也容易,那就是使用electron-rebuild
这个包来编译。
electron-rebuild
的安装方法为:
npm add electron-rebuild --save-dev
为了方便使用,我们可以改一下package.json
,内容如下:
{
"name": "demo5.1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .",
"build": "electron-rebuild"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "17.3.1",
"electron-rebuild": "^3.2.9"
}
}
其中,build
命令就是用来替代node-gyp
命令进行编译的。electron-rebuild
会解决版本不一致问题,然后调用node-gyp
完成编译。它解决版本不一致的方法其实是使用Electron
提供的头文件,而不是使用node.js
提供的头文件。而且会有一些版本上的差异,功能上并无区别。
需要留意的是,默认情况下node-gpy
会自动去https://nodejs.org/dist
下载node.js的头文件,而electron-rebuild
会去https://artifacts.electronjs.org/headers/
下载头文件。不过,electronjs
中不允许访问目录,只能通过文件完整路径去下载。
接下来,我们只需要执行下面两个命令就可以了:
npm run build # 编译
npm run start # 运行
不出意外的话,你将看到下面的画面:
关于C++扩展的参考资料:
6.2 Vue中使用C++扩展
如果想在vue中使用C++扩展,事情会变得稍微复杂一些。这是因为vue插件做了很多工作,导致我们无法直接通过require
来加载C++编译出来的node文件了,这其实是封装带来的坏处。不过,并非做不到,只是没有那么的直白了而已。
首先,我们按照第4章中的步骤创建一个vue的工程出来,然后,在工程目录中在创建一个名为:hello_addon
的目录。在该目录中执行以下命令:
npm init --yes
然后,创建两个文件,分别是binding.gyp
、hello.cpp
和index.js
。其中building.gyp
和hello.cpp
的内容跟前面完全相同。代码就不再贴出来了,想看的可以翻看前面的章节或者直接打开demo5.2
的源码看一下。index.js
有了较大不同,内容变为:
module.exports = require('./build/Release/hello_addon');
只有一行,那就是把hello_addon.node
这个扩展导出来,以便使用者调用。到这儿,扩展部分就写好了。接下来,我们回到vue工程的根目录,执行下面这条命令:
vue install ./hello_addon --save
意思就是,把子目录hello_addon
这个包添加到进来vue工程中来。你打开package.json
,就会发现hello_addon
这个依赖包已经被添加进来了。内容大致如下:
... ...
"dependencies": {
"core-js": "^3.8.3",
"hello_addon": "file:hello_addon",
"vue": "^3.2.13"
},
... ...
接下来,我们在background.js
中添加调用hello_addon
的代码,内容如下:
... ...
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
let hello = require("hello_addon")
console.log("call hello_addon in main process: ", hello.hello())
async function createWindow() {
... ...
如果,你这个时候运行npm run electron:serve
这条指令的话,你就会看到下面的错误信息:
即使在这儿我们把hello_addon
的路径改为全路径依旧会报找不到。这是因为我们在运行的时候使用webpack,加载的时候不会再直接加载,而是经过了webpack的处理,导致无法加载成功。为了解决这个问题,我们需要在vue.config.js
中做一点修改,内容如下:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
pluginOptions: {
electronBuilder: {
externals: ["hello_addon"],
},
},
})
其中,第7行是最关键的一行,它的意思是告诉webpack,它是一个外部模块,不要打包它。在官方文档也有相关的说明:
不过,官方的文档并没有说明。这个配置是针对包的,而非文件。所以,很多人会在这个地方出现理解上的偏差,导致卡壳。
改完这个配置文件以后,再次执行npm run electron:serve
就不会再有错误了。
最后。我们通过命令npm run electron:build
打包以后看一下,你会发现打包好的程序也是可以正常使用的。但是,如果你查看一下app.asar
文件就会发现有个不大不小的安全问题:
打包以后的asar文件中居然存在hello.cpp
这个源文件。这是因为webpack并不知道cpp文件不能打包进来。我们可以在vue.config.js
中将这个信息告诉给它。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
pluginOptions: {
electronBuilder: {
externals: ["hello_addon"],
builderOptions: {
files: [
"**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj,cpp,hpp,c,h}",
"!.editorconfig",
"!**/._*",
"!**/node_gyp_bins",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
"!**/{appveyor.yml,.travis.yml,circle.yml}",
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
]
},
},
},
})
其中,第15行就是告诉webpack,这些扩展名的文件不需要打包到asar文件中去的。
6.3 C++扩展中的异步
正常而言,我们调用JavaScript的话,是同一线程的,所以,对于性能有提升,但是有限。因为这样做依旧无法有效利用CPU的多核心。于是,我们需要在C++中使用多线程,使用异步处理的方式达到多核心同时运行的效果。既然使用了异步,执行完成后如何把执行完成的结果返回给JavaScript就是一个问题了。
在JavaScript中,有两种形式可以实现异步执行时返回结果。
-
使用
callback
函数。在调用函数时传入一个函数指针,当处理完成后,由异步处理函数来调用这个
callback
函数返回执行结果。 -
使用
Promise
对象。函数正常返回,但是,返回的不是结果,而是一个
Promise
对象,调用者再通过Promise
对象拿到执行的结果。
本质上二者是相同的,但是,由于使用方式的不同,二者又具有不同的特点。其中,callback
函数本身并不代表异步。只是说,如果callback
函数可以用于异步。换句话说,同步也可以使用callback
。另外,callback
很容易出现回调地狱
,也就是因为回调中嵌套回调导致代码变得复杂不容易理解。而Promise
对象不存在这个问题。因为Promise
对象本身是同步的,只有获取Promise
对象中的结果的时候才是异步的。也就避免了嵌套。所以,推荐使用Promise
对象来实现异步。
6.3.1 使用callback
函数实现异步
基于demo5.2
,我们把hello
扩展中的cpp改写为:
#include <node.h>
#include "callback.h"
namespace demo
{
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;
v8::Global<v8::Function> func_callback;
void worker_thread_func(uv_work_t *req)
{
callback::request_data *request = (callback::request_data *)req->data;
uv_sleep(5000);
request->result = "world";
}
void HelloWithCallback(const FunctionCallbackInfo<Value> &args)
{
v8::Isolate *isolate = args.GetIsolate();
v8::Local<v8::Context> context = v8::Context::New(isolate);
if (!callback::check_request_param(isolate, context, args))
{
args.GetReturnValue().Set(v8::Boolean::New(isolate, false));
return;
}
callback::begin_worker_thread(isolate, context, args, worker_thread_func);
args.GetReturnValue().Set(v8::Boolean::New(isolate, true));
}
void Initialize(Local<Object> exports)
{
NODE_SET_METHOD(exports, "HelloWithCallback", HelloWithCallback);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}
我们导出了一个名为HelloWithCallback
的函数,这个函数里面并没有实际的逻辑,而是开启了一个worker线程。这个线程的逻辑是在callback.h
里面实现的。
我们把实际的逻辑放在了函数worker_thread_func
里面,这个函数是运行在worker线程中,所以,即使运行很长时间也不会有什么影响。
callback.h
的内容如下:
#include <string>
#include <v8.h>
#include <uv.h>
namespace callback
{
struct request_data
{
std::string argv;
std::string result;
v8::Global<v8::Function> func_callback;
request_data(v8::Isolate *isolate, v8::Local<v8::String> request_json, v8::Local<v8::Function> func)
{
argv = *v8::String::Utf8Value(isolate, request_json);
func_callback.Reset(isolate, func);
}
};
void worker_callbakc_func(uv_work_t *req, int status)
{
if (status == UV_ECANCELED)
{
request_data *request = (request_data *)req->data;
delete req;
request->func_callback.Reset();
delete request;
return;
}
v8::Isolate *isolate = v8::Isolate::GetCurrent();
request_data *request = (request_data *)req->data;
delete req;
// 构造JS回调函数的参数
v8::Local<v8::Value> argv[1];
argv[0] = v8::String::NewFromUtf8(isolate, request->result.c_str()) .ToLocalChecked();
v8::TryCatch try_catch(isolate);
v8::Local<v8::Function> cb = v8::Local<v8::Function>::New(isolate, request->func_callback);
cb->Call(isolate->GetCurrentContext(), Null(isolate), 1, argv);
if (try_catch.HasCaught())
{
fprintf(stderr, "callback failure");
}
// 回收资源
request->func_callback.Reset();
delete request;
}
//检查请求参数
bool check_request_param(v8::Isolate *isolate, v8::Local<v8::Context> &context, const v8::FunctionCallbackInfo<v8::Value> &args)
{
// 判断入参是否满足条件
v8::TryCatch try_catch(isolate);
if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsFunction())
return false;
v8::MaybeLocal<v8::Value> jsonData = v8::JSON::Parse(context, args[0]->ToString(context).ToLocalChecked());
if (jsonData.IsEmpty())
return false;
if (try_catch.HasCaught())
return false;
return true;
}
// 启动线程
void begin_worker_thread(v8::Isolate *isolate, v8::Local<v8::Context> &context, const v8::FunctionCallbackInfo<v8::Value> &args, uv_work_cb work_cb)
{
//构造uv_work_t
request_data *req = new request_data(isolate, args[0]->ToString(context).ToLocalChecked(), v8::Local<v8::Function>::Cast(args[1]));
uv_work_t *uv_req = new uv_work_t();
uv_req->data = req;
//启动uv线程
uv_queue_work(uv_default_loop(), uv_req, work_cb, worker_callbakc_func);
}
}
在这个文件中,我们启动的一个uv线程,然后,将参数传入到uv线程中,在uv线程中执行我们函数的功能,然后再把结果通过回调函数返回给JavaScript。
使用方法跟普通的js函数无异:
const hello = require("hello");
ipcMain.on("hello", (event) => {
hello.HelloWithCallback(JSON.stringify({}), (result) => {
event.reply("hello_ack", result);
})
}
6.3.2 使用Promise
对象实现异步
同样基于demo5.2
,我们把hello
扩展中的cpp改写为:
#include <node.h>
#include "resolver.h"
namespace demo
{
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;
void worker_thread_Resolver(uv_work_t *req)
{
callback::request_data *request = (callback::request_data *)req->data;
uv_sleep(5000);
auto tid = syscall(SYS_gettid);
request->result = "world, " + std::to_string(tid);
}
void HelloAsync(const FunctionCallbackInfo<Value> &args)
{
v8::Isolate *isolate = args.GetIsolate();
v8::Local<v8::Context> context = v8::Context::New(isolate);
Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
resolver::begin_worker_thread(args, resolver, worker_thread_Resolver);
args.GetReturnValue().Set(resolver->GetPromise());
}
void Initialize(Local<Object> exports)
{
NODE_SET_METHOD(exports, "HelloAsync", HelloAsync);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}
resolver.h
的内容如下:
#include <node.h>
#include <thread>
namespace resolver
{
struct request_data
{
std::string argv;
std::string result;
v8::Global<v8::Promise::Resolver> resolver;
request_data(v8::Isolate *isolate, v8::Local<v8::String> request_json, v8::Local<v8::Promise::Resolver> _resolver)
{
argv = *v8::String::Utf8Value(isolate, request_json);
resolver.Reset(isolate, _resolver);
}
};
using v8::FunctionCallbackInfo;
void worker_callbakc_func(uv_work_t *req, int status)
{
if (status == UV_ECANCELED)
{
request_data *request = (request_data *)req->data;
delete req;
request->resolver.Reset();
delete request;
return;
}
v8::Isolate *isolate = v8::Isolate::GetCurrent();
request_data *request = (request_data *)req->data;
delete req;
v8::TryCatch try_catch(isolate);
v8::Local<v8::Promise::Resolver> resolver = v8::Local<v8::Promise::Resolver>::New(isolate, request->resolver);
v8::Local<v8::Value> value = v8::String::NewFromUtf8(isolate, request->result.c_str()).ToLocalChecked();
resolver->Resolve(isolate->GetCurrentContext(), value);
if (try_catch.HasCaught())
{
fprintf(stderr, "resolver failure");
}
// 回收资源
request->resolver.Reset();
delete request;
}
void begin_worker_thread(const FunctionCallbackInfo<v8::Value> &args, v8::Local<v8::Promise::Resolver> &resolver, uv_work_cb work_cb)
{
v8::Isolate *isolate = args.GetIsolate();
v8::Local<v8::Context> context = v8::Context::New(isolate);
request_data *req = new request_data(isolate, args[0]->ToString(context).ToLocalChecked(), resolver);
uv_work_t *uv_req = new uv_work_t();
uv_req->data = req;
//启动uv线程
uv_queue_work(uv_default_loop(), uv_req, work_cb, worker_callbakc_func);
}
}
可以看到,跟Callback
最大的不同仅仅是把回调函数换成了Promise
对象而已。另外,需要留意的是,无论是CallBack
还是Promise
对象回调回去以后都是在JavaScript的主线程里面。
6.4 使用nan编写C++扩展
nan
是一个node.js的插件,可以用来简化C++扩展的编写,尤其是异常的处理。使用nan
编写C++插件只需要按照下面几步即可:
第一步、创建一个普通的npm工程
mkdir nan_hello
cd nan_hello
npm init --yes
npm install --save-dev nan electron-rebuild electron@17.3.1
注意,这儿假设你已经搭建好node-gyp
。如果没有搭建node-gyp
编译环境的话,参考6.1.1
。
第二步、创建binding.gyp
,内容如下:
{
"targets": [
{
"target_name": "hello",
"sources": [ "hello.cpp" ],
"include_dirs": [
"<!(node -e \"require('nan')\")"
]
}
]
}
注意这儿的include_dirs
,需要通过这个指令让编译器在编译阶段找到nas的头文件。
第三步、创建hello.cpp
,内容如下:
#include <nan.h>
void Hello(const Nan::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
if (info.Length() != 2 && (!info[0]->IsString() || !info[1]->IsNumber())){
Nan::ThrowTypeError("Invlid arguments");
return;
}
Nan::Utf8String arg0(info[0]->ToString(context).ToLocalChecked());
int arg1 = info[1]->NumberValue(context).FromJust();
auto result = std::string(*arg0) + " world " + std::to_string(arg1);
info.GetReturnValue().Set(Nan::New(result).ToLocalChecked());
}
void Init(v8::Local<v8::Object> exports) {
v8::Local<v8::Context> context = exports->GetCreationContext().ToLocalChecked();
exports->Set(context, Nan::New("hello").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Hello)->GetFunction(context) .ToLocalChecked());
}
NODE_MODULE(hello, Init)
如果你使用的是vscode,编辑器会找不到nan.h
这个头文件,解决方法很简单。在.vscode
目录建一个名为c_cpp_properties.json
的文件,内容为:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${default}",
"${HOME}/.electron-gyp/17.3.1/include/node/",
"node_modules/nan"
]
}
],
"version": 4
}
注意:electron-gyp
路径中存在版本号,找你机器上存在的版本即可。
第四步:创建测试脚本index.js
hello = require('./build/Release/hello');
console.log(hello.hello("hello", 1));
try {
console.log(hello.hello("hello"));
} catch (error) {
console.log('发生了一个错误:', error.message)
}
注意:我们这儿为了演示,并没有使用Electron。
第五步:编译运行
首先,在package.json
中新增编译脚本:
{
"name": "nan_hello",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "electron-rebuild",
"start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^17.3.1",
"electron-rebuild": "^3.2.9",
"nan": "^2.20.0"
}
}
然后,依次运行一下命令即可:
npm run build
npm run start
如果想要导出给其他包使用,还需要写一个export的js——export.js
,内容为:
module.exports = require('./build/Release/hello');
另外,在package.json
中使用exports
字段新增导出脚本信息:
{
"name": "nan_hello",
"version": "1.0.0",
"description": "",
"main": "index.js",
"exports": [
"./export.js"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "electron-rebuild",
"start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^17.3.1",
"electron-rebuild": "^3.2.9",
"nan": "^2.20.0"
}
}
至于如何在其他包中使用,具体可以参考demo6.4
,这儿不再赘述。
当然,nan
并非只能用来写写简单的helloword,它跟原生API一样,也可以回调JS函数等等。其中,这些能力建议阅读nan
的项目页面提供的文档和例子来学习。其项目地址为:https://github.com/nodejs/nan 。不过,nan的封装其实并不是特别彻底,尤其的变量的使用并不是很统一,使用方式也不太符合C++原始习惯,所以,并不推荐使用。
6.5 使用node-addon-api编写C++扩展
和nan
类似,node-addon-api
也是简化C++扩展的编写。不过,它比nan
更加简约一些。
使用node-addon-api
只需要按照以下几步即可:
第一步:创建一个普通的npm工程
mkdir naa_hello
cd naa_hello
npm init --yes
npm install --save-dev node-addon-api electron-rebuild electron@17.3.1
第二步、创建binding.gyp
{
"targets": [
{
"target_name": "naa_hello",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ "hello.cpp" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
}
]
}
第三步、创建hello.cpp
#include <napi.h>
napi_value Hello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 2 || !info[0].IsString() || !info[1].IsNumber()) {
napi_throw_type_error(env, NULL, "Invlid arguments");
return NULL;
}
auto arg0 = info[0].As<Napi::String>();
auto arg1 = info[1].As<Napi::Number>().Int32Value();
auto result = (std::string)arg0 + " world " + std::to_string(arg1);
return Napi::String::New(env, result);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "hello"),
Napi::Function::New(env, Hello));
return exports;
}
NODE_API_MODULE(hello, Init)
我们看到,无论是传入参数,还是返回值,又或者是导出函数,都比原生接口和Nan简单了很多。而且其封装非常统一,且用法也很符合C++原始的习惯。
至于编译使用,跟nan
完全相同,这儿就不再赘述了。如有疑问,可以参考源码demo6.5
。
6.6 C++中调用Js函数
无论是使用原生API,还是使用插件都可以实现在C++中调用Js函数,其中,在6.3 C++扩展中的异步
我们使用的就是原生API调用的js函数,非常的麻烦且复杂。这次我们演示一下,如何使用node-addon-api调用js函数。本小节的代码均在demo6.6
中。
运行效果为:
6.6.1 同步调用
核心代码如下:
#include <napi.h>
napi_value CallJsFunction(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
Napi::Function cb = info[0].As<Napi::Function>();
cb.Call(env.Global(), {Napi::String::New(env, "hello world")});
return nullptr;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "CallJsFunction"),
Napi::Function::New(env, CallJsFunction));
return exports;
}
NODE_API_MODULE(call_js_function, Init)
在js中调用的方法为:
addon = require('./build/Release/call_js_function');
addon.CallJsFunction((msg)=>{
console.log("msg:", msg);
});
很明显,使用node-addon-api
插件,要比原生的简约了非常多,几乎没有冗余的代码了。
6.6.2 使用Callback函数异步调用
如果你需要在C++中异步执行,可以使用Napi::AsyncWorker
这个类。用法也非常简单,核心代码为:
#include <napi.h>
#include <thread>
class MyWorker : public Napi::AsyncWorker {
public:
MyWorker(Napi::Function& callback) : Napi::AsyncWorker(callback) {}
void Execute() {
// 在工作者线程中执行任务
std::this_thread::sleep_for(std::chrono::seconds(5));
}
void OnOK() {
// 执行完成, 回调js函数
Callback().Call({Napi::String::New(Env(), "run finish in c++")});
}
};
napi_value AsyncCallJsFunction(const Napi::CallbackInfo &info) {
Napi::Function cb = info[0].As<Napi::Function>();
MyWorker* myWorker = new MyWorker(cb);
myWorker->Queue();
return nullptr;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "AsyncCallJsFunction"),
Napi::Function::New(env, AsyncCallJsFunction));
return exports;
}
NODE_API_MODULE(call_js_function, Init)
调用方法为:
addon = require('./build/Release/call_js_function');
addon.AsyncCallJsFunction((msg)=>{
console.log("AsyncCallJsFunction msg:", msg);
});
console.log("call AsyncCallJsFunction finish.");
由于我们在C++中sleep了5秒,所以,我们会先看到call AsyncCallJsFunction finish.
,然后再看到AsyncCallJsFunction msg: run finish in c++
。
6.6.3 使用Promise异步调用
如果想使用Promise也很简单,核心代码为:
#include <napi.h>
#include <thread>
#include <sstream>
class MyPromiseWorker : public Napi::AsyncWorker
{
public:
MyPromiseWorker(const Napi::Env &env, int _a, int _b) : Napi::AsyncWorker(env), m_deferred(env), a(_a), b(_b) {}
Napi::Promise GetPromise() { return m_deferred.Promise(); }
void Execute() {
if (b < 0.00001) {
SetError("Cannot divide by zero");
return;
}
// 在工作者线程中执行任务
std::this_thread::sleep_for(std::chrono::seconds(5));
c = a / b;
}
void OnOK() {
// 执行成功, 回调js函数
std::stringstream ss;
ss << "run finish in c++:" << a << " / " << b << " = " << std::to_string(c);
m_deferred.Resolve(Napi::String::New(Env(), ss.str()));
}
void OnError(const Napi::Error &err) {
// 执行失败, 回调js函数
m_deferred.Reject(err.Value());
}
private:
Napi::Promise::Deferred m_deferred;
double a, b;
double c;
};
napi_value CallJsFunctionPromise(const Napi::CallbackInfo &info)
{
MyPromiseWorker *myWorker = new MyPromiseWorker(info.Env(), info[0].ToNumber(), info[1].ToNumber());
myWorker->Queue();
return myWorker->GetPromise();
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set(Napi::String::New(env, "CallJsFunctionPromise"),
Napi::Function::New(env, CallJsFunctionPromise));
return exports;
}
NODE_API_MODULE(call_js_function, Init)
相对于callback的方式,我们需要多定义一个Napi::Promise::Deferred
对象,然后将这个对象返回给JavaScript,最后在执行完成或者出错的时候再通过该对象回调到JavaScript里面去。
在JavaScript中的使用方法为:
addon = require('./build/Release/call_js_function');
addon.CallJsFunctionPromise(1, 2).then((msg)=>{
console.log("CallJsFunctionPromise1 msg:", msg);
});
console.log("call CallJsFunctionPromise1 finish.");
addon.CallJsFunctionPromise(1, 0).then((msg)=>{
console.log("CallJsFunctionPromise2 msg:", msg);
}).catch((err)=>{
console.log("CallJsFunctionPromise2 err:", err);
});
console.log("call CallJsFunctionPromise2 finish.");
6.6.4 在C++中给JavaScript发送消息
有时候C++需要不定期调用JavaScript,比如监听某个事件,如果电源变化事件、或者网络消息等等。这时就需要C++给JavaScript发送消息的能力。这也不难,借助Napi::ThreadSafeFunction
即可实现,核心代码如下:
#include <napi.h>
#include <thread>
#include <sstream>
class MySendWorker : public Napi::AsyncWorker
{
public:
MySendWorker(const Napi::Env &env, Napi::Function& callback) :
Napi::AsyncWorker(env),
callback_(callback),
tsfn_{Napi::ThreadSafeFunction::New(env, callback, "MySendWorker", 0, 1)} {}
virtual ~MySendWorker() override { tsfn_.Release(); }
void Execute() override {
// 在工作者线程中执行任务
for(int i = 0; i < 5; i++){
std::this_thread::sleep_for(std::chrono::seconds(1));
napi_status status = tsfn_.BlockingCall([i](Napi::Env env, Napi::Function js_callback) {
js_callback.Call({
Napi::String::New(env, "msg"),
Napi::String::New(env, "sleep " + std::to_string(i+1))
});
});
if (status != napi_ok) {
SetError("Napi::ThreadSafeNapi::Function.BlockingCall() failed");
return;
}
}
}
void OnError(const Napi::Error &error) {
// 执行失败, 回调js函数
callback_.Call({Napi::String::New(Env(), "error"), error.Value()});
}
private:
Napi::Function callback_;
Napi::ThreadSafeFunction tsfn_;
};
napi_value CallJsFunctionSend(const Napi::CallbackInfo &info)
{
Napi::Function callback = info[0].As<Napi::Function>();
MySendWorker *myWorker = new MySendWorker(info.Env(), callback);
myWorker->Queue();
return nullptr;
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set(Napi::String::New(env, "CallJsFunctionSend"),
Napi::Function::New(env, CallJsFunctionSend));
return exports;
}
NODE_API_MODULE(call_js_function, Init)
JavaScript中的使用方法为:
addon = require('./build/Release/call_js_function');
const { EventEmitter } = require('node:events');
class Receiver extends EventEmitter {
constructor () {
super();
addon.CallJsFunctionSend(this.emit.bind(this));
}
}
const receiver = new Receiver();
receiver.on('msg', (msg) => {
console.log('receive: ', msg);
});
系列文章
- 什么是Electron
- 如何使用Electron
- Electron的基础知识
- 在Electron中使用Vue
- 关于Electron性能的讨论
- 在Electron中使用C++扩展【当前文章】