Node.js C/C++插件(Addons)

 2017年03月08日    1411     声明


  1. Addons
  2. Hello World
  3. 3. Addon使用示例

1. Addons

1.1 概述

Node.js插件(Addons)是C/C++编写的动态链接对象,这些对象可以被Node.js的require()函数引用,并可以像普通的Node.js模块一样使用。Addons主要用于提供一个Node.js中运行的JavaScript和C/C++库之间的接口。

插件(Addons)是动态链接的共享对象,它提供了C/C++类库的调用能力。实现插件的方法比较复杂,涉及到以下元组件及API:

  • V8C++库,Node.js用于提供JavaScript执行环境。V8提供了对象创建、函数调用等执行机制,V8相关API包含在了v8.h头文件中(位于Node.js源码树的deps/v8/include/v8.h),也可以查看在线文档
  • libuvC库,实现了Node.js中的事件循环、工作线程及在不同平台中异步行为的相关功能。也可以做为是一个跨平台的抽象库,提供了简单的、类POSIX的对主要操作系统的常见系统任务功能,如:与文件系统、套接字、计时器、系统事件的交互等。libuv还提供了一个类pthreads的线程池抽象对象,可用于更复杂的、超越标准事件循环的异步插件的控制功能。
  • 内部Node.js库:Node.js自身提供了一定义数量的C/C++API的插件可以使用 - 其中最重要的可能是node::ObjectWrap
  • Node.js静态链接库:Node.js自身还包含了一部分静态链接库,如OpenSSL。这些位于Node.js源码树的deps/目录下,只有V8OpenSSL提供了符号出口,可以供Node.js和基它插件所使用。详见Node.js依赖链接


1.2 Node.js依赖链接

Node.js使用了一些静态链接库,如V8libuvOpenSSL。所有插件都需要连接到V8,还可以连接到其它依赖项。通常情况下,可以简单的通过添加#include <...>语法(如#include <v8.h>),而node-gyp会自动找到头文件位置。但是,也有几个问题需要注意:

  • node-gyp运行时,首先会检查Node.js的版本信息,并下载当前版本之前的完整源码的tar包或头文件。如果完整源码被下载,插件就可访问Node.js的全部依赖。如果只下载头文件,那么只有符号出口可以使用。
  • node-gyp可以通过--nodedir标识指定Node.js的本地源码镜像。使用这个选项时,插件可以使用全套依赖关系。


1.3 Node.js原生抽象(NAN

本文中的多数示例直接使用Node.js和V8API实现插件,这对了解V8引擎的API很有帮助。但是随着Node.js及V8新版本的发布,我们的插件可能也需要重新编译,这时可以通过NAN来确保V8引擎API的稳定性。

Native Abstractions for Node.js(即:nan)提供了一套工具集,可以确保插件在过去与未来版本的V8和Node.js之间的一致性。NAN会在C++代码和Node.js及V8 API之间的提供一个抽象层,让我们的插件可以跨多个Node版本,而不用担心Node.js或V8 API的改变。

在本文,我们也提供了一个NAN版本的"Hello World":基于nan实现的Hello World


2. Hello World

接下来,使用C++实现一个简单的"Hello world"插件。其功能类似于以下JavaScript功能:

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


2.1 初始化

首先,需要使用npm初始化目录。创建一个目录,并在目录内执行npm init,命令执行完成后会生成package.json文件:

$ mkdir hello-world
$ cd hello-world
$ npm init


2.2 创建C++文件

创建hello.cc(或hello.cpp)文件,并添如下代码:

// 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"));
}

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

NODE_MODULE(addon, init)

}  // namespace demo

注意:所有Node.js插件必须通过NODE_MODULE导出一个初始化函数:

void Initialize(Local<Object> exports);
NODE_MODULE(module_name, Initialize)

NODE_MODULE行为并没有分号,因为它并不是一个函数(见node.h)。而module_name必须匹配最终二进制文件名(不包括.node后缀)。

hello.cc示例中,初始化函数是init,并定义插件名为addon


2.3 创建GYP配置文件

hello.cc最终会被编译为二进制文件addon.node。要编译成功,需要在项顶层目录添加一个binding.gyp文件,该文件是一个类似JSON格式的构建配置文件,并会被Node.js插件编译工具node-gyp所使用。

创建binding.gyp文件,内容如下:

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

