C++ class instance packaging and asynchronous operation of NodeJS C++ Addons

This article is a further exploration of the native writing method of NodeJS C++ Addons, and introduces the specific method of using the API provided by native Node and V8 to implement class packaging and asynchronous calls. Before reading this article, if you are not familiar with the basics of NodeJS C++ Addons, it is recommended to read the previous blog [NodeJS C++ Addons Basics] to understand and then come back to read this article.

The code samples used in this article can be found in this repository – [cpp-addons]

Remarks: This article aims to explore the native writing method of NodeJS C++ Addons and understand some of the underlying knowledge. The NodeJS version used is 8.11.1. Since the native API of V8 will change, the support of different versions of NodeJS may be different. Therefore, the code is not guaranteed to be compatible with all versions of NodeJS.

First, the packaging of C++ classes and object instances

In addition to providing functional interfaces to JavaScript, NodeJS C++ plugins can also directly provide JavaScript to use after packaging some C++ classes or C++ object instances. For example, suppose there is a class implemented in C++, the class name is SomeClass, and now you want to use the class directly in JavaScript, by new SomeClass(...)directly creating an instance of the class and using it.

Next, a simple example will be used to illustrate how to wrap C++ classes and objects. In this example, a C++ class will be implemented Accumulator, which is an accumulator, and provides add()two getAddTimes()methods. add()The method is used to accumulate parameters and return the current accumulated value, getAddTimes()which is to return the current accumulation times. When creating an Accumulatorinstance, you can specify the initial value at which the accumulation starts. In the end, what we expect to achieve is as follows, you can use this C++ class in JavaScript and create an instance of the class, and you can call methods defined on the class.

// cpp-object-wrap demo
const AccumulatorModule = require('./build/Release/Accumulator')
let acc = new AccumulatorModule.Accumulator(2)
console.log('[ObjectWrapDemo] 2 + 12 = ' + acc.add(12))
console.log('[ObjectWrapDemo] 2 + 12 + 5 = ' + acc.add(5))
console.log('[ObjectWrapDemo] add times: ' + acc.getAddTimes())

In C++, it Accumulatoris an classordinary class defined by keywords, while in JavaScript, a class is a JS function. In C++, Accumulatoran instance is an ordinary C++ class instance, and in JavaScript, an instance is a JS object. The JS function corresponds to an v8::Functioninstance in V8, and the JS object corresponds to an instance in V8 v8::Object. Therefore, the packaging to do is to C++类wrap one into an v8::Functioninstance, C++实例对象wrap one into an v8::Objectinstance, and then provide it to JavaScript use. The following is the implementation source code of the C++ plug-in, and the comments include an introduction to the packaging process.

#include <node.h>
#include <node_object_wrap.h>

namespace CppObjectWrapDemo {
  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;
  using v8::Exception;

  /* 将C++类封装给JS使用,需要继承node::ObjectWrap */
  class Accumulator : public node::ObjectWrap {
    public:
      /* 初始化该类的JS构造函数,并返回JS构造函数 */
      static Local<Function> init (Isolate* isolate) {
        /* 利用函数模板,将一个C++函数包装成JS函数 */
        Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, AccumulatorJS);
        tpl->SetClassName(String::NewFromUtf8(isolate, "Accumulator"));
        tpl->InstanceTemplate()->SetInternalFieldCount(1);
        /* 类方法定义在构造函数的prototype上 */
        NODE_SET_PROTOTYPE_METHOD(tpl, "add", add);
        NODE_SET_PROTOTYPE_METHOD(tpl, "getAddTimes", getAddTimes);
        /* 获取Accumulator类的JS构造函数 */
        Local<Function> fn = tpl->GetFunction();
        /* JS构造函数句柄存储于constructor上,后续还会使用到 */
        constructor.Reset(isolate, fn);
        return fn;
      }
    private:
      /* 成员变量 */
      static Persistent<Function> constructor;
      double value;
      int addTimes;

      /* 该类的C++构造函数,设置成员变量初始值 */
      explicit Accumulator (double initValue = 0) {
        this->value = initValue;
        this->addTimes = 0;
      }

