LoginSignup
17
19

NAN(Native Abstractions for Node.js)でnode.jsのアドオン

Last updated at Posted at 2016-08-29

Node.jsのアドオン

Node.jsでは、C/C++で書いたネイティブのコードをアドオンとして組み込むことができます。C/C++で書かれた共有オブジェクトをnode.jsでロードすることで実現します。
下記がそのドキュメントです。
https://nodejs.org/dist/latest-v6.x/docs/api/addons.html

NAN

Nodeのアドオン(NativeのコードをNode.jsで利用するしくみ)はV8に依存しており、困ったことにバージョンアップごとにV8の変更に影響されてしまします。そのため後方互換性が保たれないので、場合によってはバージョンアップすると大きく変更しなければならいことがあります。
そこで、NANというモジュールをかませることによって、Nodeのアドオンがnode.jsのバージョンに依存しないようになり、node.jsのバージョンに依存しないコードが書けるようになります。

Node.jsのアドオンはこちらを利用して書くのが良いと思います。

NANでのアドオン実装

準備

$ npm install -g node-gyp
$ npm install --save nan

アドオンのビルドにはnode-gypが必要です。
またNANを使う場合はnpmでモジュールをダウンロードしてください。

よくある"world"という文字列を返す hello() というモジュールの実装

ビルドに必要なgypファイルです。
NANを使うにはinclude_dirsが必要です。

binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc" ],
       "include_dirs" : [
        "<!(node -e \"require('nan')\")"
      ]
    }
  ]
}

アドオンの実装は以下になります。
NAN_METHODマクロでメソッドの実装、
NAN_MODULE_INITでは初期化としてメソッドの登録、
NODE_MODULE()で初期化関数を指定します。

Methodメソッドで"world"という文字列を作って返却してます。

addon.cc
#include <nan.h>

namespace demo {
  NAN_METHOD(Method) {
    info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
  }

  NAN_MODULE_INIT(init) {
    Nan::SetMethod(target, "hello", Method);
  }

  NODE_MODULE(addon, init)
}

これをビルドするには

$ node-gyp configure build

とします。
上記により、buildフォルダのRelease以下にaddon.nodeができあがります。
JavaScriptで呼び出すには、

hello.js
var addon = require('./build/Release/addon');
var ret = addon.hello();
console.log(ret);

のようにします。

足し算を実装してみる(引数が必要なケースの実装)

足し算をするメソッド add(x, y) を実装します。
足し算なので、Number型の引数が2つ必要になります。

addon.cc
#include <nan.h>

namespace demo {
  NAN_METHOD(Add) {
    if (info.Length() < 2) {
      Nan::ThrowTypeError("Wrong number of arguments");
      return;
    }

    if (!info[0]->IsNumber() || !info[1]->IsNumber()) {
      Nan::ThrowTypeError("Wrong arguments");
      return;
    }
    
    double value = info[0]->NumberValue() + info[1]->NumberValue();
    info.GetReturnValue().Set(Nan::New(value));
  }

  NAN_MODULE_INIT(init) {
    Nan::SetMethod(target, "add", Add);
  }

  NODE_MODULE(addon, init)
}

引数の情報はinfoに配列として渡っており、引数はinfo[0], info[1]のような形で取り出せます。
Numberの場合 info[0]->NumberValue()の形でとれます。
引数がNumberじゃないと足し算できないため、型チェクをinfo[0]->IsNumber()という形で行ってます。

テストコード

hello.js
var addon = require('./build/Release/addon');
var ret = addon.add(1, 2);
console.log(ret);
try {
  addon.add(1);
} catch (e) {
  console.log(e);
}

try {
  addon.add(1, "hello");
} catch (e) {
  console.log(e);
}

を実行すると

3
[TypeError: Wrong number of arguments]
[TypeError: Wrong arguments]

となります。

コールバック関数を使ってみる

上記の足し算をコールバック関数で結果を返すようにしてみます。
第3引数にコールバック関数を渡すようにして、そこに結果を返します。

addon.cc
#include <nan.h>

namespace demo {
  NAN_METHOD(Call) {
    if (info.Length() < 3) {
      Nan::ThrowTypeError("Wrong number of arguments");
      return;
    }

    if (!info[0]->IsNumber() || !info[1]->IsNumber() || !info[2]->IsFunction()) {
      Nan::ThrowTypeError("Wrong arguments");
      return;
    }

    Nan::Callback *callback = new Nan::Callback(info[2].As<v8::Function>());
    double value = info[0]->NumberValue() + info[1]->NumberValue();
    const int argc = 1;
    v8::Local<v8::Value> argv[argc] = { Nan::New(value) };
    callback->Call(argc, argv);
  }

