Skip to content

C++ 插件

插件 是用 C++ 编写的动态链接共享对象。require() 函数可以像加载普通的 Node.js 模块一样加载插件。插件提供了 JavaScript 和 C/C++ 库之间的接口。

实现插件有三种方法:

除非需要直接访问 Node-API 未公开的功能,否则请使用 Node-API。有关 Node-API 的更多信息,请参阅 使用 Node-API 的 C/C++ 插件

如果不使用 Node-API,实现插件会变得更加复杂,需要了解多个组件和 API:

  • V8: Node.js 使用的 C++ 库,用于提供 JavaScript 实现。它提供了创建对象、调用函数等的机制。V8 的 API 主要在 v8.h 头文件中记录(在 Node.js 源代码树中为 deps/v8/include/v8.h),并且也可以 在线 获取。
  • libuv: 实现 Node.js 事件循环、其工作线程和平台所有异步行为的 C 库。它也用作跨平台抽象库,为所有主要操作系统提供简单、类似 POSIX 的访问权限,用于许多常见的系统任务,例如与文件系统、套接字、计时器和系统事件交互。libuv 还提供类似于 POSIX 线程的线程抽象,用于需要超越标准事件循环的更复杂的异步插件。插件作者应避免使用 I/O 或其他耗时任务阻塞事件循环,方法是通过 libuv 将工作卸载到非阻塞系统操作、工作线程或自定义的 libuv 线程。
  • 内部 Node.js 库:Node.js 本身导出 C++ API,插件可以使用这些 API,其中最重要的 API 是 node::ObjectWrap 类。
  • 其他静态链接库(包括 OpenSSL):这些其他库位于 Node.js 源代码树的 deps/ 目录中。Node.js 会有意地重新导出 libuv、OpenSSL、V8 和 zlib 符号,插件可以在不同程度上使用这些符号。有关其他信息,请参阅 链接到 Node.js 包含的库

以下所有示例均可 下载,并可用作插件的起点。

Hello world

这个 "Hello world" 示例是一个简单的插件,用 C++ 编写,等同于以下 JavaScript 代码:

js
module.exports.hello = () => 'world'

首先,创建文件 hello.cc

C++
// hello.cc
#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 Method(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", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo

所有 Node.js 插件都必须导出一个初始化函数,其模式如下:

C++
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

NODE_MODULE 后面没有分号,因为它不是一个函数(参见 node.h)。

module_name 必须与最终二进制文件的名称匹配(不包括 .node 后缀)。

因此,在 hello.cc 示例中,初始化函数是 Initialize,插件模块名称是 addon

使用 node-gyp 构建插件时,使用宏 NODE_GYP_MODULE_NAME 作为 NODE_MODULE() 的第一个参数将确保将最终二进制文件的名称传递给 NODE_MODULE()

使用 NODE_MODULE() 定义的插件不能同时在多个上下文或多个线程中加载。

上下文感知的插件

在某些环境中,Node.js 插件可能需要在多个上下文中多次加载。例如,Electron 运行时在一个进程中运行多个 Node.js 实例。每个实例都有自己的 require() 缓存,因此每个实例都需要一个原生插件才能在通过 require() 加载时正确运行。这意味着插件必须支持多次初始化。

可以通过使用宏 NODE_MODULE_INITIALIZER 来构建上下文感知的插件,该宏展开为 Node.js 加载插件时期望找到的函数的名称。因此,插件可以像以下示例中那样进行初始化:

C++
using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* 在此处执行插件初始化步骤。 */
}

另一个选项是使用宏 NODE_MODULE_INIT(),它也将构建一个上下文感知的插件。与用于围绕给定插件初始化函数构建插件的 NODE_MODULE() 不同,NODE_MODULE_INIT() 用作此类初始化程序的声明,后跟函数体。

以下三个变量可以在 NODE_MODULE_INIT() 调用后的函数体中使用:

  • Local<Object> exports
  • Local<Value> module,以及
  • Local<Context> context

构建上下文感知的插件需要仔细管理全局静态数据以确保稳定性和正确性。由于插件可能被多次加载,甚至可能来自不同的线程,因此存储在插件中的任何全局静态数据都必须得到适当的保护,并且不能包含对 JavaScript 对象的任何持久引用。原因是 JavaScript 对象仅在一个上下文中有效,并且在从错误的上下文或与创建它们的线程不同的线程访问时可能会导致崩溃。