      /* 该类的JS构造函数,创建该类的对象,并包装成JS对象然后进行返回 */
      static void AccumulatorJS (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        if (args.IsConstructCall()) {/* 通过 new Accumulator() 创建对象 */
          /* 提取参数数值 */
          double val = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
          /* 创建该类的实例对象 */
          Accumulator* obj = new Accumulator(val);
          /* 包装该对象 */
          obj->Wrap(args.This());
          /* 返回该对象 */
          args.GetReturnValue().Set(args.This());
        } else {/* 通过直接调用函数 Accumulator() 创建对象,抛出异常 */
          isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "Should use the new operator to create an instance.")
          ));
        }
      }

      /* 该类的成员方法,增加value的值 */
      static void add (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        /* 将被包装的JS对象还原为C++对象 */
        Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
        /* 访问C++对象上的成员变量进行操作 */
        obj->value += args[0]->NumberValue();
        obj->addTimes += 1;
        args.GetReturnValue().Set(Number::New(isolate, obj->value));
      }

      /* 该类的成员方法,获取累加次数 */
      static void getAddTimes (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        /* 将被包装的JS对象还原为C++对象 */
        Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
        args.GetReturnValue().Set(Number::New(isolate, obj->addTimes));
      }
  };

  Persistent<Function> Accumulator::constructor;

  void init (Local<Object> exports) {
    Isolate* isolate = exports->GetIsolate();
    /* 初始化Accumulator类的JS构造函数 */
    Local<Function> _Accumulator = Accumulator::init(isolate);
    /* 将Accumulator类的JS构造函数暴露给JS使用 */
    /* 这里不能使用NODE_SET_METHOD,因为NODE_SET_METHOD是暴露一个C++函数给JS使用 */
    /* NODE_SET_METHOD(exports, "Accumulator", _Accumulator); */
    /* 此处是暴露一个JS函数,它在C++里面表示为一个Function对象,不是一个C++函数 */
    /* 要通过设置属性的方法将其挂到exports上 */
    exports->Set(String::NewFromUtf8(isolate, "Accumulator"), _Accumulator);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}

To wrap C++ classes, you need to import header files <node_object_wrap.h>, and the wrapped classes need to inherit node::ObjectWrapclasses. The wrapped C++ class needs to have a JS constructor in addition to its own C++ constructor. The C++ constructor will be called when a C++ instance of the class is created, while the JS constructor is created in JavaScript. It is called when an object of a class is called. Write the operations to be performed when JS creates a C++ class instance in the JS constructor, and then wrap the JS constructor into one v8::Functionfor JavaScript to use. Taking the above code as an example, it AccumulatorJSis the JS constructor of the class. When AccumulatorJSexecuted, it will create a C++ class instance, and then wrap the instance into one v8::Objectand return it. AccumulatorJSIt is a C++ function. In order to call it in JS, you need to customize a JS function according to the Accumulator::initmethod utilization .FunctionTemplateAccumulatorJS

In JS, functions are used to implement classes, and the member methods of classes are generally defined on the prototype ( prototype) of the constructor. In the Accumulator::initmethod, use NODE_SET_PROTOTYPE_METHODto hook the member method of the C++ class to the prototype of the JS constructor. After doing this, after creating an instance of the class in JS, you can call these member methods.

The member methods of this class add()will getAddTimes()be called in JS, so they will also get const FunctionCallbackInfo<Value>& argsparameters when they are called. You can use argsparameters to obtain the parameter information passed by JS and set the return value. Since in these two member methods, member variables on the class instance need to be accessed, it is necessary to unpack the JS object that initiates the call and restore it to a C++ instance. It will be used when unpacking node::ObjectWrap::Unwrap<Accumulator>(args.Holder()), and args.Holder()you can get the instance of the JS object that initiated the call.

After implementing the packaging of class and object instances, the related interfaces can be exposed to JS for use. Like other C++ plug-in modules, it also module.exportsexposes related interfaces through properties. The points to pay attention to here are also explained in the code comments. Since what we expose to JS is a JS constructor, not an ordinary C++ function, we cannot use NODE_SET_METHODit to set the exposed content, but directly through the method of exportsthe object. Set()Make settings.

After completion, write the following content binding.gyp, compile and build, and .nodeafter obtaining the file, you can use the JS code shown above to test. The output result after running the JS code is shown in the figure. It can be verified that through the packaging of class and object instances, JS can directly use C++ classes and instances.

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

C++ class and object wrapper examples

2. C++ class instance factory

