C/C++ オブジェクトを Javascript オブジェクトにエクスポートする
V8 の Embedder's Guide を見ると、C/C++ の関数を Javascript から使えるようにするやり方が解説されています。
Accessor のセクションにある Point の例をちょっと拡張すれば C/C++ のオブジェクトを Javascript のオブジェクトとして見せることができるようになりそうですが、オブジェクトのインスタンスを C++ コードとして生成していたりするのでちょっと gap があります。
ここでは、Javascript のオブジェクトに対して C/C++ のオブジェクト(のインスタンス)が対応して、メモリの解放も GC にしたがってよしなにされる、というところを目標にします。
Internal Field の利用
基本的には Embedder's Guide のとおりです:
Javascript のクラス定義というのは、コンストラクタ関数を基軸として行うので、V8 ではコンストラクタをコールバックとした v8::FunctionTemplate オブジェクトに対応します。
FunctionTemplate は InstanceTemplate, PrototypeTemplate という 2 つの v8::ObjectTemplate オブジェクトを持ち、それぞれが instantiate されて this, this.prototype になります。
したがって、クラスメソッドの定義は PrototypeTemplate に行い、クラスメンバ(へのアクセサ)の定義は InstanceTemplate に行うことになります。
V8 のオブジェクトは、internal field という、Javascript 側からは見えない値を持つことができるので、ここに C/C++ 側のオブジェクトへのポインタを保持しておけばよいとされています。
Internal field を使うには、対応する ObjectTemplate にあらかじめいくつ internal field を用意するか知らせておく必要があります。このために v8::ObjectTemplate::SetInternalFieldCount() 関数が用いられます。
今回の場合、Javascript オブジェクトに対して internal field を設定するので、FunctionTemplate から見ると InstanceTemplate 側に設定してやればよいことになります。
// hoge というクラスのテンプレートをつくる v8::Local<v8::FunctionTemplate> hogeClass = v8::FunctionTemplate::New(hoge_Constructor); // コンストラクタが必要 hogeClass->SetClassName(v8::String::New("hoge")); // 表示名 v8::Local<v8::ObjectTemplate> instTemplate = hogeClass->InstanceTemplate(); instTemplate->SetInternalFieldCount(1); // ... フィールドアクセサの定義 ... v8::Local<v8::ObjectTemplate> protoTemplate = hogeClass->PrototypeTemplate(); // ... クラスメソッドの定義 ... globalTemplate->Set(v8::String::New("hoge"), hogeClass); // クラスをグローバル環境に登録
オブジェクトができてきたところで(コンストラクタ内部)、今度は実際に internal field を設定するわけですが、internal field にポインタを渡すためには v8::External というクラスを使います。このクラスは C/C++ の void* をカプセル化してくれるものです。
static v8::Local<v8::Value> hoge_Constructor(const v8::Arguments& args) { hoge* backend = ...; // なんとかしてオブジェクトをつくる v8::Local<v8::Object> thisObject = args.This(); thisObject->SetInternalField(0, v8::External::New(backend)); // 設定 return this; // コンストラクタは this を返すこと }
クラスメソッドやフィールドアクセサの中では、internal field から殻をはいでいくことでオブジェクトのポインタを得ることができます。
クラスメソッドの場合、this にアクセスするには v8::Arguments::This() を使います。一方フィールドアクセサの場合、v8::AccessorInfo::Holder() を使います。
static v8::Local<v8::Value> hoge_SomeMethod(const v8::Arguments& args) { v8::Local<v8::Value> internalValue = args.This()->GetInternalField(0); // internal field 読み出し v8::Local<v8::External> backendEncapsulator = v8::Local<v8::External>::Cast(internalValue); // ダウンキャスト hoge* backend = static_cast<hoge*>(backendEncapsulator->Value()); return v8::Integer::New(hoge->SomeMethod(...)); // などなど }
this オブジェクトから C/C++ オブジェクトを取り出す部分は共通化できるので、ヘルパ関数を書いておくと便利です。
static hoge* hoge_GetThis(v8::Handle<v8::Object> object) { return static_cast<hoge*>( v8::Local<v8::External>::Cast(object->GetInternalField(0))->Value()); }
(ToDo: 図を描く)
GC と一緒にメモリを解放させるしかけ
Javascript にはガベージコレクタが搭載されているので、使用しなくなった変数は C/C++ のように自分で解放する必要はありません。参照がなくなれば、いずれ GC によりメモリは解放されます。
しかし、バックエンドにある C/C++ オブジェクトには解放するコードがありません。したがって、上のようなコードを書いただけでは解放されるわけもありません。
C/C++ オブジェクトのメモリ使用量が大きい場合などは明示的な解放をするのがよいかもしれませんが、そうでない場合は特段の処理をしなくても GC が発生したときに解放される、というのが Javascript 的に考えれば自然です。
これを行うには、persistent handle にある weak reference を用います。
Weak reference は GC root にはならないため、存在していても GC が発生すると回収されます。そのときにコールバックを呼ぶように設定できるので、このタイミングでバックエンドのオブジェクトを解放してやればよいことになります。
Weak reference を作るには、v8::Persistent
そうすると、persistent handle を作る必要があるのですが、これは this になるオブジェクトを参照したハンドルをひとつ作ればいいようです。V8 のソースコード、src/api.cc を見ると、external string resource を使うあたりでこれと同じことをやっています。
(ToDo: 図を描く)
static v8::Local<v8::Value> hoge_Constructor(const v8::Arguments& args) { ... v8::Persistent<v8::Object> objectHolder = v8::Persistent<v8::Object>::New(thisObject); objectHolder.MakeWeak(backend, hoge_Destructor); ... } static void hoge_Destructor(v8::Persistent<v8::Value> handle, void* parameter) { delete static_cast<hoge*>(parameter); // など handle.Dispose(); }
実例
下記のようなクラスをエクスポートしてみます。(これくらいなら JS で書いてもぜんぜんかまいませんが…)
class Counter { public: explicit Counter(int initialValue = 0) : m_Count(initialValue) { } int GetCount() const { return m_Count; } void SetCount(int count) { m_Count = count; } void Add(int diff = 1) { m_Count += diff; } private: int m_Count; };
インターフェイスは次のようにかけます。
class CounterJSIF { private: static Counter* GetThis(const v8::Local<v8::Object> handle) { void* pThis = v8::Local<v8::External>::Cast(handle->GetInternalField(0))->Value(); return static_cast<Counter*>(pThis); } ///// コンストラクタ・デストラクタ ///// static v8::Handle<v8::Value> New(const v8::Arguments& args) { Counter* backend = NULL; if (args.Length() > 0) { backend = new Counter(args[0]->Int32Value()); } else { backend = new Counter(); } v8::Local<v8::Object> thisObject = args.This(); thisObject->SetInternalField(0, v8::External::New(backend)); v8::Persistent<v8::Object> holder = v8::Persistent<v8::Object>::New(thisObject); holder.MakeWeak(backend, CounterJSIF::Dispose); return thisObject; } static void Dispose(v8::Persistent<v8::Value> handle, void* parameter) { Counter* backend = static_cast<Counter*>(parameter); delete backend; handle.Dispose(); } ///// フィールドアクセサ ///// static v8::Handle<v8::Value> GetCount(v8::Local<v8::String> propertyName, const v8::AccessorInfo& info) { const Counter* backend = GetThis(info.Holder()); return v8::Integer::New(backend->GetCount()); } static void SetCount(v8::Local<v8::String> propertyName, v8::Local<v8::Value> value, const v8::AccessorInfo& info) { Counter* backend = GetThis(info.Holder()); backend->SetCount(value->Int32Value()); } ///// メソッドコールバック ///// static v8::Handle<v8::Value> Add(const v8::Arguments& args) { Counter* backend = GetThis(args.This()); if (args.Length() > 0) { backend->Add(args[0]->Int32Value()); } else { backend->Add(); } return v8::Undefined(); } public: static void InitializeTemplate(v8::Handle<v8::ObjectTemplate> global) { v8::Local<v8::FunctionTemplate> clazz = v8::FunctionTemplate::New(CounterJSIF::New); clazz->SetClassName(v8::String::New("Counter")); v8::Local<v8::ObjectTemplate> instTemplate = clazz->InstanceTemplate(); instTemplate->SetInternalFieldCount(1); instTemplate->SetAccessor(v8::String::New("count"), CounterJSIF::GetCount, CounterJSIF::SetCount); v8::Local<v8::ObjectTemplate> protoTemplate = clazz->PrototypeTemplate(); protoTemplate->Set(v8::String::New("add"), v8::FunctionTemplate::New(CounterJSIF::Add)); global->Set(v8::String::New("Counter"), clazz); } };
ATL みたいなコードがかけそうだなあ、と思う。