上下文感知的插件可以通过执行以下步骤来避免全局静态数据:

  • 定义一个类,该类将保存每个插件实例的数据,并具有 static 成员
  • 在插件初始化程序中使用 new 关键字堆分配此类的实例。
  • 调用 node::AddEnvironmentCleanupHook(),将上述创建的实例和指向 DeleteInstance() 的指针传递给它。这将确保在环境被拆除时删除该实例。
  • 将类的实例存储在 v8::External 中,并且
  • 通过将其传递给创建原生支持的 JavaScript 函数的 v8::FunctionTemplate::New()v8::Function::New(),将 v8::External 传递给所有公开给 JavaScript 的方法。v8::FunctionTemplate::New()v8::Function::New() 的第三个参数接受 v8::External 并使用 v8::FunctionCallbackInfo::Data() 方法在原生回调中使其可用。

这将确保每个插件实例数据到达可以从 JavaScript 调用的每个绑定。每个插件实例数据还必须传递到插件可能创建的任何异步回调中。

以下示例说明了上下文感知插件的实现:

C++
#include <node.h>

using namespace v8;

class AddonData {
 public:
  explicit AddonData(Isolate* isolate):
      call_count(0) {
    // 确保此每个插件实例数据在环境清理时被删除。
    node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
  }

  // 每个插件数据。
  int call_count;

  static void DeleteInstance(void* data) {
    delete static_cast<AddonData*>(data);
  }
};

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
  // 检索每个插件实例数据。
  AddonData* data =
      reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
  data->call_count++;
  info.GetReturnValue().Set((double)data->call_count);
}

// 初始化此插件以使其成为上下文感知的。
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // 为此插件实例创建一个新的 `AddonData` 实例,并将其生命周期与 Node.js 环境的生命周期绑定。
  AddonData* data = new AddonData(isolate);

  // 将数据包装在 `v8::External` 中,以便我们可以将其传递给我们公开的方法。
  Local<External> external = External::New(isolate, data);

  // 将方法 `Method` 公开给 JavaScript,并确保它接收我们上面创建的每个插件实例数据,方法是将 `external` 作为第三个参数传递给 `FunctionTemplate` 构造函数。
  exports->Set(context,
               String::NewFromUtf8(isolate, "method").ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
}

Worker 支持

[历史]

版本变更
v14.8.0, v12.19.0清理钩子现在可以是异步的。

为了能够从多个 Node.js 环境(例如主线程和 Worker 线程)加载,附加组件需要:

  • 是一个 Node-API 附加组件,或者
  • 如上所述,使用 NODE_MODULE_INIT() 声明为上下文感知的

为了支持 Worker 线程,附加组件需要清理它们在这样的线程退出时可能分配的任何资源。这可以通过使用 AddEnvironmentCleanupHook() 函数来实现:

C++
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
                               void (*fun)(void* arg),
                               void* arg);

此函数添加一个钩子,该钩子将在给定的 Node.js 实例关闭之前运行。如有必要,可以使用 RemoveEnvironmentCleanupHook() 在这些钩子运行之前将其移除,该函数具有相同的签名。回调按照后进先出的顺序运行。

如有必要,还有一对 AddEnvironmentCleanupHook()RemoveEnvironmentCleanupHook() 重载,其中清理钩子采用回调函数。这可用于关闭异步资源,例如附加组件注册的任何 libuv 句柄。

以下 addon.cc 使用 AddEnvironmentCleanupHook

C++
// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>

using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;

// 注意:在实际应用中,不要依赖静态/全局数据。
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;

static void cleanup_cb1(void* arg) {
  Isolate* isolate = static_cast<Isolate*>(arg);
  HandleScope scope(isolate);
  Local<Object> obj = Object::New(isolate);
  assert(!obj.IsEmpty());  // 断言 VM 仍然存活
  assert(obj->IsObject());
  cleanup_cb1_called++;
}

static void cleanup_cb2(void* arg) {
  assert(arg == static_cast<void*>(cookie));
  cleanup_cb2_called++;
}

static void sanity_check(void*) {
  assert(cleanup_cb1_called == 1);
  assert(cleanup_cb2_called == 1);
}

// 将此附加组件初始化为上下文感知的。
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
  AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
  AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
}

通过运行以下 JavaScript 代码进行测试:

js
// test.js
require('./build/Release/addon')

构建

一旦源代码编写完成,就必须将其编译成二进制文件 addon.node。为此,请在项目的顶层创建一个名为 binding.gyp 的文件,使用类似 JSON 的格式描述模块的构建配置。此文件由 node-gyp 使用,这是一个专门用于编译 Node.js 插件的工具。

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["hello.cc"]
    }
  ]
}