The previous shows how to wrap a C++ class and expose it to JavaScript. During use, JavaScript can newcreate instances of this class directly through operators. Now, we hope that the C++ module can provide a factory function, and JavaScript can also obtain an instance of the C++ class after calling the factory function, without the need newto create an instance through an operator. Continue Accumulatorto improve on the previous example to explore ways to implement C++ to provide class instance factory functions to JavaScript.

To Accumulatoradd a getInstance()method, through the method, you can actively call the JS constructor of the class Accumulator::getInstance()in the C++ code , thereby creating a class instance and wrapping the class instance into a JS object to achieve the same purpose as when using operators in JS. The following is the code display, and some operations are explained in the comments.Accumulatornew

#include <node.h>
#include <node_object_wrap.h>

namespace CppObjectWrapDemo {
  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;
  using v8::Exception;

  /* 将C++类封装给JS使用,需要继承node::ObjectWrap */
  class Accumulator : public node::ObjectWrap {
    public:
      /* 初始化该类的JS构造函数,并返回JS构造函数 */
      static Local<Function> init (Isolate* isolate) {
        /* 利用函数模板,将一个C++函数包装成JS函数 */
        Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, AccumulatorJS);
        tpl->SetClassName(String::NewFromUtf8(isolate, "Accumulator"));
        tpl->InstanceTemplate()->SetInternalFieldCount(1);
        /* 类方法定义在构造函数的prototype上 */
        NODE_SET_PROTOTYPE_METHOD(tpl, "add", add);
        NODE_SET_PROTOTYPE_METHOD(tpl, "getAddTimes", getAddTimes);
        /* 获取Accumulator类的JS构造函数 */
        Local<Function> fn = tpl->GetFunction();
        /* JS构造函数句柄存储于constructor上,后续还会使用到 */
        constructor.Reset(isolate, fn);
        return fn;
      }

      /* 获取该类实例的工厂函数 */
      static void getInstance (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        Local<Value> argv[1] = { args[0] };
        /* 获取当前上下文 */
        Local<Context> context = isolate->GetCurrentContext();
        /* 生成JS构造函数 */
        Local<Function> _constructor = Local<Function>::New(isolate, constructor);
        /* 创建实例 */
        Local<Object> obj = _constructor->NewInstance(context, 1, argv).ToLocalChecked();
        /* 返回实例 */
        args.GetReturnValue().Set(obj);
      }
    private:
      /* 成员变量 */
      static Persistent<Function> constructor;
      double value;
      int addTimes;

      /* 该类的C++构造函数,设置成员变量初始值 */
      explicit Accumulator (double initValue = 0) {
        this->value = initValue;
        this->addTimes = 0;
      }

      /* 该类的JS构造函数,创建该类的对象,并包装成JS对象然后进行返回 */
      static void AccumulatorJS (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        if (args.IsConstructCall()) {/* 通过 new Accumulator() 创建对象 */
          /* 提取参数数值 */
          double val = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
          /* 创建该类的实例对象 */
          Accumulator* obj = new Accumulator(val);
          /* 包装该对象 */
          obj->Wrap(args.This());
          /* 返回该对象 */
          args.GetReturnValue().Set(args.This());
        } else {/* 通过直接调用函数 Accumulator() 创建对象,抛出异常 */
          isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "Should use the new operator to create an instance.")
          ));
        }
      }

      /* 该类的成员方法,增加value的值 */
      static void add (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        /* 将被包装的JS对象还原为C++对象 */
        Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
        /* 访问C++对象上的成员变量进行操作 */
        obj->value += args[0]->NumberValue();
        obj->addTimes += 1;
        args.GetReturnValue().Set(Number::New(isolate, obj->value));
      }

      /* 该类的成员方法,获取累加次数 */
      static void getAddTimes (const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
        /* 将被包装的JS对象还原为C++对象 */
        Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
        args.GetReturnValue().Set(Number::New(isolate, obj->addTimes));
      }
  };

  Persistent<Function> Accumulator::constructor;

  void getAccumulatorInstance(const FunctionCallbackInfo<Value>& args) {
    Accumulator::getInstance(args);
  }

  void init (Local<Object> exports) {
    Isolate* isolate = exports->GetIsolate();
    /* 初始化Accumulator类的JS构造函数 */
    Local<Function> _Accumulator = Accumulator::init(isolate);
    /* 将Accumulator类的JS构造函数暴露给JS使用 */
    /* 这里不能使用NODE_SET_METHOD,因为NODE_SET_METHOD是暴露一个C++函数给JS使用 */
    /* NODE_SET_METHOD(exports, "Accumulator", _Accumulator); */
    /* 此处是暴露一个JS函数,它在C++里面表示为一个Function对象,不是一个C++函数 */
    /* 要通过设置属性的方法将其挂到exports上 */
    exports->Set(String::NewFromUtf8(isolate, "Accumulator"), _Accumulator);
    /* 将获取实例的工厂方法暴露给JS */
    NODE_SET_METHOD(exports, "getAccumulatorInstance", getAccumulatorInstance);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}

