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が必要です。
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc" ],
"include_dirs" : [
"<!(node -e \"require('nan')\")"
]
}
]
}
アドオンの実装は以下になります。
NAN_METHODマクロでメソッドの実装、
NAN_MODULE_INITでは初期化としてメソッドの登録、
NODE_MODULE()で初期化関数を指定します。
Methodメソッドで"world"という文字列を作って返却してます。
#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で呼び出すには、
var addon = require('./build/Release/addon');
var ret = addon.hello();
console.log(ret);
のようにします。
足し算を実装してみる(引数が必要なケースの実装)
足し算をするメソッド add(x, y) を実装します。
足し算なので、Number型の引数が2つ必要になります。
#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()という形で行ってます。
テストコード
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引数にコールバック関数を渡すようにして、そこに結果を返します。
#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)を渡して上げる必要があります。
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()で実行します。
サンプルは以下です。
#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を用意します。
#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
#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で設定した値を取得できます。
後は、
#include <nan.h>
#include "myobject.h"
namespace demo {
NAN_MODULE_INIT(init) {
MyObject::Init(target);
}
NODE_MODULE(addon, init)
}
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc" ],
"include_dirs" : [
"<!(node -e \"require('nan')\")"
]
}
]
}
と記述してビルドすれば、
var addon = require('./build/Release/addon');
var myobject = new addon.MyObject(10);
console.log(myobject.getValue()); //10を出力
でObjectが作れるのを確認できます。
イベントを登録・発火する
最後にイベントを登録する方法です。
ObjectWrapを継承したクラスにコールバックをもたせるようにして、
イベントを発火させるときはそのコールバックをイベント名と主にコールします。
下記はplusOne()というメソッドで値を+1したら、"result"イベントを発火してそこで結果を返すようにします。
#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
#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のプロトタイプを継承するようにします。
下記のようにします。
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ライフを!