基于N-API和node-addon-api的Node.js异步C++扩展

作者:API传播员 · 2025-12-17 · 阅读时间:6分钟

从长远来看,在 Node.js 应用程序中引入 C/C++ 代码库的能力可能会成为改变游戏规则的关键因素。在许多场景下,使用 C/C++ 代码可以带来显著优势:

  • 将长时间运行的任务分配到独立线程中。
  • 显著提升 CPU 密集型任务的性能。
  • 利用大量成熟且经过验证的 C/C++ 代码库。

Node.js(以及其底层的 V8 引擎)本身是用 C/C++ 实现的,因此从 JavaScript 调用第三方 C/C++ 代码显得顺理成章。然而,这并非完全自动化的过程,存在许多潜在问题。如果处理不当,可能会大幅降低性能收益。最大的挑战在于 Node.js 和底层 V8 API 在每个新版本中可能发生重大变化,这意味着每次 Node.js 升级都需要重写 C/C++ 插件,这显然效率不高。


Node.js 本机插件抽象层选项

虽然 NAN(Native Abstractions for Node.js)已经存在很长时间,并且经过了充分的实践验证,但它是一个第三方插件,需要随着每个新的 Node.js 版本进行更新。相比之下,N-API 是 Node.js 的一部分,由 Node.js 官方团队维护,并且保证与未来的 Node.js 版本保持 ABI 兼容性。这意味着使用 N-API 构建的 C/C++ 插件在 Node.js 升级时无需重新编译,可以直接使用。

因此,从技术角度来看,N-API 是一个非常有前途的选择。然而,目前关于 N-API 的文档仍然较为匮乏。本文将重点探讨 N-API 的使用。


N-API 实验阶段的澄清

N-API 最初在 Node.js 8 中作为实验性功能引入,并在 Node.js 10 中正式退出实验阶段。然而,在 Node.js 8 的不同版本中,N-API 的状态存在一些混淆。

简而言之,N-API 在 Node.js 8 的所有版本中均为实验性功能,直到 8.12 版本才被认为是稳定的,并且不再显示警告。


N-API 和 node-addon-API

N-API 是一个功能强大的 C API,但对于创建 C++ 插件的开发者来说,直接使用它并不方便。为了解决这一问题,Node.js 官方团队提供了 node-addon-API,这是 N-API 的 C++ 封装。它是一个轻量级的包装层,与 N-API 本身一样由官方维护。

值得注意的是,node-addon-API 通过 N-API 与 Node.js 交互,因此不会影响 N-API 的 ABI 兼容性。


示例任务:异步处理大块数据

我们将通过一个常见场景来演示 N-API 的使用:Node.js 应用程序接收大块数据(如来自 HTTP 请求或 WebSocket 的数据),对其进行操作后返回结果。为了简单起见,数据将从一个文件(约 90MB)中读取并写入,操作是将每个字节的值加倍。我们的原生插件将以异步方式执行数据操作,从而允许 Node.js 同时处理其他请求。


工具选择

在本示例中,我们将使用最新的 N-API 方法来编写本机插件。为了提高开发效率,我们选择使用 C++ 而非 C,因此需要用到 node-addon-API 模块。


实现对比

为了便于性能和复杂性比较,我们将分别用纯 JavaScript 和 N-API/node-addon-API 实现上述任务。

纯 JavaScript 实现

以下是使用纯 JavaScript 实现的代码:

console.time("程序运行时");
const fs = require('fs');
const buf = fs.readFileSync("测试数据");
console.time("数据操作");
for (let i = 0; i < buf.length; i++) {
  buf[i] *= 2;
}
console.timeEnd("数据操作");
fs.writeFileSync("测试数据修改", buf);
console.timeEnd("程序运行时");

这段代码简单明了:读取文件“测试数据”,将每个字节的值加倍,然后写回文件“测试数据修改”。

使用 N-API/node-addon-API 的 C++ 插件

在 JavaScript 中调用插件

以下是使用 N-API C++ 插件实现相同功能的 JavaScript 代码:

console.time("程序运行时");
const fs = require('fs');
const addon = require('./build/Release/addon.node');
const buf = fs.readFileSync("测试数据");
console.time("本机插件在主事件循环线程上花费的时间");
console.time("数据操作");
addon.processData(buf, () => {
  console.timeEnd("数据操作");
  fs.writeFileSync("测试数据修改", buf);
  console.timeEnd("程序运行时");
});
console.timeEnd("本机插件在主事件循环线程上花费的时间");

通过 require('./build/Release/addon.node') 加载 C++ 插件后,我们可以像调用普通 JavaScript 函数一样调用插件导出的函数。

C++ 插件实现

以下是 C++ 插件的核心代码:

#include 
#include "DataProcessingAsyncWorker.h"

using namespace Napi;void ProcessData(const CallbackInfo& info) {
  Buffer data = info[0].As<Buffer>();
  Function cb = info[1].As();
  DataProcessingAsyncWorker* worker = new DataProcessingAsyncWorker(data, cb);
  worker->Queue();
}Object Init(Env env, Object exports) {
  exports.Set(String::New(env, "processData"), Function::New(env, ProcessData));
  return exports;
}NODE_API_MODULE(addon, Init)

在上述代码中,ProcessData 函数通过 DataProcessingAsyncWorker 实现异步数据处理。NODE_API_MODULE 宏用于注册模块,并定义初始化函数。


性能对比

以下是在运行 Ubuntu 18.04 的 i7 CPU 笔记本电脑上测量的性能数据:

异步本机插件

  • 主事件循环线程耗时:0.266ms
  • 数据操作耗时:47.0888ms
  • 程序总运行时间:168.2024ms

纯 JavaScript 实现

  • 数据操作耗时:4123.7252ms
  • 程序总运行时间:4241.3536ms

从结果可以看出,使用本机插件的整体性能比纯 JavaScript 提高了 25 倍以上。同时,本机插件在主事件循环上的阻塞时间不到 0.3 毫秒,而纯 JavaScript 实现的阻塞时间超过 4 秒。


总结

通过本文的示例,我们可以清楚地看到,N-API 和 node-addon-API 提供了一种高效且灵活的方式,将 C/C++ 的性能优势引入到 Node.js 应用中。尽管开发本机插件的复杂性较高,但其性能提升是显而易见的,尤其是在处理 CPU 密集型任务时。

对于需要极致性能优化的场景,N-API 和 node-addon-API 是值得深入研究和使用的工具。

原文链接: https://codemerx.com/blog/asynchronous-c-addon-for-node-js-with-n-api-and-node-addon-api/