配置文件中的"target_name"要与hello.cc中通过NODE_MODULE导出的模块名一致。在这个文件中会告诉GYP以下信息:

  • 编译后的二进制文件名为"addon",编译完成后我们会在./build/Release/./build/Debug/目录下得到一个"hello.node"文件。
  • 需要编译的源文件,在本例中只有当前目录下的"hello.cc"文件

所有Node.js插件都要使用GYP工具(如node-gyp)进行编译。当制npm包时,还需要在package.json文件中添加一个"gypfile": true入口点,以使npm知道这是一个需要编译的二进制插件,并需要引用node-gyp。引入node-gyp后,就会在package.json文件的同级目录中查找binding.gyp文件。

binding.gyp文件中

注:GYP(Generate Your Projects)是由Chromium开发的一款自动化构建工具,其功能与CMake类似。而binding.gypGYP的编译配置文件,其功能与CMakeCMakeLists.txt文件类似。

注意:一个版本的node-gyp会做为npm的一部分,随Node.js的安装而安装。但开发者却不能直接使用这个版本,仅能用于npm install命令编译及插件安装。如果想要直接使用node-gyp,就需要通过npm install -g node-gyp命令来安装。


2.4 构建编译

binding.gyp文件创建后,就可以使用node-gyp configure命令构建项目。构建完成后,会在build/目录下生成在Makefile(类Unix系统)或vcxproj(Windows系统)文件。

$ node-gyp configure

然后,可以通过node-gyp build命令进行编译:

$ node-gyp build

编译后完成后,就会得到一个二进制的Node.js插件。在本例中,所生成的二进制文件位于./build/Release/addon.node

编译生成的二进制文件可能位于build/Release/build/Debug/目录下,如果编译时使用了--debug参数,二进制文件会生成长build/Debug/目录下。

configurebuild也可以同一步中完成:

$ node-gyp configure build


2.5 require()加载插件

node-gyp构建成功后,可以通过Node.js的require()函数来引用刚构建的二进制插件addon.node

如,创建一个hello.js,文件内容如下:

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

console.log(addon.hello());
// 输出:'world'


Node.js中已编译的二进制插件的文件扩展名是.node(反对.dll.so)。require()函数会寻找.node扩展名文件,并将其初始化为动态链接库。

当使用require()函数调用模块时,.node通常可以省略,Node.js仍会查找和初始化插件。但时,当加载目录内有相同基础文件名时,会发出一个警告。如:在一个目录下同时有addon.jsaddon.node文件时,require('addon')会优先尝试加载addon.js文件。


3. Addon使用示例

3.1 基于nan实现的Hello World

在前面的Hello World示例中,我们基于Node.js和V8 API实现了插件。这样会有一个问题,当Node.js或V8引擎更新后,插件还需要重新编译。这时,我们可以基于nan编写插件。

1. 初始化

创建一个工作目录,并使用npm init进行初始化:

$ mkdir nan-hello-world
$ cd nan-hello-world
$ npm init

2. 安装nan

我们基于nan构建Node.js插件,因此需要安装这个插件:

$ npm install nan --save

3. 编写binding.gyp

添加GYP编译配置文件binding.gyp,文件内容如下:

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.cc" ],
      "include_dirs": [
        "<!(node -e \"require('nan')\")"
      ]
    }
  ]
}

这个配置文件,不同在Hello World中使用的配置文件。在这个文件中,额外添加了一个"include_dirs"配置项,编译时该目录所包含的NAN会被使用,而NAN的路径会通过node -e "require('nan')"获取。

4. 编写hello.cc

添加hello.cc文件,文件内容如下:

#include <nan.h>

void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
  info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
}

void Init(v8::Local<v8::Object> exports) {
  exports->Set(Nan::New("hello").ToLocalChecked(),
               Nan::New<v8::FunctionTemplate>(Method)->GetFunction());
}

NODE_MODULE(hello, Init)

在这个文件中,主要有三个主要组件。

以下代码定义了Node.js插件的入口点:

NODE_MODULE(hello, Init)

其中,第一个参数必须与binding.gyp中的"target"配置项相匹配,而第二个参数是入口点所要调用的函数。如:

void Init(v8::Local<v8::Object> exports) {
  exports->Set(Nan::New("hello").ToLocalChecked(),
               Nan::New<v8::FunctionTemplate>(Method)->GetFunction());
}