node-gyp 实用程序的一个版本与 Node.js 一起捆绑分发,作为 npm 的一部分。此版本并非直接提供给开发人员使用,仅用于支持使用 npm install 命令编译和安装插件。希望直接使用 node-gyp 的开发人员可以使用命令 npm install -g node-gyp 来安装它。有关更多信息,包括特定于平台的要求,请参阅 node-gyp安装说明

创建 binding.gyp 文件后,使用 node-gyp configure 为当前平台生成相应的项目构建文件。这将在 build/ 目录中生成 Makefile(在 Unix 平台上)或 vcxproj 文件(在 Windows 上)。

接下来,调用 node-gyp build 命令生成编译后的 addon.node 文件。它将被放入 build/Release/ 目录中。

当使用 npm install 安装 Node.js 插件时,npm 使用其自身的捆绑版 node-gyp 来执行相同的操作集,根据用户的平台按需生成插件的编译版本。

构建完成后,可以通过将 require() 指向构建的 addon.node 模块,在 Node.js 中使用二进制插件:

js
// hello.js
const addon = require('./build/Release/addon')

console.log(addon.hello())
// 打印:'world'

由于编译后的插件二进制文件的准确路径可能因其编译方式而异(例如,有时它可能在 ./build/Debug/ 中),插件可以使用 bindings 包来加载编译后的模块。

虽然 bindings 包的实现更复杂,因为它查找插件模块的方式,但它本质上使用了类似于以下的 try…catch 模式:

js
try {
  return require('./build/Release/addon.node')
} catch (err) {
  return require('./build/Debug/addon.node')
}

链接 Node.js 内置库

Node.js 使用静态链接库,例如 V8、libuv 和 OpenSSL。所有插件都需要链接到 V8,并且也可能链接到任何其他依赖项。通常,这只需包含适当的 #include \<...\> 语句(例如 #include \<v8.h\>),node-gyp 将自动找到合适的头文件。但是,需要注意一些警告:

  • node-gyp 运行时,它会检测 Node.js 的特定发行版本,并下载完整的源代码包或仅下载头文件。如果下载了完整的源代码,则插件将可以完全访问完整的 Node.js 依赖项集。但是,如果仅下载了 Node.js 头文件,则只有 Node.js 导出的符号可用。
  • node-gyp 可以使用 --nodedir 标志运行,该标志指向本地 Node.js 源代码镜像。使用此选项,插件将可以访问完整的依赖项集。

使用 require() 加载插件

编译后的插件二进制文件的扩展名为 .node(而不是 .dll.so)。require() 函数被编写为查找具有 .node 文件扩展名的文件并将它们初始化为动态链接库。

调用 require() 时,通常可以省略 .node 扩展名,Node.js 仍然会找到并初始化插件。但是,需要注意的是,Node.js 将首先尝试查找并加载碰巧具有相同基名的模块或 JavaScript 文件。例如,如果在二进制文件 addon.node 的同一目录中存在文件 addon.js,则 require('addon') 将优先加载 addon.js 文件。

Node.js 原生抽象

本文档中演示的每个示例都直接使用 Node.js 和 V8 API 来实现插件。V8 API 在不同的 V8 版本(以及不同的主要 Node.js 版本)之间发生了巨大的变化。每次更改后,可能需要更新和重新编译插件才能继续运行。Node.js 的发布计划旨在最大限度地减少此类更改的频率和影响,但 Node.js 几乎无法确保 V8 API 的稳定性。

Node.js 原生抽象(或 nan)提供了一套工具,建议插件开发者使用这些工具来保持 V8 和 Node.js 的过去和未来版本之间的兼容性。请参阅 nan示例,了解如何使用它。

Node-API

[稳定: 2 - 稳定]

稳定: 2 稳定性: 2 - 稳定

Node-API 是一个用于构建原生插件的 API。它独立于底层的 JavaScript 运行时(例如 V8),并作为 Node.js 本身的一部分进行维护。此 API 将在 Node.js 的不同版本之间保持应用程序二进制接口 (ABI) 稳定。它的目的是使插件免受底层 JavaScript 引擎更改的影响,并允许为一个版本编译的模块在更高版本的 Node.js 上运行而无需重新编译。插件使用本文档中概述的相同方法/工具构建/打包(node-gyp 等)。唯一的区别是原生代码使用的 API 集。它不使用 V8 或Node.js 原生抽象 API,而是使用 Node-API 中提供的函数。

创建和维护一个利用 Node-API 提供的 ABI 稳定性的插件会带来某些实现方面的考虑

要在上面的“Hello world”示例中使用 Node-API,请将 hello.cc 的内容替换为以下内容。所有其他说明保持不变。

C++
// hello.cc 使用 Node-API
#include <node_api.h>

