凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Node.js で C++ アドオンから EventEmitter のイベントリスナを呼ぶ

はじめに

EventEmitter は Socket.IO などでも採用されているように、次のようなコードでイベントリスナの登録/処理ができるモジュールです。

var EventEmitter = require('events').EventEmitter;
var ev = new EventEmitter();

ev.on('hoge', function(data) {
	console.log(data);
});

setTimeout(function() {
	ev.emit('hoge', 'piyopiyo');
}, 1000);

参考: Events Node.js v0.10.24 Manual & Documentation

このイベントリスナの呼び出しを C++ のネイティブモジュールから行う方法を本エントリでは紹介します。

概要

Node.js の 0.5.2 からは C++ での EventEmitter が削除されました。

それ以前は、 をインクルードして node::EventEmitter を継承したクラス内で、node::EventEmitter で定義されている Emit を呼び出すことで実現されていました。0.5.2 からは ObjectWrap を継承して JavaScript 側の EventEmitter を利用する形となりました。

コード

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

using namespace v8;
using namespace node;

static Persistent<String> emit_symbol;

class MyObject : public ObjectWrap {
public:
	// MyObject を Node.js の世界へ送り出す
	static void Init(v8::Handle<v8::Object>& target)
	{
		Local<FunctionTemplate> clazz = FunctionTemplate::New(MyObject::New);
		clazz->SetClassName( String::NewSymbol("MyObject") );
		clazz->InstanceTemplate()->SetInternalFieldCount(1);
		clazz->PrototypeTemplate()->Set(
			String::NewSymbol("hoge"),
			FunctionTemplate::New(MyObject::Hoge)->GetFunction()
		);
		target->Set( String::NewSymbol("MyObject"), clazz->GetFunction() );
	};

private:
	MyObject(const std::string& name)
	: ObjectWrap(), name_(name) {};

	~MyObject() {};

	// JavaScript の世界で new されたら呼ばれる
	static v8::Handle<v8::Value> New(const v8::Arguments& args)
	{
		HandleScope scope;

		v8::String::Utf8Value name(args[0]);
		MyObject* obj = new MyObject(*name);
		obj->Wrap( args.This() );

		return args.This();
	};

	// JavaScript の世界で MyObject.hoge したら呼ばれる
	static v8::Handle<v8::Value> Hoge(const v8::Arguments& args)
	{
		HandleScope scope;

		MyObject* obj = ObjectWrap::Unwrap<MyObject>( args.This() );
		obj->Emit("hoge");

		return scope.Close(Undefined());
	};

	// JavaScript の世界で定義したイベントリスナを呼ぶ
	void Emit(const std::string& msg)
	{
		HandleScope scope;

		Local<Value> emit_v = handle_->Get(emit_symbol);
		assert( emit_v->IsFunction() );
		Local<Function> emit = emit_v.As<Function>();

		const size_t argc = 2;
		Handle<Value> argv[argc] = {
			String::New( msg.c_str() ),
			String::New( name_.c_str() )
		};

		TryCatch tc;
		emit->Call(this->handle_, argc, argv);
		if ( tc.HasCaught() ) {
			FatalException(tc);
		}
	}

	// クラス毎に扱う変数とか
	const std::string name_;
};

void Init(Handle<Object> target) {
	emit_symbol = NODE_PSYMBOL("emit");
	MyObject::Init(target);
}

NODE_MODULE(addon, Init)
myobj.js
var EventEmitter = require('events').EventEmitter
  , addon = require('./build/Debug/addon');

addon.MyObject.prototype.__proto__ = EventEmitter.prototype;
module.exports = addon.MyObject;
test.js
var MyObject = require('./myobj.js');
var http = require('http');

var obj1 = new MyObject('object 1')
  , obj2 = new MyObject('object 2')
;

obj1.on('hoge', function(str) {
	console.log(str);
});

obj2.on('hoge', function(str) {
	console.log(str);
});

obj1.hoge();
obj2.hoge();
コンパイルと実行
$ node-gyp configure build
$ node test
object 1
object 2

解説

まずはシンボルを作ります。お作法のようなものです。

static Persistent<String> emit_symbol;
void Init(Handle<Object> target) {
	emit_symbol = NODE_PSYMBOL("emit");
	...
}

NODE_PSYMBOL は Persistent::New(String::NewSymbol(...)) へ展開するマクロです。
クラスの書き方などは通常のアドオン作成方法と大差ありません。

次に node::ObjectWrap を継承したクラスにします。

class MyObject : public ObjectWrap {
	MyObject(const std::string& name) : ObjectWrap() {};
	...
}

Emit 部分は以下のようにします。

HandleScope scope;

Local<Value> emit_v = handle_->Get(emit_symbol);
assert( emit_v->IsFunction() );
Local<Function> emit = emit_v.As<Function>();

const size_t argc = 2;
Handle<Value> argv[argc] = {
	String::New( msg.c_str() ),
	String::New( name_.c_str() )
};

TryCatch tc;
emit->Call(this->handle_, argc, argv);
if ( tc.HasCaught() ) {
	FatalException(tc);
}

ここで、handle_->Get() で JavaScript 側で定義された "emit"(emit_symbol の文字列)という関数を取得しています。handle_ って何?という感じだと思いますが、handle_ は New 内で行った obj->Wrap( args.This() ) 、つまり ObjectWrap::Wrap 内で初期化されており、new した際の this オブジェクトを保管しているものになります。つまり、C++ で handle_->Get() で emit を呼び出しているというのは、JavaScript 側で this.emit に相当するものを取得していることになります。

なので JavaScript 側で new した際のオブジェクトに "emit" というメソッドがないといけません。そこで myobj.js で、

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

として on やら emit やら EventEmitter の一連の関数を継承しています。
後はこの EventEmitter から継承した "emit" を冒頭のコードで ev.emit('hoge', 'piyopiyo') と行ったように C++ で呼んであげれば OK です。

emit->Call(this->handle_, argc, argv);

argv の第1引数が on の第1引数で指定するイベント名になります。ここでは Init 内にて "hoge" という名前で JavaScript 側から MyObject::Hoge が呼ばれるようにセットしてあり、MyObject::Hoge の中では obj->Emit("hoge") と hoge のイベントリスナを呼ぶようになっています。そして JavaScript 側(test.js)では、次のような形でこのイベントを受け取っています。

obj1.on('hoge', function(str) {
	console.log(str);
});

この str は MyObject のコンストラクタの引数をそのまま垂れ流すようになっているので、 obj1、obj2 の on でそれぞれ object 1 / object 2 と別々のメッセージが表示される、という寸法になっています。

おわりに

C++ で何か重い処理をして終わったら Emit する、という形にしたい!と自然な流れで思いつきますが、Node.js はシングルスレッドベースのため、普通にやってしまうと重い処理が終了するまで別の処理ができなくなってしまいます。これを実現するためにはマルチスレッドの処理が必要になりますので、次回はこれについて説明したいと思います。