Accumulator::getInstance()In the method, _constructorthe used NewInstance()method can create an instance. The function of executing this method is equivalent to the function of using newthe operator in JS, and it will also call the corresponding JS constructor, thereby creating an instance of the class and wrapping it into a JS object. return.

Accumulator::getInstance()The method is called in C++. In order to expose it to JavaScript, a getAccumulatorInstance()function needs to be created at the end, and the call to the pair is completed in this function Accumulator::getInstance(). After it is getAccumulatorInstance()attached to the exportsproperty, it can be provided to JavaScript for use.

You can use the following JS code to check the C++ module, and the code output is shown in the figure. JavaScript can call the factory function provided by the C++ module to obtain an instance of a C++ class and use the instance to call member methods defined by the class.

// cpp-object-wrap-factory demo
const AccumulatorModule = require('./build/Release/Accumulator')
let acc2 = AccumulatorModule.getAccumulatorInstance(3)
console.log('[ObjectWrapFactoryDemo] 3 + 16 = ' + acc2.add(16))
console.log('[ObjectWrapFactoryDemo] 3 + 16 + 7 = ' + acc2.add(7))
console.log('[ObjectWrapFactoryDemo] 3 + 16 + 7 + 4 = ' + acc2.add(4))
console.log('[ObjectWrapFactoryDemo] add times: ' + acc2.getAddTimes())

C++ class instance factory instance

3. Asynchronous operation

NodeJS C++ plugins can provide interfaces for JavaScript to use, and many examples have been given in the previous blog [NodeJS C++ Addons Basics] . However, in the previous examples, all synchronous calls were implemented. According to the previous practice, JavaScript will block the execution of JavaScript code after calling the interface provided by the C++ plug-in module. The blocking caused by synchronous calls does not conform to the asynchronous characteristics of JavaScript. When encountering complex and time-consuming tasks, this blocking will seriously affect the performance of the application. In order to solve this problem, it is necessary to implement asynchronous calls, so that the interface of the C++ plug-in module will not block the execution of JavaScript after being called.

libuvThis library is required to implement asynchronous operations . It libuvis a cross-platform abstract library that implements Node.js event loops, worker threads, and all asynchronous operations on the platform. The use of libuvC++ plug-in modules makes it easy to make interfaces asynchronous. Next, we will improve the example of cumulative sum used in the previous blog [NodeJS C++ Addons Basics] to implement asynchronous calls. The following is the improved code, some operations will be explained in the comments.

#include <uv.h>
#include <node.h>
#include <vector>

namespace AsyncDemo {
  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;
  using v8::Undefined;
  using v8::Persistent;
  using v8::HandleScope;

  /* 存放数据供子线程使用的结构体 */
  struct Data {
    /* 回调函数 */
    Persistent<Function> callback;
    /* 求和参数 */
    std::vector<double> args;
    /* 求和结果 */
    double result;
  };

  /* 子线程执行的代码 */
  void calculate (uv_work_t* req) {
    Data* data = static_cast<Data*>(req->data);
    /* 遍历参数进行求和 */
    data->result = 0.0;
    for (int i = 0; i < data->args.size(); ++i) {
      data->result += data->args[i];
    }
  }

  /* 子线程结束后执行的代码 */
  void calculateComplete (uv_work_t* req) {
    Data* data = static_cast<Data*>(req->data);
    Isolate* isolate = Isolate::GetCurrent();
    /* 必须创建一个HandleScope,否则后面无法创建句柄 */
    HandleScope handleScope(isolate);
    /* 将求和结果转换为一个JS Number */
    Local<Value> argv[1] = { Number::New(isolate, data->result) };
    /* 通过回调函数返回求和结果 */
    Local<Function> cb = Local<Function>::New(isolate, data->callback);
    cb->Call(Null(isolate), 1, argv);
    /* 回调完成后清除资源 */
    data->callback.Reset();
    delete data;
    delete req;
  }

