Node.js插件(Addons)是C/C++编写的动态链接对象,这些对象可以被Node.js的require()函数引用,并可以像普通的Node.js模块一样使用。Addons主要用于提供一个Node.js中运行的JavaScript和C/C++库之间的接口。
1. Addons
1.1 概述
Node.js插件(Addons)是C/C++编写的动态链接对象,这些对象可以被Node.js的require()函数引用,并可以像普通的Node.js模块一样使用。Addons主要用于提供一个Node.js中运行的JavaScript和C/C++库之间的接口。
插件(Addons)是动态链接的共享对象,它提供了C/C++类库的调用能力。实现插件的方法比较复杂,涉及到以下元组件及API:
V8:C++库,Node.js用于提供JavaScript执行环境。V8提供了对象创建、函数调用等执行机制,V8相关API包含在了v8.h头文件中(位于Node.js源码树的deps/v8/include/v8.h),也可以查看在线文档。libuv:C库,实现了Node.js中的事件循环、工作线程及在不同平台中异步行为的相关功能。也可以做为是一个跨平台的抽象库,提供了简单的、类POSIX的对主要操作系统的常见系统任务功能,如:与文件系统、套接字、计时器、系统事件的交互等。libuv还提供了一个类pthreads的线程池抽象对象,可用于更复杂的、超越标准事件循环的异步插件的控制功能。内部Node.js库:Node.js自身提供了一定义数量的C/C++API的插件可以使用 - 其中最重要的可能是node::ObjectWrap类Node.js静态链接库:Node.js自身还包含了一部分静态链接库,如OpenSSL。这些位于Node.js源码树的deps/目录下,只有V8和OpenSSL提供了符号出口,可以供Node.js和基它插件所使用。详见Node.js依赖链接
1.2 Node.js依赖链接
Node.js使用了一些静态链接库,如V8、libuv、OpenSSL。所有插件都需要连接到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.gyp是GYP的编译配置文件,其功能与CMake的CMakeLists.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/目录下。
configure和build也可以同一步中完成:
$ 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.js和addon.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
在这个文件中,我们在初始化函数中,通过"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');
