插件

Node.js 的插件是基于 C/C++ 编写的动态链接共享对象(dynamically-linked shared objects),可以像普通的 Node.js 模块一样通过 require() 函数加载到 Node.js 的开发环境中。Node.js 插件主要用于支持 Node.js 中 JavaScript 调用 C/C++ 库的接口。

目前,创建 Node.js 插件的方法比较复杂,需要了解以下几个组件和 API:

  • V8:基于 C++ 函数库,当前 Node.js 的 JavaScript 引擎。V8 实现了 JavaScript 创建对象、调用函数等诸多底层机制。V8 的 API 主要集中在 v8.h 头文件(详见 Node.js 项目中的 deps/v8/include/v8.h)中,更多信息请参考线上地址 https://v8docs.nodesource.com/
  • libuv:基于 C 的函数库,实现了 Node.js 的事件循环机制、worker 线程机制以及 Node.js 上所有与异步相关的机制。同时,它是一个跨平台的抽象库,提供了简洁、类 POSIX 的接口,方便开发者调用主流操作系统的常规系统任务,比如与文件系统、网络套接字、定时器以及系统事件的交互。此外,libuv 实现了一个类 pthreads 的线程机制,便于开发者在标准事件循环机制之上构建更强大的异步插件。libuv 鼓励开发者思考如何避免 I/O、任务加载失败等因素对系统操作、worker 线程以及自定义 libuv 线程的阻塞行为。
  • Node.js 内建库:Node.js 本身内建了一系列 C/C++ API 供插件使用,其中最重要的就是 node::ObjectWrap 类。
  • Node.js 内置了一系列静态链接库,比如 OpenSSL,其中大部分位于项目的 deps/ 目录内,只有 V8 和 OpenSSL 刻意被 Node.js 进行了重定向输出,便于诸多插件的调用,更多信息请参考Linking to Node.js’s own dependencies

以下所有示例都可以在 https://github.com/nodejs/node-addon-examples 下载到。如果你想要开发 Node.js 的插件,也可以使用这些示例作为初始模板。

Hello World

该示例是使用 C++ 开发插件的简单示例,等同于下面这段 JavaScript 代码:

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

首先,创建 hello.cc 文件:

  1. // hello.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::FunctionCallbackInfo;
  5. using v8::Isolate;
  6. using v8::Local;
  7. using v8::Object;
  8. using v8::String;
  9. using v8::Value;
  10. void Method(const FunctionCallbackInfo<Value>& args) {
  11. Isolate* isolate = args.GetIsolate();
  12. args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
  13. }
  14. void init(Local<Object> exports) {
  15. NODE_SET_METHOD(exports, "hello", Method);
  16. }
  17. NODE_MODULE(addon, init)
  18. } // namespace demo

值得注意的是,开发 Node.js 插件的开发时,必须输出一个初始化函数,模式如下:

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

NODE_MODULE 的行末没有分号,因为它并不是一次函数调用(详见 node.h)。module_name 必须二进制文件名相匹配(不包含文件名的 .node 后缀)。通过上述代码,我们在 hello.cc 文件中声明了初始化函数是 init,插件名称是 addon

构建

在上面的源代码编写完成后,需要将其编译为二进制文件 addon.node。首先,在项目的根目录创建一个 binding.gyp 文件,使用类 JSON 格式配置构建信息。该文件创建完成后会被 node-gyp 用来编译模块,node-gyp 是一个专门用于编译 Node.js 插件的工具。

  1. {
  2. "targets": [
  3. {
  4. "target_name": "addon",
  5. "sources": [ "hello.cc" ]
  6. }
  7. ]
  8. }

注意:Node.js 中捆绑了一个特殊版本的 node-gyp 工具集,它是 npm 的一部分,用于支持 npm install 命令编译和安装插件,但是不能直接被开发者使用。如果开发者想直接使用 node-gyp,需要使用 npm install -g node-gyp 命令安装 node-gyp,更多信息请参考 node-gyp 的安装指南,其中包括了对特定平台的需求信息。

创建完 bindings.gyp 文件之后,即可使用 node-gyp configure 命令生成适用于当前平台的构建文件,同时会在 build 目录下生成一个适用于 UNIX 平台的 Makefile 或者适用于 Windows 平台的 vcxproj 文件。

