一探 C++ addons

425 阅读8分钟

我之前写过一篇 写一个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 的原理

  1. github.com/nodejs/nan/…

"main": "include_dirs.js" 指定入口文件

  1. github.com/nodejs/nan/…

require('path').relative('.', __dirname) 导入根目录所有文件

  1. github.com/nodejs/nan

根目录下包含 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); 指定了对外暴露 API hello()
  • 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)); 指定了对外暴露 API hello()
  • 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 来实现:

github.com/X-Profiler/…

扩展阅读 - Addon 的演变历史

从暴力到 NAN 再到 NAPI——Node.js 原生模块开发方式变迁

语雀桌面端如何使用 C++ addon

通过 N-API 写 C++ addon 来使用操作系统 API。

  • 使用 wlanapinetlistmgr 这两个动态链接库,检查网络状况:

github.com/electron-mo…

  • 使用 dwmapi 这个动态链接库,修改 Windows 标题栏主题颜色以支持暗黑模式:

github.com/electron-mo…

  • 此外,语雀桌面端也使用到了前面提到的 X-profile。

其他 Node Addons 技术

用 Rust 写 Addons:

github.com/napi-rs/nap…

参考

本文搬运自我发布在语雀上的文章 《一探 C++ addons》,推荐在语雀有更好的阅读体验。欢迎关注我的语雀个人主页。同时欢迎关注我的掘金专栏,这个专栏将会持续分享学习各种前端/桌面端相关技术(Electron、node.js、V8、Chromium等)的新特性