以上是我们程序入口点的代码,在这里我们可以接收两个参数。第一个是exports,这个参数与.js文件中的module.exports相同;而第二个参数是module(本例不需要,已删除),这个参数与.js文件中的module相同。

在这个示例中,我们想要向module.exports添加一个"hello"属性,这时就可以通过设置一个V8的String属性到V8的Function。在本例中,我们用定义了一个字符串,通过FunctionTemplate来返回一个可以被V8调用的C++函数。

在本例中,Method函数是:

void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
  info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
}

这是NAN对我们的有用之处,它改变了V8 API难以在不同版本下运行相同C++代码的情况。NAN提供了一个简单的映射,这样我们可以定义了与V8兼容的FunctionTemplate将被接受。

在当前V8版本中,void Method(const v8::FunctionCallbackInfo<v8::Value>& args),它是一个可以被V8调用的标准函数签名。args包含了调用信息,如JavaScript函数参数、及允许我们设置的返回值。

5. 构建及编译

编写完C++代码后,就可以使用node-gyp configure构建项目,再使用node-gyp build编译。这两条命令也可以一起使用:

$ node-gyp configure build

6. 使用插件

编译完成后,就可以在JavaScript文件中使用插件了。编辑如下内容的hello.js文件,并执行就可以看到执行效果:

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

console.log(addon.hello());

// 输出:world


3.2 函数参数

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

下面是一个从JavaScript中接受参数,并返回结果的示例:

1. 初始化

创建一个工作目录,并在目录内执行npm init初始化项目:

$ mkdir function-arguments
$ cd function-arguments
$ npm init

2. 创建C++文件

创建C++文件addon.cc ,内容如下:

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

  // Check the number of arguments passed.
  if (args.Length() < 2) {
    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Wrong number of arguments")));
    return;
  }

  // Check the argument types
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Wrong arguments")));
    return;
  }

  // Perform the operation
  double value = args[0]->NumberValue() + args[1]->NumberValue();
  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(addon, Init)

}  // namespace demo

在这个文件中,我们在初始化函数中,通过NODE_SET_METHOD方法向外暴露了一个"add"方法。这个方法对应用这个C++文件中的Add方法:

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

Add方法中,通过结构体FunctionCallbackInfo传参数,及设置返回值:

void Add(const FunctionCallbackInfo<Value>& args);

3. 配置、构建、编译

完成addon.cc文件后,添加编译配置文件binding.gpy,内容如下:

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

构建项目,并编译:

$ node-gyp configure build

4. 引用插件

构建完成后,编写add.js文件,实现插件在Node.js中的调用:

// add.js

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

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

// 输出:"This should be eight: 8"


3.3 回调

回调是JavaScript编程中常用的一种编程方式,接下来将演示怎么样将一个函数传递给C++代码,及怎样调用这个回调函数。

从本例开始,不演示详细项目构建过程,只提供C++代码及最终的JavaScript调用代码,请诸君按前面示例介绍的步骤自行构建。

编写C++代码,内容如下:

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

namespace demo {

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<Function> cb = Local<Function>::Cast(args[0]);
  const unsigned argc = 1;
  Local<Value> argv[argc] = { String::NewFromUtf8(isolate, "hello world") };
  cb->Call(Null(isolate), argc, argv);
}

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

NODE_MODULE(addon, Init)

}  // namespace demo

在这个示例中,在初始化函数Init()中,我们在第二个参数中传递了整个module对象。这样将允许插件使用一个函数完全重写exports,而不是向其添加一个函数属性。

添加编译配置文件binding.gyp,并构建、编译完后,可以使用以下JavaScript代码调用插件测试运行效果:

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

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


3.4 对象工厂(Object Factory)

插件可以在C++函数内部创建并返回一个对象。在下面示例中,每向createObject()传入一个字符串,都会通过msg属性返回一个对象。

添加addon.cc文件,内容如下:

// addon.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 CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // 创建JavaScript对象
  Local<Object> obj = Object::New(isolate);
  obj->Set(String::NewFromUtf8(isolate, "msg"), args[0]->ToString());

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

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

NODE_MODULE(addon, Init)

}  // namespace demo


添加编译配置文件binding.gyp,并构建、编译完后,可以使用以下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'


3.5 对象工厂(Function Factory)