接下来,调用 node-gyp build 命令在 build/Release/ 目录下编译生成 addon.node 文件。

当使用 npm install 命令安装 Node.js 的插件时,npm 会使用自身捆绑的 node-gyp 再次编译和生成适用于用户平台的相关文件。

构建完成后,就可以在 Node.js 的开发中使用 require() 加载 addon.node 模块了:

  1. // hello.js
  2. const addon = require('./build/Release/addon');
  3. console.log(addon.hello()); // 'world'

更多信息请参考下面的几个示例,或者查看用于生产环境的示例 https://github.com/arturadib/node-qt

因为插件编译后二进制文件路径(有时候可能是 ./build/Debug/)由编译方式确定,所以建议使用 bindings 包加载编译后的模块。

值得注意的是,bindings 包定位插件模块的方式非常类似于 try-catch

  1. try {
  2. return require('./build/Release/addon.node');
  3. } catch (err) {
  4. return require('./build/Debug/addon.node');
  5. }

链接到 Node.js 自身的依赖

Node.js 自身用到了许多静态链接库,比如 V8、libuv 以及 OpenSSL。所有的插件都可以链接到这些静态链接库,链接方式非常简单,只需要声明 #include <...>(比如:#include <v8.h>)语句,node-gyp 就会自动定位到合适的头文件。不过,开发时需要注意以下几点:

  • 运行 node-gyp 时,它会检测 Node.js 的特定版本并下载源码或者头文件信息。如果下载的是源码,那么插件就可以完整的使用 Node.js 的依赖;如果下载的只是头文件信息,那么只能使用 Node.js 输出的特定依赖。
  • 使用 node-gyp 命令时,可以添加 --nodedir 标志,用于指定本地的 Node.js 源数据。开启这个可选参数之后,插件可以使用 Node.js 的全部依赖。

使用 require() 加载插件

插件编译后的二进制文件使用 .node 作为后缀名,通过 require() 函数可以查找以 .node 为扩展名的文件并将它们初始化为动态链接库。

调用 require() 时,.node 扩展名虽然可以被 Node.js 查找到并初始化为动态链接库,但是有一个问题值得注意,那就是如果插件名字和其他文件重名了, Node.js 会优先加载 Node.js 模块和 JavaScript 文件。 举例来说,如果在同一目录下有 addon.jsaddon.node 两个文件,那么 require('addon') 函数会优先加载 addon.js 文件。

Node.js 的本地抽象

本文档中的每个示例都直接使用了 Node.js 和 V8 的 API 开发插件,所以有一点非常值得注意,那就是 V8 API 随着版本升级仍在快速迭代之中。对于 V8 和 Node.js 的版本升级,插件需要重新修改和编译以适应新的 API。当前 Node.js 在发布计划中尽量减少 API 的变动对插件所带来的影响,但是这无法确保 V8 API 的稳定性。

Native Abstrations for Node.js(简称 NAN)提供了一系列工具帮助插件开发者在新旧版本的 V8 和 Node.js 之间保持插件的一致性。更多有多 NAN 的使用信息请参考 NAN 开发实例

插件实例

以下内容是用于帮助开发者快读上手的插件实例,它们都在开发过程中调用了 V8 API。通过在线的 V8 指南 可以了解更多有关 V8 调用的信息,也可以参考 Embedder 的使用指南 了解 V8 的核心概念,比如句柄、作用域、函数模板等等。

每一个示例都使用了如下的 binding.gyp 文件:

  1. {
  2. "targets": [
  3. {
  4. "target_name": "addon",
  5. "sources": [ "addon.cc" ]
  6. }
  7. ]
  8. }

如果插件涉及多个 .cc 文件,可以使用将其依次添加入 source 字段所引用的数组中:

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

binding.gyp 创建完成后,最后使用 node-gyp 生成配置文件和编译成二进制文件:

  1. $ node-gyp configure build

函数参数

插件通常都会暴漏某些对象和函数给 Node.js 中的 JavaScript 调用。当 JavaScript 调用函数时,也必须将将传入的参数映射给 C/C++ 代码,在函数调用完成后,还要映射 C/C++ 传回的返回值。

