Addons是用C ++编写的动态链接(DLL:Dynamic Linked Library)的共享对象,利用V8提供的API,require()函数可以将Addons加载为普通的Node.js模块。Addons提供JavaScript和C / C ++库之间的接口。

在 Nodejs 中, 当编译出 DLL 的时候, 会被导出为.node 的后缀文件. 然后可以 require 该文件, 像 js 文件一样。

在做一些高性能或者底层模块的时候,需要用到一些C++库,NodeJS C++插件可以帮助我们封装这些C++库的接口,使得JavaScript具备调用C++库的能力。

因为 Nodejs 是由低层级的 C 和 C++编译而成的, 所以本身就具有与 C 和 C++相互调用的能力.

ABI Application Binary Interface 应用二进制接口
ABI 是特指应用去访问编译好|compiled的程序, 跟 API(Application Programming Interface)非常相似, 只不过是与二进制文件进行交互, 而且是访问内存地址去查找 Symbols, 比如 numbers, objects, classes和 functions

举个例子, 有个 Native Addon 想添加一个sayHello的方法到exports对象上, 他可以通过访问 Libuv 的 API 来创建一个新的线程,异步的执行任务, 执行完毕之后再调用回调函数. 这样 Nodejs 提供的 ABI 的工作就完成了.

实施Addons有四种选择:

  1. N-API: 基于C的API,可确保跨不同节点版本以及JavaScript引擎的ABI稳定性(Node官方维护)。
  2. NAN:项目最开始就是为了抽象 nodejs 和 v8 引擎的内部实现(非官方维护)。
  3. node-addon-api: header-only C++ wrapper classes which simplify the use of the C-based N-API(官方维护).
  4. V8,libuv和Node.js库(底层自带)

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

NAN缺点:

  • 不完全抽象出了 V8 的 api
  • 并不提供 nodejs 所有库的支持
  • 不是Nodejs 官方维护的库.

node-addon-api:有自己的头文件napi.h, 包含了 N-API 的所有对 C++的封装, 并且跟 N-API 一样是由官方维护, 点这里查看仓库.因为他的使用相较于其他更加的简单, 所以在进行 C++API 封装的时候优先选择该方法.

当不使用N-API时,实现Addons会很复杂,涉及一些组件和API的知识:

  • V8: Node.js当前使用的C ++库提供JavaScript实现。V8提供了用于创建对象,调用函数等的机制。V8的API主要记录在 v8.h 头文件(Node.js源代码树中的 deps/v8/include/v8.h )中,该文件也可以在线获得
  • libuv: 实现Node.js事件循环,其工作线程以及平台的所有异步行为的C库。它还用作跨平台的抽象库,在所有主要操作系统上提供类似于POSIX的轻松访问,可以访问许多常见的系统任务,例如与文件系统,套接字,计时器和系统事件进行交互。libuv还提供了类似于pthreads的线程抽象,可用于驱动需要超越标准事件循环的更复杂的异步Addons。鼓励Addon作者考虑如何通过通过libuv将工作卸载到非阻塞系统操作,工作线程或定制使用libuv线程来避免因I / O或其他耗时的任务而阻塞事件循环。
  • 内部Node.js库。 Node.js本身会导出Addons可以使用的C ++ API,其中最重要的是 node::ObjectWrap class。
  • Node.js包括其他静态链接的库,包括OpenSSL。这些其他库位于Node.js源代码树的deps/目录中。Node.js只会有目的地重新导出libuv,OpenSSL,V8和zlib符号,并且Addons可以在不同程度上使用它们。

本文记录利用基础的V8 API编写NodeJS C++插件的过程,实现C++和JavaScript之间的参数传递、函数调用以及回调、异常处理以及对象函数传递等功能。

记录过程中也会对部分概念和API进行阐述。详细的示例可以下载

基本概念

1 Hello world示例

首先通过Hello world程序,来熟悉C++与Nodejs的消息和方法互通。在示例中,C++模块向JavaScript暴露一个Hello接口,在Javascript中调用该接口会得到返回的值:hello world;
等效于:

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

