Node.jsのモジュールをC++で作成する(その2)

関連投稿
Node.jsのモジュールをC++で作成する
Node.js + nobleを使ってMicrobitのセンサー情報を読み取る

前回では、関数をコールすると文字列が返ってくる単純なものだったので、もう少し実用的なサンプルを備忘録として残しておく。

基本は、以下を参照だが、正直多種多様なモジュールを作る気がないなら、引数付きの関数実行を覚えておけばよいと思う。
Node.js C++ Addons

あと、一番やりたかった非同期のコールバック関数の作り方は乗ってなかった(コールバック形式の作成方法はあったが、そのまま実行しても同期処理になる)ので、以下の素晴らしいサイトを参考にした。

libuvを用いた非同期処理を含むNode.js Native Addon入門

引数付きの同期処理を行う関数サンプル

作成すると、以下のような呼び出しができる

// add.js
const addon = require('./build/Release/addon');
var val = addon.add(3,5);
console.log(val);
var val2 = addon.add(1,0.033);
console.log(val2);
> node add.js
8
1.003

といっても前回のHello関数とほぼ一緒で、argsから代入された引数が取得できますよ。という違いだけになる。

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

namespace demo {
  using v8::Exception;
  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Object;
  using v8::String;
  using v8::Value;
  using v8::Number;
  
  void Add( const FunctionCallbackInfo<Value> & args ){
    Isolate * isolate = args.GetIsolate();
    
    // check argument
    if( args.Length() < 2 ){
      // argument error
      isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, "Wrong number of arguments")
      ));
      return;
    }

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

    // add operation
    double value = args[0]->NumberValue() + args[1]->NumberValue();
    
    // set return value
    Local<Number> num = Number::New(isolate, value);
    args.GetReturnValue().Set(num);
  }

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

binding.gypファイル

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cc" ],
      "conditions":[
        [
          'OS=="linux"',{
            "defines": [ "_GNU_SOURCE" ],
            "cflags": [ "-g", "-O2", "-std=c++11", ]
          }
        ],
        [
          'OS=="win"',{
            'libraries': [
              'dbghelp.lib',
              'Netapi32.lib'
            ],
            'dll_files': [
              'dbghelp.dll',
              'Netapi32.dll'
            ]
          }
        ],
        [
          'OS=="mac"',{
            'xcode_settings': {
              'CLANG_CXX_LIBRARY': 'libc++',
              'CLANG_CXX_LANGUAGE_STANDARD':'c++11',
            }
          }
        ]
      ]
    }
  ]
}

非同期のコールバック関数(ノンブロッキング?)

以下のようなjavascriptを記述して非同期処理できる関数が作成できる

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

console.log("-- start --");

addon.async( 3, 5, function(result){
  console.log("async = "+result);
});

console.log("-- end --");

endのあとに結果が表示されているため、ノンブロッキング処理になっていることがわかる。

> node async.js
-- start --
-- end --
async = 45

ポイントは、libuvというライブラリを使用するところで、linuxのイベントループ処理をおこなうliveventlibevをマルチプラットフォーム化したライブラリで、Windowsでもビルドできるモジュールらしい。

以下のサンプルでは、AsyncDataとクラスでスレッドデータをやり取りして、非同期で実行したい負荷の高い処理をComputeで処理する。ほかは、ほぼお約束?のようなコードだと思っている。
いずれの関数名も制約はないので、好きな名前でOK。

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

#include <iostream>
#include <string>
#include <uv.h>
#include <unistd.h>

namespace demo {

using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Number;
using v8::Value;
using v8::Exception;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Persistent;

// スレッド間でやり取りするデータ
class AsyncData {
  public:

  uv_work_t uv_request;
  Persistent<Function> callback;
  int number;
  int number2;
  std::string result;
};

// 非同期実行したい重たい処理
std::string Compute(int number, int number2) {

  int i;
  long y = 0;

  for(int i = number2; i > 0; i--){
    y += i * number;
  }
  return std::to_string(y);
}

// スレッド処理
static void AsyncWork(uv_work_t *req) {

  //Threadデータからデータを取り出して計算実行
  AsyncData *work = static_cast<AsyncData *>(req->data);
  std::string result = Compute(work->number, work->number2);
  // 結果を設定する
  work->result = result;

}

// スレッド完了時の処理
static void AsyncAfter(uv_work_t *req, int status) {

  Isolate *isolate = Isolate::GetCurrent();
  HandleScope scope(isolate);

  // スレッドデータから計算結果を取り出してコールバックの引数に詰める
  AsyncData *work = static_cast<AsyncData *>(req->data);
  Local<String> result = String::NewFromUtf8(isolate, work->result.c_str());
  const unsigned argc = 1;
  Local<Value> argv[argc] = {result};
  
  // コールバックの呼び出し
  Local<Function> cb = Local<Function>::New(isolate, work->callback);
  cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
  
  // 不要になったスレッドを削除
  work->callback.Reset();
  delete work;
}

// 非同期処理の開始
void Async(const FunctionCallbackInfo<Value> &args) {

  Isolate *isolate = Isolate::GetCurrent();
  HandleScope scope(isolate);

  // 引数取得
  int number = args[0]->NumberValue();
  int number2 = args[1]->NumberValue();
  Local<Function> cb_local = Local<Function>::Cast(args[2]);

  // スレッドデータ作成
  AsyncData *work = new AsyncData;
  work->uv_request.data = work;
  work->callback.Reset(isolate, cb_local);
  work->number = number;
  work->number2 = number2;

  // イベントループ作成
  uv_loop_t *loop = uv_default_loop();

  // スレッド開始
  uv_queue_work(
    loop,
    &work->uv_request,
    (uv_work_cb)AsyncWork,
    (uv_after_work_cb)AsyncAfter);
}

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

NODE_MODULE(NODE_GYP_MODULE_NAME, Init);
}