下面代码演示了如何读取 JavaScript 传递来的函数以及如何传输返回值:

  1. // addon.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::Exception;
  5. using v8::FunctionCallbackInfo;
  6. using v8::Isolate;
  7. using v8::Local;
  8. using v8::Number;
  9. using v8::Object;
  10. using v8::String;
  11. using v8::Value;
  12. // This is the implementation of the "add" method
  13. // Input arguments are passed using the
  14. // const FunctionCallbackInfo<Value>& args struct
  15. void Add(const FunctionCallbackInfo<Value>& args) {
  16. Isolate* isolate = args.GetIsolate();
  17. // Check the number of arguments passed.
  18. if (args.Length() < 2) {
  19. // Throw an Error that is passed back to JavaScript
  20. isolate->ThrowException(Exception::TypeError(
  21. String::NewFromUtf8(isolate, "Wrong number of arguments")));
  22. return;
  23. }
  24. // Check the argument types
  25. if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
  26. isolate->ThrowException(Exception::TypeError(
  27. String::NewFromUtf8(isolate, "Wrong arguments")));
  28. return;
  29. }
  30. // Perform the operation
  31. double value = args[0]->NumberValue() + args[1]->NumberValue();
  32. Local<Number> num = Number::New(isolate, value);
  33. // Set the return value (using the passed in
  34. // FunctionCallbackInfo<Value>&)
  35. args.GetReturnValue().Set(num);
  36. }
  37. void Init(Local<Object> exports) {
  38. NODE_SET_METHOD(exports, "add", Add);
  39. }
  40. NODE_MODULE(addon, Init)
  41. } // namespace demo

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. console.log('This should be eight:', addon.add(3,5));

回调函数

在 Node.js 开发环境中使用 JavaScript 通过插件向 C++ 函数传递并执行一个回调函数是非常常见的操作,下面代码演示了如何在 C++ 中执行回调函数:

  1. // addon.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::Function;
  5. using v8::FunctionCallbackInfo;
  6. using v8::Isolate;
  7. using v8::Local;
  8. using v8::Null;
  9. using v8::Object;
  10. using v8::String;
  11. using v8::Value;
  12. void RunCallback(const FunctionCallbackInfo<Value>& args) {
  13. Isolate* isolate = args.GetIsolate();
  14. Local<Function> cb = Local<Function>::Cast(args[0]);
  15. const unsigned argc = 1;
  16. Local<Value> argv[argc] = { String::NewFromUtf8(isolate, "hello world") };
  17. cb->Call(Null(isolate), argc, argv);
  18. }
  19. void Init(Local<Object> exports, Local<Object> module) {
  20. NODE_SET_METHOD(module, "exports", RunCallback);
  21. }
  22. NODE_MODULE(addon, Init)
  23. } // namespace demo

值得注意的是,在上面的示例中 Init 函数使用了两个参数,其中第二个参数用于接收完整的 module 对象。这种做法便于插件通过简单的函数重写 exports,而无需将函数挂载在 exports 之下。

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. var obj1 = addon('hello');
  4. var obj2 = addon('world');
  5. console.log(obj1.msg+' '+obj2.msg); // 'hello world'
  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. addon((msg) => {
  4. console.log(msg); // 'hello world'
  5. });

在上面的示例中,回调函数是以同步方式调用的。

对象工厂

在下面的代码中演示了如何使用 C++ 函数创建和返回一个新对象,新对象有一个 msg 属性:

  1. // addon.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::FunctionCallbackInfo;
  5. using v8::Isolate;
  6. using v8::Local;
  7. using v8::Object;
  8. using v8::String;
  9. using v8::Value;
  10. void CreateObject(const FunctionCallbackInfo<Value>& args) {
  11. Isolate* isolate = args.GetIsolate();
  12. Local<Object> obj = Object::New(isolate);
  13. obj->Set(String::NewFromUtf8(isolate, "msg"), args[0]->ToString());
  14. args.GetReturnValue().Set(obj);
  15. }
  16. void Init(Local<Object> exports, Local<Object> module) {
  17. NODE_SET_METHOD(module, "exports", CreateObject);
  18. }
  19. NODE_MODULE(addon, Init)
  20. } // namespace demo

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. var obj1 = addon('hello');
  4. var obj2 = addon('world');
  5. console.log(obj1.msg+' '+obj2.msg); // 'hello world'

