はじめに
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.js 日本ユーザグループ
- EventEmitter constructor should not be in C++ · Issue #1335 · joyent/node · GitHub
それ以前は、
コード
- 参考: Google Groups
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
クラスの書き方などは通常のアドオン作成方法と大差ありません。
次に 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 はシングルスレッドベースのため、普通にやってしまうと重い処理が終了するまで別の処理ができなくなってしまいます。これを実現するためにはマルチスレッドの処理が必要になりますので、次回はこれについて説明したいと思います。