我之前写过一篇 写一个C++ addon支持语雀Win暗黑标题栏 ,(前往语雀有更好的阅读体验),有些人对 C++ addons 很感兴趣,但不太了解具体有哪些用处也不知道怎么写,今天就来聊一聊。
先说明一下,我们常聊到的 C++ Addons,其实特指的是用 C++ 写的 Node Addons。
什么是 Node Addons
Node 通过支持 Modules 来鼓励模块化软件开发。模块,本质上是符合特定要求的文件和目录的集合。
使用 Node 的真正好处之一是可以使用 Node 模块的强大生态。npm 提供了最大的 Node 模块集合。
除了用 JavaScript 编写的模块外,Node 还提供了支持用 C 和 C++ 编写的 Node 模块的技术,即 Node Addons。它允许将现有的 C 和 C++ 库编译成 Node 原生模块,这些模块与完全用 JavaScript 编写的模块几乎没有区别。
为什么要用 C++
- Node.js 生态系统完全依赖于 C/C++。
- Node.js 是用 C/C++ 编写的 JavaScript 的运行时环境。基本上 Node.js 是一个嵌入 V8 的 C/C++ 程序,V8 是最初为 Google Chrome 构建的 JavaScript 执行引擎,它还在底层使用 libuv 来处理异步事件。
- C++ 生态丰富。
- C/C++ 拥有大量经过验证且高效的算法或库。可以很方便的集成一个用 C/C++ 编写的第三方库,并直接在 Node.js 中使用它。
- 访问操作系统资源。
- C++ 可以开发需要硬件级别或操作系统级别操作的应用程序,可以访问一些 JS 难以访问的、Node.js 暂时不提供的系统工具库和原生 API。例如那些构建在 Electron 或 NW.js 上的桌面应用,需要调用操作系统的窗口、网络、外接设备、安全、系统设置等系统 API。
- 计算任务。
- 对于低功率设备,或者高计算密集型任务,C++ 比 JavaScript 能更快地运行 CPU 密集型操作。虽然 Node 的 JavaScript 运行时引擎最终会将 JavaScript 编译成二进制,但 Node addons 仅编译一次 C/C++ 代码,并直接为 Node 提供二进制文件。例如音视频的编解码等高耗计算能力的应用,如淘宝直播、猿辅导等直播应用,通过 C++ addon 把音视频引擎的基础库提供给上层的桌面应用。
什么是 C++ addons
字面意思就是 C++ 插件,是用 C++ 编写的动态链接库,即可以像普通的 Node.js 模块一样使用 require()
函数加载插件。Addons 提供了 JavaScript 和 C/C++ 库之间的接口。
实现 addon 有三种选择:N-API、nan 或直接使用内部 V8、libuv 和 Node.js 库。
官方建议:除非需要直接访问 N-API 未公开的功能,否则请使用 N-API。
1. 用 NAN 写 Addons
NAN 介绍
Node.js 的原生抽象 (NAN),即 Native Abstractions for Node.js。NAN 是通过直接调用 Chrome V8 的 API 来支持 C/C++ 代码访问、创建和操作 JavaScript 对象,Node 历来将其用作其 JavaScript 引擎。
这种方法的缺点是,Node 使用的 V8 引擎每次更新时,NAN 层本身以及依赖它的代码都需要更新。
用 NAN 编写 Addons
初始化项目
初始化项目,安装 node-gyp 和 NAN:
npm init
npm i node-gyp --save-dev
npm i --save nan
npm i bindings
package.json 加入
{
"name": "test-nan-addon",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "node-gyp rebuild",
"clean": "node-gyp clean"
},
"dependencies": {
"bindings": "^1.5.0",
"nan": "^2.17.0"
},
"devDependencies": {
"node-gyp": "^9.3.1"
}
}
新增 binding.gyp 文件:
{
"targets": [
{
"target_name": "test-nan-addon",
"sources": [
"./src/main.cpp"
],
"include_dirs": [
"<!(node -e "require('nan')")"
]
}
]
}
target_name
:指定编译后的 node 二进制文件名sources
:C++ 入口文件include_dirs
:写入引用 nan,我们就可以直接在 cpp 文件里#include <nan.h>
引入 NAN
通过 require('nan') 加载 nan.h 的原理
"main": "include_dirs.js"
指定入口文件
require('path').relative('.', __dirname)
导入根目录所有文件
根目录下包含 nan.h 头文件,通过头文件可以直接调用 NAN 模块。
写一个 Hello World
新建 src/main.cpp:
#include <nan.h>
void Hello(const Nan::FunctionCallbackInfo<v8::Value>& info) {
info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
}
void Init(v8::Local<v8::Object> exports) {
v8::Local<v8::Context> context = exports->CreationContext();
exports->Set(context,
Nan::New("hello").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Hello)
->GetFunction(context)
.ToLocalChecked());
}
NODE_MODULE(hello, Init)
#include<nan.h>
引入 NAN 头文件exports->Set(ctx, func_name, func);
指定了对外暴露 APIhello()
ToLocalChecked()
单纯把结果转成v8::Local
NODE_MODULE
注册模块入口函数为Init
然后跑一下编译:
npm run build
在 index.js 引入:
var addon = require('bindings')('test-nan-addon');
console.log(addon.hello());
运行一下就能看到结果:
更多用法参考:github.com/nodejs/node…
2. 用 N-API 写 Addons
N-API 介绍
Node-API
N-API,即 Node-API,Node-API 是 Node 8.0.0 中引入的工具包,充当 C/C++ 代码和 Node JavaScript 引擎之间的中介。它允许 C/C++ 代码访问、创建和操作 JavaScript 对象。Node-API 内置于 Node 8.0.0 及更高版本中,无需进一步安装。
Node-API 从底层 JavaScript 引擎中抽象出它的 API。这提供了两个直接的好处:
- 保证 API 始终向后兼容。 这意味着今天创建的模块将继续在所有未来版本的 Node 上运行。由于 Node-API 是 ABI (Application Binary Interface) 稳定的,因此即使不重新编译,模块也可以继续运行。
- 与底层 JavaScript 引擎的更改隔离。 即使 Node 的底层 JavaScript 引擎发生变化,模块仍可以运行。例如,基于 Node-API 构建的模块无需修改和重新编译,即可在 V8、ChakraCore、SpiderMonkey 等 不同 JavaScript 引擎上运行。
node-addon-api
为了支持使用 C++,Node.js 维护了一个名为 node-addon-api
的 C++包装器模块。
注意,node-addon-api 不是 node.js 的一部分,需要独立安装。
用 N-API 编写 Addons
我之前在写一个C++ addon支持语雀Win暗黑标题栏 里写过,这里就直接复制一遍内容:
初始化项目
在初始化仓库后,需要安装:
npm i node-gyp --save-dev
npm i node-addon-api
npm i bindings
新增 ./binding.gyp 文件:
{
"targets": [
{
"target_name": "electron-windows-titlebar",
"sources": [
"./src/cpp/main.cpp"
],
"include_dirs": [
"<!@(node -p "require('node-addon-api').include")"
],
"dependencies": [
"<!(node -p "require('node-addon-api').gyp")"
],
"cflags!": [
"-fno-exceptions"
],
"cflags_cc!": [
"-fno-exceptions"
],
"msvs_settings": {
"VCCLCompilerTool": {
"ExceptionHandling": 1
}
},
"defines": [
"NAPI_DISABLE_CPP_EXCEPTIONS",
"_HAS_EXCEPTIONS=1"
],
"libraries": [],
}
]
}
- target_name:指定编译后的 node 二进制文件名
- sources:C++ 入口文件
package.json 加入 scripts:
{
"name": "electron-windows-titlebar",
"version": "1.0.0",
"description": "windows-style title bar component for Electron",
"main": "index.js",
"scripts": {
"build": "node-gyp rebuild",
"clean": "node-gyp clean"
},
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^3.0.0"
},
"devDependencies": {
"node-gyp": "^9.3.0",
}
}
写一个 Hello World
新建 src/cpp/main.cpp
#include <napi.h>
std::string hello(){
return "Hello World";
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("hello", Napi::Function::New(env, hello));
return exports;
}
NODE_API_MODULE(titlebar, Init);
#include<napi.h>
引入 N-API 头文件exports.Set("hello", Napi::Function::New(env, hello));
指定了对外暴露 APIhello()
NODE_API_MODULE
注册模块入口函数为Init
然后跑一下编译:
npm run build
./index.js 里引入编译好的 node 文件:
const titlebar = require('./build/Release/electron-windows-titlebar.node');
console.log('C++ addon', titlebar.hello());
module.exports = titlebar;
运行一下,就能看到 ‘C++ addon Hello World’ :
node index.js
这样我们完成了一个简单的C++ addon啦~
更多 N-API 用法和例子可以参考官方文档:github.com/nodejs/node…
3. 直接用内部库写 Addons
当不使用 N-API 时,实现插件就涉及到多个组件和 API 的知识,这里摘抄一些內部库的简介供参考:
- V8:Node.js 用于提供 JavaScript 实现的 C++ 库。V8 提供了创建对象、调用函数等机制。V8 的 API 主要记录在
v8.h
头文件中(deps/v8/include/v8.h
在 Node.js 源代码树中),该文件也可在线获取。 - libuv:实现 Node.js 事件循环、其工作线程和平台的所有异步行为的 C 库。它还作为一个跨平台抽象库,允许跨所有主要操作系统轻松、类似于 POSIX 访问许多常见的系统任务,例如与文件系统、套接字、计时器和系统事件交互。libuv 还提供类似于 pthreads 的线程抽象,可用于为需要超越标准事件循环的更复杂的异步插件提供支持。鼓励 Addon 作者考虑如何通过 libuv 将工作卸载到非阻塞系统操作、工作线程或 libuv 线程的自定义使用,从而避免 I/O 或其他时间密集型任务阻塞事件循环。
- 内部 Node.js 库。Node.js 本身导出插件可以使用的 C++ API,其中最重要的是类
node::ObjectWrap
。 - Node.js 包括其他静态链接库,包括 OpenSSL。这些其他库位于
deps/
Node.js 源代码树的目录中。只有 libuv、OpenSSL、V8 和 zlib 符号被 Node.js 有目的地重新导出,并且可以在不同程度上被插件使用。
因为我没有实践过这种方式,就不展开说怎么编写了。
这里推荐研究一君老师的性能监控库 X-profile,是 Node addon 大型项目,结合了 NAN,同时大量用到 v8.h 和 uv.h 来实现:
扩展阅读 - Addon 的演变历史
从暴力到 NAN 再到 NAPI——Node.js 原生模块开发方式变迁
语雀桌面端如何使用 C++ addon
通过 N-API 写 C++ addon 来使用操作系统 API。
- 使用
wlanapi
、netlistmgr
这两个动态链接库,检查网络状况:
- 使用
dwmapi
这个动态链接库,修改 Windows 标题栏主题颜色以支持暗黑模式:
- 此外,语雀桌面端也使用到了前面提到的 X-profile。
其他 Node Addons 技术
用 Rust 写 Addons:
参考
- An Introduction to C++ addons in Node.js
- Beginners guide to writing NodeJS Addons using C++ and N-API (node-addon-api)
- UNB - David Bremner - C++ addons
- github.com/nodejs/node…
- Welcome to the Node-API Resource
- NAPI 开发 C++ Addon
- Node.js C++ Addon应用实践
- NodeJS Advanced — How to create a native add-on using C++
本文搬运自我发布在语雀上的文章 《一探 C++ addons》,推荐在语雀有更好的阅读体验。欢迎关注我的语雀个人主页。同时欢迎关注我的掘金专栏,这个专栏将会持续分享学习各种前端/桌面端相关技术(Electron、node.js、V8、Chromium等)的新特性