函数工厂

另一个常见操作就是通过 C++ 函数创建 Javascript 函数并将其返回到 JavaScript 开发环境中:

  1. // addon.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::Function;
  5. using v8::FunctionCallbackInfo;
  6. using v8::FunctionTemplate;
  7. using v8::Isolate;
  8. using v8::Local;
  9. using v8::Object;
  10. using v8::String;
  11. using v8::Value;
  12. void MyFunction(const FunctionCallbackInfo<Value>& args) {
  13. Isolate* isolate = args.GetIsolate();
  14. args.GetReturnValue().Set(String::NewFromUtf8(isolate, "hello world"));
  15. }
  16. void CreateFunction(const FunctionCallbackInfo<Value>& args) {
  17. Isolate* isolate = args.GetIsolate();
  18. Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
  19. Local<Function> fn = tpl->GetFunction();
  20. // omit this to make it anonymous
  21. fn->SetName(String::NewFromUtf8(isolate, "theFunction"));
  22. args.GetReturnValue().Set(fn);
  23. }
  24. void Init(Local<Object> exports, Local<Object> module) {
  25. NODE_SET_METHOD(module, "exports", CreateFunction);
  26. }
  27. NODE_MODULE(addon, Init)
  28. } // namespace demo

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. var fn = addon();
  4. console.log(fn()); // 'hello world'

包装 C++ 对象

在某些情况下,可以通过包装 C++ 对象或类让 JavaScript 使用 new 操作符创建新的实例:

  1. // addon.cc
  2. #include <node.h>
  3. #include "myobject.h"
  4. namespace demo {
  5. using v8::Local;
  6. using v8::Object;
  7. void InitAll(Local<Object> exports) {
  8. MyObject::Init(exports);
  9. }
  10. NODE_MODULE(addon, InitAll)
  11. } // namespace demo

然后,在 myobject.h 头文件中,包装类继承 node::ObjectWrap

  1. // myobject.h
  2. #ifndef MYOBJECT_H
  3. #define MYOBJECT_H
  4. #include <node.h>
  5. #include <node_object_wrap.h>
  6. namespace demo {
  7. class MyObject : public node::ObjectWrap {
  8. public:
  9. static void Init(v8::Local<v8::Object> exports);
  10. private:
  11. explicit MyObject(double value = 0);
  12. ~MyObject();
  13. static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  14. static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
  15. static v8::Persistent<v8::Function> constructor;
  16. double value_;
  17. };
  18. } // namespace demo
  19. #endif

myobject.cc 文件中,具体实现需要暴漏出来的代码逻辑。在下面的代码中,通过将 plusOne() 方法挂载在构造器的原型上,将其暴漏给了外部:

  1. // myobject.cc
  2. #include "myobject.h"
  3. namespace demo {
  4. using v8::Function;
  5. using v8::FunctionCallbackInfo;
  6. using v8::FunctionTemplate;
  7. using v8::Isolate;
  8. using v8::Local;
  9. using v8::Number;
  10. using v8::Object;
  11. using v8::Persistent;
  12. using v8::String;
  13. using v8::Value;
  14. Persistent<Function> MyObject::constructor;
  15. MyObject::MyObject(double value) : value_(value) {
  16. }
  17. MyObject::~MyObject() {
  18. }
  19. void MyObject::Init(Local<Object> exports) {
  20. Isolate* isolate = exports->GetIsolate();
  21. // Prepare constructor template
  22. Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  23. tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
  24. tpl->InstanceTemplate()->SetInternalFieldCount(1);
  25. // Prototype
  26. NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
  27. constructor.Reset(isolate, tpl->GetFunction());
  28. exports->Set(String::NewFromUtf8(isolate, "MyObject"),
  29. tpl->GetFunction());
  30. }
  31. void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  32. Isolate* isolate = args.GetIsolate();
  33. if (args.IsConstructCall()) {
  34. // Invoked as constructor: `new MyObject(...)`
  35. double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
  36. MyObject* obj = new MyObject(value);
  37. obj->Wrap(args.This());
  38. args.GetReturnValue().Set(args.This());
  39. } else {
  40. // Invoked as plain function `MyObject(...)`, turn into construct call.
  41. const int argc = 1;
  42. Local<Value> argv[argc] = { args[0] };
  43. Local<Function> cons = Local<Function>::New(isolate, constructor);
  44. args.GetReturnValue().Set(cons->NewInstance(argc, argv));
  45. }
  46. }
  47. void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  48. Isolate* isolate = args.GetIsolate();
  49. MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
  50. obj->value_ += 1;
  51. args.GetReturnValue().Set(Number::New(isolate, obj->value_));
  52. }
  53. } // namespace demo