  NAN_MODULE_INIT(init) {
    Nan::SetMethod(target, "call", Call);
  }

  NODE_MODULE(addon, init)
}

Nan::Callbackで受け取ったCallbackからNan::Callbackオブジェクトをつくり、そをにCallメソッドで呼び出します。
コールバック関数には引数情報(argc,argv)を渡して上げる必要があります。

hello.js
var addon = require('./build/Release/addon');
addon.call(1, 2, function(value){
  console.log(value);
});

try {
  addon.call(1, 2);
} catch (e) {
  console.log(e);
}

try {
  addon.call(1, 2, "hello");
} catch (e) {
  console.log(e);
}
3
[TypeError: Wrong number of arguments]
[TypeError: Wrong arguments]

非同期に処理をする

上記は同期的にコールバックを読んでしまいましたが、
非同期にコールバックを呼び出すにはAsyncWorkerというしくみが必要です。

AsyncWorker

非同期処理にはNan::AsyncWorker を継承したクラスを定義し、それを実装します。
AsyncWorkerはExecute()で非同期処理の内容を実行し、結果をどうするかをHandleOKCallback()で実行します。

サンプルは以下です。

addon.cc
#include <nan.h>

namespace demo {
  class AddWorker : public Nan::AsyncWorker {
  public:
    AddWorker(Nan::Callback *callback, double value1, double value2) :
      Nan::AsyncWorker(callback), value1(value1), value2(value2), result(0) {}
    ~AddWorker() {}

    void Execute() {
      result = value1 + value2;
    }

    void HandleOKCallback() {
      Nan::HandleScope scope;
      const int argc = 1;
      v8::Local<v8::Value> argv[argc] = { Nan::New(result) };
      callback->Call(argc, argv);
    }
  private:
    double value1, value2, result;
  };

  NAN_METHOD(Call) {
    if (info.Length() < 3) {
      Nan::ThrowTypeError("Wrong number of arguments");
      return;
    }

    if (!info[0]->IsNumber() || !info[1]->IsNumber() || !info[2]->IsFunction()) {
      Nan::ThrowTypeError("Wrong arguments");
      return;
    }

    Nan::Callback *callback = new Nan::Callback(info[2].As<v8::Function>());
    Nan::AsyncQueueWorker(new AddWorker(callback, info[0]->NumberValue(), info[1]->NumberValue()));
  }

  NAN_MODULE_INIT(init) {
    Nan::SetMethod(target, "call", Call);
  }

  NODE_MODULE(addon, init)
}

上記はExecuteで足し算を行い、HandleOKCallbackで結果を渡すコールバックを呼び出しています。

ObjectWrap

C++ のクラスをJavaScriptからnewでインスタンス化するためにはオブジェクトをWrapして上げる必要があり、
これは、ObjectWrapで継承したクラスにより実現できます。

NANの場合は、Nan::ObjectWrapを継承させます。
ObjectWrapを継承したMyObjectを用意します。

myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <nan.h>

namespace demo {
  class MyObject: public Nan::ObjectWrap {
  public:
    static NAN_MODULE_INIT(Init);

  private:
    explicit MyObject(double value = 0);
    ~MyObject();

    static NAN_METHOD(New);
    static NAN_METHOD(GetValue);
    static Nan::Persistent<v8::Function> constructor;
    double value_;

  };
}
#endif
myobject.cc
#include "myobject.h"

namespace demo {
  Nan::Persistent<v8::Function> MyObject::constructor;

  NAN_MODULE_INIT(MyObject::Init) {

    //コンストラクターテンプレートを準備する
    v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
    tpl->SetClassName(Nan::New("MyObject").ToLocalChecked());
    tpl->InstanceTemplate()->SetInternalFieldCount(1);

    //プロトタイプ
    SetPrototypeMethod(tpl, "getValue", GetValue);

    constructor.Reset(Nan::GetFunction(tpl).ToLocalChecked());
    Nan::Set(target, Nan::New("MyObject").ToLocalChecked(),
      Nan::GetFunction(tpl).ToLocalChecked());
  }

  MyObject::MyObject(double value) : value_(value) {}
  MyObject::~MyObject() {}

  NAN_METHOD(MyObject::New) {
    if (info.IsConstructCall()) {
      double value = info[0]->IsUndefined() ? 0 : Nan::To<double>(info[0]).FromJust();
      MyObject* obj = new MyObject(value);
      obj->Wrap(info.This());
      info.GetReturnValue().Set(info.This());
    } else {
      const int argc = 1;
      v8::Local<v8::Value> argv[argc] = {info[0]};
      v8::Local<v8::Function> cons = Nan::New(constructor);
      info.GetReturnValue().Set(cons->NewInstance(argc, argv));
    }
  }