C++实现

  1. // hello.cc
  2. #include <node.h>
  3. namespace demo
  4. {
  5. using v8::FunctionCallbackInfo;
  6. using v8::Isolate;
  7. using v8::Local;
  8. using v8::NewStringType;
  9. using v8::Object;
  10. using v8::String;
  11. using v8::Value;
  12. /* 通过 FunctionCallbackInfo<Value>& args 可以设置返回值 */
  13. void Method(const FunctionCallbackInfo<Value> &args)
  14. {
  15. Isolate *isolate = args.GetIsolate();
  16. args.GetReturnValue().Set(String::NewFromUtf8(
  17. isolate, "Hello world", NewStringType::kNormal)
  18. .ToLocalChecked());
  19. }
  20. void Initialize(Local<Object> exports)
  21. {
  22. /* 设置模块的导出方法 hello */
  23. /* 等价于 js 模块中的 module.exports.hello = hello */
  24. NODE_SET_METHOD(exports, "hello", Method);
  25. }
  26. NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
  27. }

JavaScript调用C++模块的方法时,会传递一个V8对象,类型为 FunctionCallbackInfo<Value> 。通过这个V8对象,JavaScript可以向C++接口传递参数,C++函数也可以通过这个对象来向JavaScript回传信息,即设置返回值。

在C++接口中,通过参数 const FunctionCallbackInfo<Value>& args 可以拿到一个 Isolate 对象,Isolate 代表一个V8虚拟机实例。通过 args.GetIsolate() 可以获取到运行JavaScript调用者的V8虚拟机实例。这个V8实例包含了内存堆,在C++接口中创建V8提供的JavaScript对象类型实例的时候会使用到。
例如前面的hello world例子中,在创建一个JS字符串的时候需要传递 isolate 对象,表示在该V8虚拟机上创建了一个JS字符串对象,之后该字符串便可以被V8虚拟机上运行的JS调用者所使用。

Local是一个x,Local<SomeType> 代表指向某种类型的句柄。例如模块的exports属性是一个JavaScript对象,句柄类型为 Local<Object> 。传递给init函数的参数其实是指向相应对象的句柄。

NODE_MODULE是一个宏,设置模块初始化函数为init。init函数中执行模块的初始化,当模块第一次被加载进NodeJS应用中的时候就会执行init函数,init函数中可以设置exports属性将C++接口暴露出去给JavaScript使用。NODE_SET_METHOD用于设置属性或方法,第二个参数为属性名,第三个参数为方法对应的属性值。如果需要给exports对象设置多个属性或方法,可以调用多次NODE_SET_METHOD。exports对象上设置的属性方法将会作为接口暴露给外部使用。