构建该示例之前,需要将 myobject.cc 文件名添加到 binding.gyp 文件中:

  1. {
  2. "targets": [
  3. {
  4. "target_name": "addon",
  5. "sources": [
  6. "addon.cc",
  7. "myobject.cc"
  8. ]
  9. }
  10. ]
  11. }

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. var obj = new addon.MyObject(10);
  4. console.log( obj.plusOne() ); // 11
  5. console.log( obj.plusOne() ); // 12
  6. console.log( obj.plusOne() ); // 13

包装对象工厂方法

此外,可以使用工厂模式在 JavaScript 中隐式创建对象实例:

  1. var obj = addon.createObject();
  2. // instead of:
  3. // var obj = new addon.Object();

首先,需要在 addon.cc 文件中实现 createObject() 方法的逻辑:

  1. // addon.cc
  2. #include <node.h>
  3. #include "myobject.h"
  4. namespace demo {
  5. using v8::FunctionCallbackInfo;
  6. using v8::Isolate;
  7. using v8::Local;
  8. using v8::Object;
  9. using v8::String;
  10. using v8::Value;
  11. void CreateObject(const FunctionCallbackInfo<Value>& args) {
  12. MyObject::NewInstance(args);
  13. }
  14. void InitAll(Local<Object> exports, Local<Object> module) {
  15. MyObject::Init(exports->GetIsolate());
  16. NODE_SET_METHOD(module, "exports", CreateObject);
  17. }
  18. NODE_MODULE(addon, InitAll)
  19. } // namespace demo

myobject.h 头文件中,添加静态方法 NewInstance() 用于实例化对象,该方法用于替换 JavaScript 中的 new 操作:

  1. // myobject.h
  2. #ifndef MYOBJECT_H
  3. #define MYOBJECT_H
  4. #include <node.h>
  5. #include <node_object_wrap.h>
  6. namespace demo {
  7. class MyObject : public node::ObjectWrap {
  8. public:
  9. static void Init(v8::Isolate* isolate);
  10. static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
  11. private:
  12. explicit MyObject(double value = 0);
  13. ~MyObject();
  14. static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  15. static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
  16. static v8::Persistent<v8::Function> constructor;
  17. double value_;
  18. };
  19. } // namespace demo
  20. #endif

myobject.cc 文件的具体实现与前例代码非常类似:

  1. // myobject.cc
  2. #include <node.h>
  3. #include "myobject.h"
  4. namespace demo {
  5. using v8::Function;
  6. using v8::FunctionCallbackInfo;
  7. using v8::FunctionTemplate;
  8. using v8::Isolate;
  9. using v8::Local;
  10. using v8::Number;
  11. using v8::Object;
  12. using v8::Persistent;
  13. using v8::String;
  14. using v8::Value;
  15. Persistent<Function> MyObject::constructor;
  16. MyObject::MyObject(double value) : value_(value) {
  17. }
  18. MyObject::~MyObject() {
  19. }
  20. void MyObject::Init(Isolate* isolate) {
  21. // Prepare constructor template
  22. Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  23. tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
  24. tpl->InstanceTemplate()->SetInternalFieldCount(1);
  25. // Prototype
  26. NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
  27. constructor.Reset(isolate, tpl->GetFunction());
  28. }
  29. void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  30. Isolate* isolate = args.GetIsolate();
  31. if (args.IsConstructCall()) {
  32. // Invoked as constructor: `new MyObject(...)`
  33. double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
  34. MyObject* obj = new MyObject(value);
  35. obj->Wrap(args.This());
  36. args.GetReturnValue().Set(args.This());
  37. } else {
  38. // Invoked as plain function `MyObject(...)`, turn into construct call.
  39. const int argc = 1;
  40. Local<Value> argv[argc] = { args[0] };
  41. Local<Function> cons = Local<Function>::New(isolate, constructor);
  42. args.GetReturnValue().Set(cons->NewInstance(argc, argv));
  43. }
  44. }
  45. void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  46. Isolate* isolate = args.GetIsolate();
  47. const unsigned argc = 1;
  48. Local<Value> argv[argc] = { args[0] };
  49. Local<Function> cons = Local<Function>::New(isolate, constructor);
  50. Local<Object> instance = cons->NewInstance(argc, argv);
  51. args.GetReturnValue().Set(instance);
  52. }
  53. void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  54. Isolate* isolate = args.GetIsolate();
  55. MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
  56. obj->value_ += 1;
  57. args.GetReturnValue().Set(Number::New(isolate, obj->value_));
  58. }
  59. } // namespace demo