  void accumulateAsync (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]);
    /* 创建Data结构体存储数据 */
    Data* data = new Data();
    /* 存储回调函数 */
    data->callback.Reset(isolate, callback);
    /* 提取参数并存储到data */
    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 {
        data->args.push_back(args[i]->NumberValue());
      }
    }

    /* 启动工作线程进行求和计算 */
    uv_work_t *req = new uv_work_t();
    req->data = data;
    uv_queue_work(
      uv_default_loop(),
      req,
      (uv_work_cb)calculate,
      (uv_after_work_cb)calculateComplete
    );

    /* 本函数直接返回,无需等待线程计算完成 */
    args.GetReturnValue().Set(Undefined(isolate));
  }

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

  NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}

To use libuvthis library, header files need to be included <uv.h>. Use uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb)this method to send the task to the thread pool, specify the execution function work_cband callback function after_work_cbof the child thread, the child thread will execute work_cb, and call back after the thread is executed after_work_cb. uv_work_t* reqIt is used to store the request information of the initiating thread call. In the execution function work_cband the callback function , the data that the main thread wants to be processed by the child thread after_work_cbcan be obtained. reqIn memory, threads do not share the stack, but share the heap. Therefore, the main thread can wrap data through a structure and then create an instance of the structure on the heap. The child thread can obtain the structure instance from the heap through the pointer. Get the data that the main thread wants to pass, and you can also return the data to the main thread through this instance.

Taking the above code as an example, in the accumulateAsync()method, after completing the parameter check and extraction, you can start a sub-thread to perform operation and calculation on the data, without waiting for the end of the sub-thread to return directly. After JavaScript calls accumulateAsync()the method, it actually starts a child thread to complete the summation process, and accumulateAsync()returns immediately after the method is called, so it will not block the execution of subsequent JavaScript code. The execution function of the child thread calculatewill complete the summation calculation process, and store the summation result Datain an instance of the structure. After the child thread is executed, it will wake up the main thread, run the callback function calculateComplete, calculateCompleteand complete the call to the JavaScript callback function in it. Return the summation result to JavaScript through the callback function.

In the structure Data, the handle needs to be used when storing the handle of the JS callback function Persistent, and the Persistenthandle needs to be released by actively executing the code. The corresponding Localhandle will be released when the handle scope disappears after the function call ends, and the variable that loses the handle reference will be released by V8. The garbage cleaning mechanism automatically removes it. Since the method of the main thread accumulateAsync()has already finished and returned when the child thread is executed, in order to prevent the callback function from cleaning up, and to successfully complete the callback JavaScript after the child thread ends, a Persistenthandle must be used here.

Write the following content to the file binding.gyp, and complete the compilation and build to get the .nodefile.

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

Use the following JS code to test, the output result is as shown in the figure, you can see that the first output statement is Hi~that because the asynchronous call is implemented, the summation process will not block the execution of the JavaScript code accumulateAsync(). console.log('[AsyncDemo] Hi~')will be blocked and execute normally.

// async-demo
const AccumulateAsync = require('./build/Release/accumulate_async')
AccumulateAsync.accumulateAsync(1, 3, 4, 7, (sum) => {
  console.log('[AsyncDemo] 1 + 3 + 4 + 7 = ' + sum)
})
console.log('[AsyncDemo] Hi~')

Asynchronous call instance

4. Summary

Implement the packaging of C++ classes and instances, so that JavaScript can directly use C++ classes and instances. Although the operation of this process is a bit cumbersome, it is not too difficult. <node_object_wrap.h>The API provided in this function can already be easily implemented. Implementing the asynchrony of the C++ plug-in interface so that JavaScript can make asynchronous calls without being blocked requires knowledge of multithreading. There is no relevant example on how to use asynchronous operation in the official NodeJS documentation libuv, so it is quite difficult to write the previous example. libuvThe API documentation is rather obscure. Fortunately, there is another introduction document [An Introduction to libuv] on the Internet . In addition to introducing some concepts and APIs, this document will also give code examples. For beginners more friendly.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324650848&siteId=291194637