另一种常见场景是,在C++内部创建一个JavaScript函数,并返回被JavaScript调用。

添加addon.cc文件,内容如下:

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

namespace demo {

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

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

  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
  Local<Function> fn = tpl->GetFunction();

  // omit this to make it anonymous
  fn->SetName(String::NewFromUtf8(isolate, "theFunction"));

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

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

NODE_MODULE(addon, Init)

}  // namespace demo

构建、编译完成后,在JavaScript中可以像调用回调函数一样,调用在C++插件中返回的函数:

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

const fn = addon();
console.log(fn());
// 输出: 'hello world'


3.6 C++对象包装(Wrapping C++ objects)

还有一种情况是,包装由JavaScript的new操作符创建的C++对象/类。

添加如下addon.cc文件:

// 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(addon, InitAll)

}  // namespace democ

在这个文件中,我们引用了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::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);
  static v8::Persistent<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

在这个文件中,包含一个MyObject类的定义,该类继承自node::ObjectWrap

以下是MyObject类定义文件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::Persistent;
using v8::String;
using v8::Value;

Persistent<Function> MyObject::constructor;

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

MyObject::~MyObject() {
}

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

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

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

  constructor.Reset(isolate, tpl->GetFunction());
  exports->Set(String::NewFromUtf8(isolate, "MyObject"),
               tpl->GetFunction());
}

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

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
    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<Context> context = isolate->GetCurrentContext();
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    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"
      ]
    }
  ]
}

构建、编译完成后,可以通过以下JavaScript代码进行测试:

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


3.7 对象包装工厂(Factory of wrapped objects)

或者,可以使用工厂模式,以避免直接在JavaScript中使用new操作符创建对象实例。如:

const obj = addon.createObject();
// instead of:
// 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(addon, 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::Persistent<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

myobject.cc的实现与上例类似:

// myobject.cc
#include <node.h>
#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::Persistent;
using v8::String;
using v8::Value;

Persistent<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"));
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

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

  constructor.Reset(isolate, tpl->GetFunction());
}

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

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
    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<Context> context = isolate->GetCurrentContext();
    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"
      ]
    }
  ]
}

编译、构建完成后,可以通过以下JavaScript代码测试:

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


3.8 传递包装的对象(Passing wrapped objects around)

除可以包装和返回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::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();

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

  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(addon, 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::Persistent<v8::Function< constructor;
  double value_;
};

}  // namespace demo

#endif

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

// myobject.cc
#include <node.h>
#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::Object;
using v8::Persistent;
using v8::String;
using v8::Value;

Persistent<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"));
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  constructor.Reset(isolate, tpl->GetFunction());
}

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

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
    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<Context> context = isolate->GetCurrentContext();
    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

构建完成后,可以通过以下JavaScript代码测试:

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


3.8 AtExit钩子(AtExit Hooks)

"AtExit"是一个函数,它会在Node.js事件循环结束后但JavaScript VM和Node.js关闭前被调用。"AtExit"通过node::AtExitAPI注册。该函数包含两个参数,其中回调函数的调用顺序是后入先出:

void AtExit(callback, args)
  • callback: void (*)(void*)一个函数指针,在退出时调用
  • args: void*传递给退出时调用的回调函数的指定

以下addon.cc是一个对"AtExit"的实现:

// addon.cc
#undef NDEBUG
#include <assert.h>
#include <stdlib.h>
#include <node.h>

namespace demo {

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

static char cookie[] = "yum yum";
static int at_exit_cb1_called = 0;
static int at_exit_cb2_called = 0;

static void at_exit_cb1(void* arg) {
  Isolate* isolate = static_cast<Isolate*>(arg);
  HandleScope scope(isolate);
  Local<Object> obj = Object::New(isolate);
  assert(!obj.IsEmpty()); // assert VM is still alive
  assert(obj->IsObject());
  at_exit_cb1_called++;
}

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

static void sanity_check(void*) {
  assert(at_exit_cb1_called == 1);
  assert(at_exit_cb2_called == 2);
}

void init(Local<Object> exports) {
  AtExit(sanity_check);
  AtExit(at_exit_cb2, cookie);
  AtExit(at_exit_cb2, cookie);
  AtExit(at_exit_cb1, exports->GetIsolate());
}

NODE_MODULE(addon, init);

}  // namespace demo

构建并编译后,可以使用以下JavaScript代码测试:

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