namespace demo {

napi_value Method(napi_env env, napi_callback_info args) {
  napi_value greeting;
  napi_status status;

  status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
  if (status != napi_ok) return nullptr;
  return greeting;
}

napi_value init(napi_env env, napi_value exports) {
  napi_status status;
  napi_value fn;

  status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
  if (status != napi_ok) return nullptr;

  status = napi_set_named_property(env, exports, "hello", fn);
  if (status != napi_ok) return nullptr;
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo

可用的函数以及使用方法在使用 Node-API 的 C/C++ 插件中进行了介绍。

插件示例

以下是一些示例插件,旨在帮助开发者入门。这些示例使用 V8 API。有关各种 V8 调用的帮助,请参考在线的 V8 参考文档,有关使用的多个概念(如句柄、作用域、函数模板等)的解释,请参考 V8 的 嵌入程序指南

每个示例都使用以下 binding.gyp 文件:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc"]
    }
  ]
}

如果有多个 .cc 文件,只需将附加的文件名添加到 sources 数组中:

json
"sources": ["addon.cc", "myexample.cc"]

binding.gyp 文件准备就绪后,可以使用 node-gyp 配置和构建示例插件:

bash
node-gyp configure build

函数参数

插件通常会公开可在 Node.js 中运行的 JavaScript 访问的对象和函数。当从 JavaScript 调用函数时,必须将输入参数和返回值映射到 C/C++ 代码,并从 C/C++ 代码映射回来。

以下示例说明如何读取从 JavaScript 传递的函数参数以及如何返回结果:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// 这是 "add" 方法的实现
// 输入参数使用 const FunctionCallbackInfo<Value>& args 结构体传递
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // 检查传递的参数数量。
  if (args.Length() < 2) {
    // 抛出一个错误,该错误将传递回 JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong number of arguments").ToLocalChecked()));
    return;
  }

  // 检查参数类型
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong arguments").ToLocalChecked()));
    return;
  }

  // 执行操作
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // 设置返回值(使用传入的 FunctionCallbackInfo<Value>&)
  args.GetReturnValue().Set(num);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

编译完成后,可以在 Node.js 中加载并使用示例插件:

js
// test.js
const addon = require('./build/Release/addon')

console.log('This should be eight:', addon.add(3, 5))

回调函数

在插件中,将 JavaScript 函数传递给 C++ 函数并在其中执行它们是一种常见做法。以下示例说明如何调用此类回调函数:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;

void RunCallback(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<Function> cb = Local<Function>::Cast(args[0]);
  const unsigned argc = 1;
  Local<Value> argv[argc] = {
      String::NewFromUtf8(isolate,
                          "hello world").ToLocalChecked() };
  cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", RunCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

此示例使用了 Init() 的双参数形式,它接收完整的 module 对象作为第二个参数。这允许插件用单个函数完全覆盖 exports,而不是将函数作为 exports 的属性添加。

要测试它,请运行以下 JavaScript 代码:

js
// test.js
const addon = require('./build/Release/addon')

addon(msg => {
  console.log(msg)
  // 打印:'hello world'
})

在此示例中,回调函数是同步调用的。

对象工厂

插件可以在 C++ 函数中创建并返回新对象,如下例所示。创建一个对象并返回,该对象具有一个属性 msg,它会回显传递给 createObject() 的字符串:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<Object> obj = Object::New(isolate);
  obj->Set(context,
           String::NewFromUtf8(isolate,
                               "msg").ToLocalChecked(),
                               args[0]->ToString(context).ToLocalChecked())
           .FromJust();

  args.GetReturnValue().Set(obj);
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", CreateObject);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

在 JavaScript 中测试:

js
// test.js
const addon = require('./build/Release/addon')

const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// 打印:'hello world'

函数工厂

另一种常见场景是创建包装 C++ 函数的 JavaScript 函数并将这些函数返回给 JavaScript:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void MyFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "hello world").ToLocalChecked());
}

void CreateFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  Local<Context> context = isolate->GetCurrentContext();
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
  Local<Function> fn = tpl->GetFunction(context).ToLocalChecked();

  // 省略此行使其成为匿名函数
  fn->SetName(String::NewFromUtf8(
      isolate, "theFunction").ToLocalChecked());

  args.GetReturnValue().Set(fn);
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", CreateFunction);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

测试方法:

js
// test.js
const addon = require('./build/Release/addon')

const fn = addon()
console.log(fn())
// 打印:'hello world'

包装 C++ 对象

也可以通过某种方式包装 C++ 对象/类,从而允许使用 JavaScript 的 new 运算符创建新实例:

C++
// addon.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::Local;
using v8::Object;