  NAN_METHOD(MyObject::GetValue) {
    MyObject* obj = Nan::ObjectWrap::Unwrap<MyObject>(info.Holder());
    info.GetReturnValue().Set(obj->value_);
  }
}

ちょっとコンストラクタを作るのにFunctionTemplateなるものを用意したり、おまじない的なコードが多いのですが、
GetValueでNewで設定した値を取得できます。

後は、

addon.cc
#include <nan.h>
#include "myobject.h"

namespace demo {
  NAN_MODULE_INIT(init) {
    MyObject::Init(target);
  }

  NODE_MODULE(addon, init)
}
binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc" ],
       "include_dirs" : [
        "<!(node -e \"require('nan')\")"
      ]
    }
  ]
}

と記述してビルドすれば、

hello.js
var addon = require('./build/Release/addon');
var myobject = new addon.MyObject(10);
console.log(myobject.getValue());  //10を出力

でObjectが作れるのを確認できます。

イベントを登録・発火する

最後にイベントを登録する方法です。
ObjectWrapを継承したクラスにコールバックをもたせるようにして、
イベントを発火させるときはそのコールバックをイベント名と主にコールします。
下記はplusOne()というメソッドで値を+1したら、"result"イベントを発火してそこで結果を返すようにします。

myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <nan.h>

namespace demo {
  class MyObject: public Nan::ObjectWrap {
  public:
    static NAN_MODULE_INIT(Init);

  private:
    explicit MyObject(double value = 0);
    ~MyObject();

    static NAN_METHOD(New);
    static NAN_METHOD(GetValue);
    static NAN_METHOD(PlusOne);
    static Nan::Persistent<v8::Function> constructor;
    double value_;
    Nan::Callback *emit = nullptr;
  };
}
#endif

myobject.cc
#include "myobject.h"

namespace demo {
  Nan::Persistent<v8::Function> MyObject::constructor;

  NAN_MODULE_INIT(MyObject::Init) {

    //コンストラクターテンプレートを準備する
    v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
    tpl->SetClassName(Nan::New("MyObject").ToLocalChecked());
    tpl->InstanceTemplate()->SetInternalFieldCount(1);

    //プロトタイプ
    SetPrototypeMethod(tpl, "getValue", GetValue);
    SetPrototypeMethod(tpl, "plusOne", PlusOne);

    constructor.Reset(Nan::GetFunction(tpl).ToLocalChecked());
    Nan::Set(target, Nan::New("MyObject").ToLocalChecked(),
      Nan::GetFunction(tpl).ToLocalChecked());
  }

  MyObject::MyObject(double value) : value_(value) {}
  MyObject::~MyObject() {
    if (emit != nullptr) {
      delete emit;
    }
  }

  NAN_METHOD(MyObject::New) {
    if (info.IsConstructCall()) {
      double value = info[0]->IsUndefined() ? 0 : Nan::To<double>(info[0]).FromJust();
      MyObject* obj = new MyObject(value);
      obj->Wrap(info.This());

      //イベントを登録できるようにする
      obj->emit = new Nan::Callback(
        v8::Local<v8::Function>::Cast(obj->handle()->Get(Nan::New("emit").ToLocalChecked()))
      );

      info.GetReturnValue().Set(info.This());
    } else {
      const int argc = 1;
      v8::Local<v8::Value> argv[argc] = {info[0]};
      v8::Local<v8::Function> cons = Nan::New(constructor);
      info.GetReturnValue().Set(cons->NewInstance(argc, argv));
    }
  }

  NAN_METHOD(MyObject::GetValue) {
    MyObject* obj = Nan::ObjectWrap::Unwrap<MyObject>(info.Holder());
    info.GetReturnValue().Set(obj->value_);
  }

  NAN_METHOD(MyObject::PlusOne) {
    MyObject* obj = Nan::ObjectWrap::Unwrap<MyObject>(info.Holder());
    obj->value_ += 1;

    //resultイベントを発火して結果を渡す
    v8::Local<v8::Value> argv[] = { Nan::New("result").ToLocalChecked(), Nan::New(obj->value_) };
    obj->emit->Call(obj->handle(), 2, argv);
  }
}

あとは、onでイベントが登録できるようにEventEmitterのプロトタイプを継承するようにします。
下記のようにします。

hello.js
var EventEmitter = require('events').EventEmitter;
var addon = require('./build/Release/addon');

addon.MyObject.prototype.__proto__ = EventEmitter.prototype;

var myobject = new addon.MyObject(10);
myobject.on('result', function(ret) {
  console.log(ret);
});
myobject.plusOne();  //11を出力
myobject.plusOne();  //12を出力

以上です。
NANをつかって楽しいNode.jsのaddonライフを!

17
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
19