C++ 插件#

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

实现插件有三种选择:N-API、nan、或直接使用内部的 V8、libuv 和 Node.js 库。 除非需要直接访问 N-API 未公开的函数,否则请使用 N-API。 有关 N-API 的更多信息,参见具有 N-API 的 C/C++ 插件

当不使用 N-API 时,实现插件很复杂,涉及多个组件和 API 的知识:

  • V8:Node.js 用于提供 JavaScript 实现的 C++ 库。 V8 提供了用于创建对象、调用函数等的机制。 V8 的 API 文档主要在 v8.h 头文件中(Node.js 源代码树中的 deps/v8/include/v8.h),也可以在查看在线文档

  • libuv:实现了 Node.js 的事件循环、工作线程、以及平台所有的的异步行为的 C 库。 它也是一个跨平台的抽象库,使所有主流操作系统中可以像 POSIX 一样访问常用的系统任务,比如与文件系统、socket、定时器、以及系统事件的交互。 libuv 还提供了一个类似 POSIX 多线程的线程抽象,可被用于强化更复杂的需要超越标准事件循环的异步插件。 建议插件开发者多思考如何通过在 libuv 的非阻塞系统操作、工作线程、或自定义的 libuv 线程中降低工作负载来避免在 I/O 或其他时间密集型任务中阻塞事件循环。

  • 内置的 Node.js 库。Node.js 自身公开了插件可以使用的 C++ API,其中最重要的是 node::ObjectWrap 类。

  • Node.js 包含了其他的静态链接库,如 OpenSSL。 这些库位于 Node.js 源代码树中的 deps/ 目录。 只有 libuv、OpenSSL、V8 和 zlib 符号是被 Node.js 有目的地重新公开,并且可以被插件在不同程度上使用。 更多信息可查看链接到 Node.js 自带的库

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

Hello world#

“Hello World” 示例是一个简单的插件,用 C++ 编写,相当于以下 JavaScript 代码:

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

首先,创建 hello.cc 文件:

// hello.cc
#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
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").ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // 命名空间示例

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

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.js 插件可能需要在多个上下文中多次加载的环境。 比如,Electron 的运行时会在单个进程中运行多个 Node.js 实例。 每个实例都有它自己的 require() 缓存,因此当通过 require() 进行加载时,每个实例需要一个原生的插件才能正常运行。 从插件程序的角度来看,这意味着它必须支持多次初始化。

可以使用 NODE_MODULE_INITIALIZER 宏来构建上下文感知的插件,它扩展为 Node.js 函数的名字使得 Node.js 能在加载时找到。 可以按如下例子初始化一个插件:

using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* Perform addon initialization steps here. */
}

另一个选择是使用 NODE_MODULE_INIT() 宏,这也可以用来构建上下文感知的插件。 但和 NODE_MODULE() 不一样的是,它可以基于一个给定的函数来构建一个插件, NODE_MODULE_INIT() 充当了这种跟有函数体的给定函数的声明。

可以在调用 NODE_MODULE_INIT() 之后在函数体内部使用以下三个变量:

  • Local<Object> exports
  • Local<Value> module
  • Local<Context> context

这种构建上下文感知的插件的方式顺带了管理全局静态数据的职责。 由于插件可能被多次加载,可能甚至来自不同的线程,插件中任何的全局静态数据必须加以保护,并且不能包含任何对 JavaScript 对象持久性的引用。 因为 JavaScript 对象只在一个上下文中有效,从错误的上下文或者另外的线程中访问有可能会导致崩溃。

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

  • 定义一个类,该类会保存每个插件实例的数据,并且具有以下形式的静态成员:
    static void DeleteInstance(void* data) {
      // 将 `data` 转换为该类的实例并删除它。
    }
  • 在插件的初始化过程中堆分配此类的实例。 这可以使用 new 关键字来完成。
  • 调用 node::AddEnvironmentCleanupHook(),将上面创建的实例和指向 DeleteInstance() 的指针传给它。 这可以确保实例会在销毁环境之后被删除。
  • 将类的实例保存在 v8::External中。
  • 通过将 v8::External 传给 v8::FunctionTemplate::New()v8::Function::New()(用以创建原生支持的 JavaScript 函数)来将其传给暴露给 JavaScript 的所有方法。 v8::FunctionTemplate::New()v8::Function::New() 的第三个参数接受 v8::External,并使用 v8::FunctionCallbackInfo::Data() 方法使其在原生回调中可用。