构建示例之前,先将 myobject.cc 文件名添加到 binding.gyp 配置文件中:

  1. {
  2. "targets": [
  3. {
  4. "target_name": "addon",
  5. "sources": [
  6. "addon.cc",
  7. "myobject.cc"
  8. ]
  9. }
  10. ]
  11. }

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const createObject = require('./build/Release/addon');
  3. var obj = createObject(10);
  4. console.log( obj.plusOne() ); // 11
  5. console.log( obj.plusOne() ); // 12
  6. console.log( obj.plusOne() ); // 13
  7. var obj2 = createObject(20);
  8. console.log( obj2.plusOne() ); // 21
  9. console.log( obj2.plusOne() ); // 22
  10. console.log( obj2.plusOne() ); // 23

传递包装对象

在包装盒返回 C++ 对象之外,还可以使用 Node.js 中的辅助函数 node::ObjectWrap::Unwrap 解包包装对象。在下面的示例中演示了函数 add() 如何解析传入的两个对象参数:

  1. // addon.cc
  2. #include <node.h>
  3. #include <node_object_wrap.h>
  4. #include "myobject.h"
  5. namespace demo {
  6. using v8::FunctionCallbackInfo;
  7. using v8::Isolate;
  8. using v8::Local;
  9. using v8::Number;
  10. using v8::Object;
  11. using v8::String;
  12. using v8::Value;
  13. void CreateObject(const FunctionCallbackInfo<Value>& args) {
  14. MyObject::NewInstance(args);
  15. }
  16. void Add(const FunctionCallbackInfo<Value>& args) {
  17. Isolate* isolate = args.GetIsolate();
  18. MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
  19. args[0]->ToObject());
  20. MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
  21. args[1]->ToObject());
  22. double sum = obj1->value() + obj2->value();
  23. args.GetReturnValue().Set(Number::New(isolate, sum));
  24. }
  25. void InitAll(Local<Object> exports) {
  26. MyObject::Init(exports->GetIsolate());
  27. NODE_SET_METHOD(exports, "createObject", CreateObject);
  28. NODE_SET_METHOD(exports, "add", Add);
  29. }
  30. NODE_MODULE(addon, InitAll)
  31. } // namespace demo

myobject.h 头文件中,添加一个新的公共方法,用于调用对象解包后的私有属性:

  1. // myobject.h
  2. #ifndef MYOBJECT_H
  3. #define MYOBJECT_H
  4. #include <node.h>
  5. #include <node_object_wrap.h>
  6. namespace demo {
  7. class MyObject : public node::ObjectWrap {
  8. public:
  9. static void Init(v8::Isolate* isolate);
  10. static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
  11. inline double value() const { return value_; }
  12. private:
  13. explicit MyObject(double value = 0);
  14. ~MyObject();
  15. static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  16. static v8::Persistent<v8::Function> constructor;
  17. double value_;
  18. };
  19. } // namespace demo
  20. #endif