编写NodeJS C++插件必须遵循以下这种模式:
必须有一个初始化函数对模块进行初始化(设置方法属性等),然后加上NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)设置模块名和初始化函数。
初始化函数可以有两种写法,

  • 第一种写法常用于设置模块的exports对象上的某个属性或方法
  • 第二种写法可用于直接重写整个exports对象。 ```cpp // 基础语法 void Initialize(Local exports); NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

    // 写法1 void Initialize_1(Local exports) { // 进行初始化… // example // 等价于js模块中的 module.exports.hello = hello NODE_SET_METHOD(exports, “hello”, hello); } NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize_1)

    // 写法2 void Initialize_2(Local exports, Local module) { // 进行初始化… // example // 等价于js模块中的 module.exports = hello NODE_SET_METHOD(module, “exports”, hello); } NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize_2)

    1. NODE_MODULE后没有分号,因为它不是函数(请参见node.h).<br />module_name必须与最终二进制文件名匹配(不包括.node后缀).<br />在hello.cc示例中,初始化函数为Initialize,附加模块名称为addon.<br />在使用 `node-gyp` 构建插件时,将 macro `NODE_GYP_MODULE_NAME` 用作 `NODE_MODULE()` 的第一个参数将确保将最终二进制文件的名称传递给 `NODE_MODULE()` .
    2. <a name="oeLMu"></a>
    3. #### 编译文件
    4. 编写源代码后,必须将其编译到二进制addon.node文件中。为此,请在项目的根目录创建一个名为 binding.gyp 的文件,该文件使用类似JSON的格式描述模块的构建配置,该文件由专门用于编译Node.js插件的工具node-gyp使用。<br />如果有多个插件,可以在 `targets` 数组上继续添加。数组的元素为一个对象,对象的 `target_name` 属性指明构建后的插件名称,sources属性则是C++源码路径。
    5. ```bash
    6. {
    7. "targets": [
    8. {
    9. "target_name": "addon",
    10. "sources": [ "./src/hello.cc" ]
    11. }
    12. ]
    13. }

    构建

    使用node-gyp configure生成当前平台的适当项目构建文件,这将在build /目录中生成Makefile(在Unix平台上)或vcxproj文件(在Windows上)。
    接下来,调用 node-gyp build 命令来生成已编译的 addon.node 文件。这将被放入 build/Release/ 目录。

    $ node-gyp configure
    
    $ node-gyp build
    

    构建完成后,在项目 build/Release/ 目录下会生成一个 .node 文件 addon.node ,此文件是二进制文件,可以不用关注内部的代码;

    运行

    当使用npm install安装Node.js插件时,npm使用自己的捆绑版的node-gyp来执行相同的操作,从而根据需要为用户平台生成插件的编译版本。
    构建完成后,可以通过将 require() 指向构建的 addon.node 模块,从Node.js中使用二进制Addon:

    const addon = require('../build/Release/addon');
    
    const msg = addon.hello();
    console.log(msg);
    
    // output:
    // Hello world
    

    恭喜,第一个Hello world 的 C++ 插件已经完成。

    2. 上下文感知Addon

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

    可以使用宏 NODE_MODULE_INITIALIZER 构造上下文感知的addon,该宏扩展为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() ,该宏还将构造一个上下文感知的addon。与 NODE_MODULE() 用于围绕给定的插件初始化器函数构造插件的方法不同,NODE_MODULE_INIT() 用作此类初始化器的声明,后跟函数体。

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

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

    选择构建上下文感知的加载项时,它有责任仔细管理全局静态数据。由于addon可能会多次加载(甚至可能从不同的线程加载),因此必须正确保护插件中存储的所有全局静态数据,并且不得包含对JavaScript对象的任何持久引用。原因是JavaScript对象仅在一个上下文中有效,并且从错误的上下文或与创建对象不同的线程访问时,可能会导致崩溃。

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

    • 定义一个类,该类将保存每个附加实例数据,并且具有以下形式的静态成员:

      static void DeleteInstance(void* data) {
      // Cast `data` to an instance of the class and delete it.
      }
      
    • 在addon初始化程序中堆分配此类的实例。可以使用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调用的每个绑定。每个插件实例数据还必须传递到插件可能创建的任何异步回调中。

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

    #include <node.h>
    
    using namespace v8;
    
    class AddonData {
     public:
      explicit AddonData(Isolate* isolate):
          call_count(0) {
        // Ensure this per-addon-instance data is deleted at environment cleanup.
        node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
      }
    
      // Per-addon data.
      int call_count;
    
      static void DeleteInstance(void* data) {
        delete static_cast<AddonData*>(data);
      }
    };
    
    static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
      // Retrieve the per-addon-instance data.
      AddonData* data =
          reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
      data->call_count++;
      info.GetReturnValue().Set((double)data->call_count);
    }
    
    // Initialize this addon to be context-aware.
    NODE_MODULE_INIT(/* exports, module, context */) {
      Isolate* isolate = context->GetIsolate();
    
      // Create a new instance of `AddonData` for this instance of the addon and
      // tie its life cycle to that of the Node.js environment.
      AddonData* data = new AddonData(isolate);
    
      // Wrap the data in a `v8::External` so we can pass it to the method we
      // expose.
      Local<External> external = External::New(isolate, data);
    
      // Expose the method `Method` to JavaScript, and make sure it receives the
      // per-addon-instance data we created above by passing `external` as the
      // third parameter to the `FunctionTemplate` constructor.
      exports->Set(context,
                   String::NewFromUtf8(isolate, "method", NewStringType::kNormal)
                      .ToLocalChecked(),
                   FunctionTemplate::New(isolate, Method, external)
                      ->GetFunction(context).ToLocalChecked()).FromJust();
    }
    

    3. Function arguments

    插件通常会公开可从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::NewStringType;
    using v8::Number;
    using v8::Object;
    using v8::String;
    using v8::Value;
    
    // This is the implementation of the "add" method
    // Input arguments are passed using the
    // const FunctionCallbackInfo<Value>& args struct
    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",
                                NewStringType::kNormal).ToLocalChecked()));
        return;
      }
    
      // Check the argument types
      if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate,
                                "Wrong arguments",
                                NewStringType::kNormal).ToLocalChecked()));
        return;
      }
    
      // Perform the operation
      double value =
          args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
      Local<Number> num = Number::New(isolate, value);
    
      // Set the return value (using the passed in
      // FunctionCallbackInfo<Value>&)
      args.GetReturnValue().Set(num);
    }
    
    void Init(Local<Object> exports) {
      NODE_SET_METHOD(exports, "add", Add);
    }
    
    NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
    
    }  // namespace demo
    

    Once compiled, the example Addon can be required and used from within Node.js:

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

    4. Callbacks

    // addon.cc
    #include <node.h>
    
    namespace demo {
    
    using v8::Context;
    using v8::Function;
    using v8::FunctionCallbackInfo;
    using v8::Isolate;
    using v8::Local;
    using v8::NewStringType;
    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",
                              NewStringType::kNormal).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
    

    To test it, run the following javascript:

    // test.js
    const addon = require('./build/Release/addon');
    
    addon((msg) => {
      console.log(msg);
    // Prints: 'hello world'
    });
    

    5. Object factory

    // addon.cc
    #include <node.h>
    
    namespace demo {
    
    using v8::Context;
    using v8::FunctionCallbackInfo;
    using v8::Isolate;
    using v8::Local;
    using v8::NewStringType;
    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",
                                   NewStringType::kNormal).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
    

    To test it in Javascript:

    // test.js
    const addon = require('./build/Release/addon');
    
    const obj1 = addon('hello');
    const obj2 = addon('world');
    console.log(obj1.msg, obj2.msg);
    // Prints: 'hello world'
    

    6. Wrapping C++ objects

    还可以用允许使用JavaScript new运算符创建新实例的方式包装C ++对象/类:

    // addon.cc
    #include <node.h>
    #include "myobject.h"
    
    namespace demo {
    
    using v8::Local;
    using v8::Object;
    
    void InitAll(Local<Object> exports) {
      MyObject::Init(exports);
    }
    
    NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
    
    }  // namespace demo
    

    Then, in myobject.h, the wrapper class inherits from 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::NewStringType;
    using v8::Number;
    using v8::Object;
    using v8::ObjectTemplate;
    using v8::String;
    using v8::Value;
    
    MyObject::MyObject(double value) : value_(value) {
    }
    
    MyObject::~MyObject() {
    }
    
    void MyObject::Init(Local<Object> exports) {
      Isolate* isolate = exports->GetIsolate();
      Local<Context> context = isolate->GetCurrentContext();
    
      Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
      addon_data_tpl->SetInternalFieldCount(1);  // 1 field for the MyObject::New()
      Local<Object> addon_data =
          addon_data_tpl->NewInstance(context).ToLocalChecked();
    
      // Prepare constructor template
      Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
      tpl->SetClassName(String::NewFromUtf8(
          isolate, "MyObject", NewStringType::kNormal).ToLocalChecked());
      tpl->InstanceTemplate()->SetInternalFieldCount(1);
    
      // Prototype
      NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
    
      Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
      addon_data->SetInternalField(0, constructor);
      exports->Set(context, String::NewFromUtf8(
          isolate, "MyObject", NewStringType::kNormal).ToLocalChecked(),
                   constructor).FromJust();
    }
    
    void MyObject::New(const FunctionCallbackInfo<Value>& args) {
      Isolate* isolate = args.GetIsolate();
      Local<Context> context = isolate->GetCurrentContext();
    
      if (args.IsConstructCall()) {
        // Invoked as constructor: `new MyObject(...)`
        double value = args[0]->IsUndefined() ?
            0 : args[0]->NumberValue(context).FromMaybe(0);
        MyObject* obj = new MyObject(value);
        obj->Wrap(args.This());
        args.GetReturnValue().Set(args.This());
      } else {
        // Invoked as plain function `MyObject(...)`, turn into construct call.
        const int argc = 1;
        Local<Value> argv[argc] = { args[0] };
        Local<Function> cons =
            args.Data().As<Object>()->GetInternalField(0).As<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
    

    To build this example, the myobject.cc file must be added to the binding.gyp:

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

    Test it with:

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

    7. 异常处理

    C++插件提供的接口函数如果在运行后遇到异常,JavaScript调用者是否可以知道异常并进行处理呢?答案是可以的,V8提供的API使得C++可以直接向JavaScript抛出异常。前面提到,Isolate对象代表一个V8虚拟机实例。我们可以通过这个实例直接向该V8虚拟机抛出异常,该虚拟机实例上运行的JavaScript代码只要对异常进行捕获就可以知道异常的发生并进行相应的处理了。

    上面的累加求和例子中,没有考虑传递的参数类型就直接进行求和,在某些情况下可能发生异常。接下来,对上面的例子进行改进,增加异常处理机制,探索C++插件如何向JavaScript报告异常。代码如下所示。

    #include <node.h>
    
    namespace FunctionArgumentsAndCallbackDemo {
      using v8::Function;
      using v8::FunctionCallbackInfo;
      using v8::Isolate;
      using v8::Local;
      using v8::Number;
      using v8::Object;
      using v8::Value;
      using v8::Null;
      using v8::Exception;
      using v8::String;
    
      void accumulate (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        /* 参数不合理异常 */
        if (args.Length() < 1) {
          isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "Arguments Number Error.")
          ));
          return;
        }
    
        /* 没有回调函数 */
        if (!args[args.Length() - 1]->IsFunction()) {
          isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "No Callback Error.")
          ));
          return;
        }
    
        /* 提取通过参数传递的回调函数 */
        Local<Function> callback = Local<Function>::Cast(args[args.Length() - 1]);
    
        /* 遍历参数进行求和 */
        double sum = 0.0;
        for (int i = 0; i < args.Length() - 1; ++i) {
          /* 如果参数不是数字,向js抛出异常 */
          if (!args[i]->IsNumber()) {
            isolate->ThrowException(Exception::TypeError(
              String::NewFromUtf8(isolate, "Arguments Type Error.")
            ));
            return;
          } else {
            sum += args[i]->NumberValue();
          }
        }
    
        /* 将求和结果转成一个js Number, 通过回调函数进行返回 */
        Local<Number> num = Number::New(isolate, sum);
        Local<Value> argv[1] = { num };
        callback->Call(Null(isolate), 1, argv);
      }
    
      void init (Local<Object> exports) {
        NODE_SET_METHOD(exports, "accumulate", accumulate);
      }
    
      NODE_MODULE(NODE_GYP_MODULE_NAME, init)
    }
    

    通过Isolate对象的ThrowException方法,可以直接向JavaScript抛出异常。在JavaScript中,通过try/catch机制便可以捕获和处理异常。下面是代码示例,调用C++接口的时候故意引发异常,捕获到异常后将异常信息进行输出。如果图所示,可以成功实现C++模块向JavaScript抛出异常以及JavaScript捕获处理异常。

    // exception demo
    try {
      Accumulate.accumulate()
    } catch (err) {
      console.log('[ExceptionDemo] ' + err)
    }
    
    try {
      Accumulate.accumulate(1, 2, 3)
    } catch (err) {
      console.log('[ExceptionDemo] ' + err)
    }
    
    try {
      Accumulate.accumulate(1, 2, 'a', (sum) => {
        console.log(sum)
      })
    } catch (err) {
      console.log('[ExceptionDemo] ' + err)
    }
    

    N-API

    N-API是用于构建Native Addon的API。它独立于底层JavaScript运行时(例如V8),并作为Node.js本身的一部分进行维护。
    该API在所有版本的Node.js中都是稳定的应用程序二进制接口(ABI)。旨在使Addons与基础JavaScript引擎的更改保持隔离,并使为一个版本编译的模块可以在更高版本的Node.js上运行,而无需重新编译。
    唯一的区别是N-API使用的API集。代替使用V8或 Native Abstractions for Nodejs(NAN) ,使用N-API中可用的功能。

    文档推荐

    1. Node C++ Addons

    https://nodejs.org/api/addons.html

    2. Nodejs C++ Addons基础

    https://blog.csdn.net/hongchh/article/details/79961474

    安装编译C++环境

    如果需要测试c++ code,在Mac客户端上建议安装XCode,在app store中可以安装;

    // 全局安装node-gyp
    $ npm install -g node-gyp
    
    // 安装Xcode Comman Line Tools
    $ xcode-select --install
    
    // 查看安装的Xcode版本
    $ xcode-select -version
    
    // find them under the menu
    // Xcode -> Open Developer Tool -> More Developer Tools.... 
    // This step will install clang, clang++, and make.
    

    To see if Xcode Command Line Tools is installed in a way that will work with node-gyp, run:

    $ /usr/sbin/pkgutil --packages | grep CL
    # com.apple.pkg.CLTools_Executables should be listed. If it isn't, this test failed.
    
    $ /usr/sbin/pkgutil --pkg-info com.apple.pkg.CLTools_Executables
    # version: 11.0.0 (or later) should be listed. If it isn't, this test failed.
    # If both tests succeeded, you are done! You should be ready to install node-gyp.
    

    踩坑之旅

    1. Error

    xcode-select: error: tool ‘xcodebuild’ requires Xcode, 
    but active developer directory ‘/Library/Developer/CommandLineTools’ is a command line tools instance
    
    xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), 
    missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
    
    xcode-select: error: command line tools are already installed, use “Software Update” to install updates
    

    Solution:

    xcode-select — install
    sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
    sudo xcodebuild -license accept
    

    2. Error

    image.png

    gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
      ACTION Regenerating Makefile
    make: *** No rule to make target `Release/obj.target/addon/src/hello.o', needed by `Release/addon.node'.  Stop.
    gyp ERR! build error
    gyp ERR! stack Error: `make` failed with exit code: 2
    

    Solution:

    the filename in binding.gyp (field: targets -> sources) is not right!!!

    👍 21😄 4🎉 5❤️ 3🚀 1

    3. vscode 无法找到node.h

    # 执行搜索c++库命令
    $ gcc -v -E -x c++ -
    
    # open vscode
    $ command + shift + p
    
    # input 
    $ c/c++
    # select C/C++ Edit Configuration UI
    # find incloud path
    # 填写搜索到的结果
    
    # 通过命令搜索node.h头文件
    $ find / -name node.h | grep "[node-gyp]"
    # /System/Volumes/Data/usr/local/Cellar/node/11.14.0/include/node/node.h
    

    4. node-gyp build error

    Traceback (most recent call last):
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py", line 50, in <module>
        sys.exit(gyp.script_main())
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 554, in script_main
        return main(sys.argv[1:])
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 547, in main
        return gyp_main(args)
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 523, in gyp_main
        options.duplicate_basename_check)
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 139, in Load
        params['parallel'], params['root_targets'])
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 2783, in Load
        variables, includes, depth, check, True)
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 392, in LoadTargetBuildFile
        includes, True, check)
      File "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 244, in LoadOneBuildFile
        None)
      File "binding.gyp", line 8
        ]ƒ
         ^
    SyntaxError: invalid syntax
    gyp ERR! configure error
    gyp ERR! stack Error: `gyp` failed with exit code: 1
    gyp ERR! stack     at ChildProcess.onCpExit (/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16)
    gyp ERR! stack     at ChildProcess.emit (events.js:310:20)
    gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
    gyp ERR! System Darwin 19.4.0
    gyp ERR! command "/Users/luojinghui/.nvm/versions/node/v12.16.3/bin/node" "/Users/luojinghui/.nvm/versions/node/v12.16.3/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
    

    image.png
    Resolution:
    building.gyp 第8行多了一个f字母。。。
    参考:https://codeforgeek.com/make-failed-with-exit-code-2/