这确保了每个扩展实例数据到达每个能被 JavaScript 访问的绑定。每个扩展实例数据也必须通过其创建的任何异步回调函数。

下面的例子说明了一个上下文感知的插件的实现:

#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);
}

// context-aware 初始化
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // 为此插件实例创建一个新的 `AddonData` 实例,并将其生命周期与 Node.js 环境的生命周期联系起来。
  AddonData* data = new AddonData(isolate, exports);
  // 在 `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();
}

工作线程的支持#

为了从多个 Node.js 环境加载,例如主线程和工作线程,插件还需要:

  • 是一个 N-API 插件,或
  • 如上所述,使用 NODE_MODULE_INIT() 声明为上下文感知。

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

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

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

If necessary, there is an additional pair of AddEnvironmentCleanupHook() and RemoveEnvironmentCleanupHook() overloads, where the cleanup hook takes a callback function. This can be used for shutting down asynchronous resources, such as any libuv handles registered by the addon.

以下的 addon.cc 使用 AddEnvironmentCleanupHook

// addon.cc
#include <assert.h>
#include <stdlib.h>
#include <node.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 中进行测试:

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

构建#

当源代码已被编写,它必须被编译成二进制 addon.node 文件。 要做到这点,需在项目的顶层创建一个名为 binding.gyp 的文件,它使用一个类似 JSON 的格式来描述模块的构建配置。 该文件会被 node-gyp(一个用于编译 Node.js 插件的工具)使用。

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

Node.js 会捆绑与发布一个版本的 node-gyp 工具作为 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 版本来执行同样的一套动作,为用户要求的平台生成一个插件编译后的版本。

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

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

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

因为编译后的二进制插件的确切路径取决于它如何被编译(比如有时它可能在 ./build/Debug/ 中),所以插件可以使用 bindings 包加载编译后的模块。

虽然 bindings 包在如何定位插件模块的实现上更复杂,但它本质上使用了一个 try…catch 模式,类似如下:

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 公开的符号可用。

  • 可以使用 --nodedir 标志指向本地的 Node.js 源代码镜像来运行 node-gyp。 如果使用此选项,则插件将有权访问全部依赖项。

使用 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 可以确保 V8 API 的稳定性。

Node.js 的原生抽象(或称为 nan)提供了一组工具,建议插件开发者使用这些工具来保持插件在过往与将来的 V8 和 Node.js 的版本之间的兼容性。 查看 nan 示例了解它是如何被使用的。

N-API#

稳定性: 2 - 稳定

N-API 是用于构建本机插件的API。 它独立于底层 JavaScript 运行时(例如 V8),并作为 Node.js 本身的一部分进行维护。 此 API 将是跨 Node.js 版本的应用程序二进制接口(Application Binary Interface,ABI)稳定版。 它旨在将插件与底层 JavaScript 引擎中的更改隔离开来,并允许为一个版本编译的模块在更高版本的 Node.js 上运行而无需重新编译。 插件使用本文档中概述的相同方法/工具(node-gyp 等)构建/打包。 唯一的区别是原始代码使用的 API 集。 不使用 V8 或 Node.js 的原生抽象,而是使用 N-API 中可用的功能。

创建和维护受益于 N-API 提供的 ABI 稳定性的插件会带来某些实现的注意事项

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

// hello.cc using N-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

可用的函数和使用文档请参见具有 N-API 的 C/C++ 插件

插件示例#