myobject.cc 的具体实现和前面的示例类似:

  1. // myobject.cc
  2. #include <node.h>
  3. #include "myobject.h"
  4. namespace demo {
  5. using v8::Function;
  6. using v8::FunctionCallbackInfo;
  7. using v8::FunctionTemplate;
  8. using v8::Isolate;
  9. using v8::Local;
  10. using v8::Object;
  11. using v8::Persistent;
  12. using v8::String;
  13. using v8::Value;
  14. Persistent<Function> MyObject::constructor;
  15. MyObject::MyObject(double value) : value_(value) {
  16. }
  17. MyObject::~MyObject() {
  18. }
  19. void MyObject::Init(Isolate* isolate) {
  20. // Prepare constructor template
  21. Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  22. tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
  23. tpl->InstanceTemplate()->SetInternalFieldCount(1);
  24. constructor.Reset(isolate, tpl->GetFunction());
  25. }
  26. void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  27. Isolate* isolate = args.GetIsolate();
  28. if (args.IsConstructCall()) {
  29. // Invoked as constructor: `new MyObject(...)`
  30. double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
  31. MyObject* obj = new MyObject(value);
  32. obj->Wrap(args.This());
  33. args.GetReturnValue().Set(args.This());
  34. } else {
  35. // Invoked as plain function `MyObject(...)`, turn into construct call.
  36. const int argc = 1;
  37. Local<Value> argv[argc] = { args[0] };
  38. Local<Function> cons = Local<Function>::New(isolate, constructor);
  39. args.GetReturnValue().Set(cons->NewInstance(argc, argv));
  40. }
  41. }
  42. void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  43. Isolate* isolate = args.GetIsolate();
  44. const unsigned argc = 1;
  45. Local<Value> argv[argc] = { args[0] };
  46. Local<Function> cons = Local<Function>::New(isolate, constructor);
  47. Local<Object> instance = cons->NewInstance(argc, argv);
  48. args.GetReturnValue().Set(instance);
  49. }
  50. } // namespace demo

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

  1. // test.js
  2. const addon = require('./build/Release/addon');
  3. var obj1 = addon.createObject(10);
  4. var obj2 = addon.createObject(20);
  5. var result = addon.add(obj1, obj2);
  6. console.log(result); // 30

AtExit 钩子函数

“AtExit” 钩子函数在 Node.js 事件循环结束之后、JavaScript VM 终止以及 Node.js 退出之前调用。需要使用 “node::AtExit” 接口注册 “AtExit” 钩子函数。

void AtExit(callback, args)

  • callbackvoid (*)(vodi*),一个指向函数的指针,函数结束时调用
  • argsvoid*,函数结束是传递给 callback 的指针

注册 AtExit 钩子函数需要在事件循环之后和 VM 退出之前。callback 按照后入先出的顺序执行。下面 addon.cc 文件中的代码实现了 AtExit 钩子函数:

  1. // addon.cc
  2. #undef NDEBUG
  3. #include <assert.h>
  4. #include <stdlib.h>
  5. #include <node.h>
  6. namespace demo {
  7. using node::AtExit;
  8. using v8::HandleScope;
  9. using v8::Isolate;
  10. using v8::Local;
  11. using v8::Object;
  12. static char cookie[] = "yum yum";
  13. static int at_exit_cb1_called = 0;
  14. static int at_exit_cb2_called = 0;
  15. static void at_exit_cb1(void* arg) {
  16. Isolate* isolate = static_cast<Isolate*>(arg);
  17. HandleScope scope(isolate);
  18. Local<Object> obj = Object::New(isolate);
  19. assert(!obj.IsEmpty()); // assert VM is still alive
  20. assert(obj->IsObject());
  21. at_exit_cb1_called++;
  22. }
  23. static void at_exit_cb2(void* arg) {
  24. assert(arg == static_cast<void*>(cookie));
  25. at_exit_cb2_called++;
  26. }
  27. static void sanity_check(void*) {
  28. assert(at_exit_cb1_called == 1);
  29. assert(at_exit_cb2_called == 2);
  30. }
  31. void init(Local<Object> exports) {
  32. AtExit(sanity_check);
  33. AtExit(at_exit_cb2, cookie);
  34. AtExit(at_exit_cb2, cookie);
  35. AtExit(at_exit_cb1, exports->GetIsolate());
  36. }
  37. NODE_MODULE(addon, init);
  38. } // namespace demo

编译成功之后,在 Node.js 环境中使用 require() 函数加载插件:

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