void InitAll(Local<Object> exports) {
  MyObject::Init(exports);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo

然后,在 myobject.h 中,包装器类继承自 node::ObjectWrap

C++
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Local<v8::Object> exports);

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);

  double value_;
};

}  // namespace demo

#endif

myobject.cc 中,实现要公开的各种方法。在下面的代码中,方法 plusOne() 通过将其添加到构造函数的原型来公开:

C++
// myobject.cc
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Value;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Local<Object> exports) {
  Isolate* isolate = exports->GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
  addon_data_tpl->SetInternalFieldCount(1);  // 1 field for the MyObject::New()
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

  Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
  addon_data->SetInternalField(0, constructor);
  exports->Set(context, String::NewFromUtf8(
      isolate, "MyObject").ToLocalChecked(),
      constructor).FromJust();
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons =
        args.Data().As<Object>()->GetInternalField(0)
            .As<Value>().As<Function>();
    Local<Object> result =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(result);
  }
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
  obj->value_ += 1;

  args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

}  // namespace demo

要构建此示例,必须将 myobject.cc 文件添加到 binding.gyp 中:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc"]
    }
  ]
}

使用以下代码进行测试:

js
// test.js
const addon = require('./build/Release/addon')

const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// Prints: 11
console.log(obj.plusOne())
// Prints: 12
console.log(obj.plusOne())
// Prints: 13

包装对象的析构函数将在垃圾回收时运行。对于析构函数测试,可以使用命令行标志来强制进行垃圾回收。这些标志由底层的 V8 JavaScript 引擎提供。它们随时可能更改或删除。Node.js 或 V8 并未对此进行记录,并且不应在测试之外使用它们。

在进程或工作线程关闭期间,JS 引擎不会调用析构函数。因此,用户有责任跟踪这些对象并确保正确销毁,以避免资源泄漏。

包装对象的工厂

或者,可以使用工厂模式来避免使用 JavaScript new 运算符显式创建对象实例:

js
const obj = addon.createObject()
// instead of:
// const obj = new addon.Object();

首先,createObject() 方法在 addon.cc 中实现:

C++
// addon.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  MyObject::NewInstance(args);
}

void InitAll(Local<Object> exports, Local<Object> module) {
  MyObject::Init(exports->GetIsolate());

  NODE_SET_METHOD(module, "exports", CreateObject);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo

myobject.h 中,添加了静态方法 NewInstance() 来处理对象的实例化。此方法取代了在 JavaScript 中使用 new

C++
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Isolate* isolate);
  static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
  static v8::Global<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

myobject.cc 中的实现类似于之前的示例:

C++
// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
  obj->value_ += 1;

  args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

}  // namespace demo

再次,要构建此示例,必须将 myobject.cc 文件添加到 binding.gyp 中:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc"]
    }
  ]
}

使用以下代码进行测试:

js
// test.js
const createObject = require('./build/Release/addon')

const obj = createObject(10)
console.log(obj.plusOne())
// Prints: 11
console.log(obj.plusOne())
// Prints: 12
console.log(obj.plusOne())
// Prints: 13

const obj2 = createObject(20)
console.log(obj2.plusOne())
// Prints: 21
console.log(obj2.plusOne())
// Prints: 22
console.log(obj2.plusOne())
// Prints: 23

传递包装后的对象

除了包装和返回 C++ 对象外,还可以通过使用 Node.js 辅助函数 node::ObjectWrap::Unwrap 解包来传递包装后的对象。以下示例显示了一个可以接受两个 MyObject 对象作为输入参数的函数 add()

C++
// addon.cc
#include <node.h>
#include <node_object_wrap.h>
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  MyObject::NewInstance(args);
}

void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
      args[0]->ToObject(context).ToLocalChecked());
  MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
      args[1]->ToObject(context).ToLocalChecked());

  double sum = obj1->value() + obj2->value();
  args.GetReturnValue().Set(Number::New(isolate, sum));
}

void InitAll(Local<Object> exports) {
  MyObject::Init(exports->GetIsolate());

  NODE_SET_METHOD(exports, "createObject", CreateObject);
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo

myobject.h 中,添加了一个新的公共方法,允许在解包对象后访问私有值。

C++
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Isolate* isolate);
  static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
  inline double value() const { return value_; }

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static v8::Global<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

myobject.cc 的实现与之前的版本类似:

C++
// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

}  // namespace demo

使用以下代码进行测试:

js
// test.js
const addon = require('./build/Release/addon')

const obj1 = addon.createObject(10)
const obj2 = addon.createObject(20)
const result = addon.add(obj1, obj2)

console.log(result)
// Prints: 30