以下是一些插件示例,用于帮助开发者入门。 这些例子使用了 V8 的 API。 查看在线的 V8 文档有助于了解各种 V8 调用,V8 的嵌入器指南解释了句柄、作用域和函数模板等的一些概念。

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

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

如果有一个以上的 .cc 文件,则只需添加额外的文件名到 sources 数组。 例如:

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

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

$ node-gyp configure build

函数的参数#

插件通常会开放一些对象和函数,供运行在 Node.js 中的 JavaScript 访问。 当从 JavaScript 调用函数时,输入参数和返回值必须与 C/C++ 代码相互映射。

以下例子描述了如何读取从 JavaScript 传入的函数参数与如何返回结果:

// 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,
                            "参数的数量错误").ToLocalChecked()));
    return;
  }

  // 检查参数的类型。
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "参数错误").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)

}  // 命名空间示例

但已被编译,示例插件就可以在 Node.js 中引入和使用:

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

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

回调#

把 JavaScript 函数传入到一个 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:

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

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

在这个例子中,回调函数是被同步地调用。

对象工厂#

插件可从 C++ 函数中创建并返回新的对象,如以下例子所示。 一个带有 msg 属性的对象被创建并返回,该属性会输出传入 createObject() 的字符串:

// 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 中测试它:

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

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

函数工厂#

另一种常见情况是创建 JavaScript 函数来包装 C++ 函数,并返回到 JavaScript:

// 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

测试它:

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

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

包装 C++ 对象#

也可以包装 C++ 对象/类使其可以使用 JavaScript 的 new 操作来创建新的实例:

// 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

// 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() 添加到构造函数的原型来开放它:

// 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);  // MyObject::New() 的一个字段。
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

  // 准备构造函数模版
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // 原型
  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()) {
    // 像构造函数一样调用:`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 {
    // 像普通方法 `MyObject(...)` 一样调用,转为构造调用。
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons =
        args.Data().As<Object>()->GetInternalField(0).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.Holder());
  obj->value_ += 1;

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

}  // namespace demo

要构建这个例子, myobject.cc 文件必须被添加到 binding.gyp

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

测试:

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

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

当对象被垃圾收集时,包装器对象的析构函数将会运行。 对于析构函数测试,有一些命令行标志可用于强制垃圾收集。 这些标志由底层 V8 JavaScript 引擎提供。 它们随时可能更改或删除。 它们没有由 Node.js 或 V8 记录,并且它们永远不应该在测试之外使用。

包装对象的工厂#

也可以使用一个工厂模式,避免显式地使用 JavaScript 的 new 操作来创建对象实例:

const obj = addon.createObject();
// 而不是:
// const obj = new addon.Object();

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

// 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

// 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 中的实现类似与之前的例子:

// 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;

// 注意!这不是线程安全的,此插件不能用于工作线程。
Global<Function> MyObject::constructor;

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

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // 准备构造函数模版
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // 原型
  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()) {
    // 像构造函数一样调用:`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 {
    // 像普通方法 `MyObject(...)` 一样调用,转为构造调用。
    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.Holder());
  obj->value_ += 1;

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

}  // namespace demo

要构建这个例子, myobject.cc 文件必须被添加到 binding.gyp

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

测试:

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

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

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

传递包装的对象#

除了包装和返回 C++ 对象,也可以通过使用 Node.js 的辅助函数 node::ObjectWrap::Unwrap 进行去包装来传递包装的对象。 以下例子展示了一个 add() 函数,它可以把两个 MyObject 对象作为输入参数:

// 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 中,新增了一个新的公共方法用于在去包装对象后访问私有值。

// 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 中的实现类似之前的例子:

// 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;

// 注意!这不是线程安全的,此插件不能用于工作线程。
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()) {
    // 像构造函数一样调用:`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 {
    // 像普通方法 `MyObject(...)` 一样调用,转为构造调用。
    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

测试:

// 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);
// 打印: 30