node.js 面白いですよね!ェ
node.js ってアプリケーションを作る側(つまりライブラリを使う側)からすると、Web周りの便利なライブラリが既に色々あって、そのライブラリが一体どうやって動いてるのか気にすることってあんまり無いかと思います。
pure javascriptな物ならばコードを読むのは簡単です。ただしやれる事に限りがあります。node.js はGoogle製のJavaScript Engineであるv8をベースに作られているのですが、このv8はアプリケーションに組み込むのに適した構成になっていて、関数テンプレートやインスタンス、プロトタイプという各機能がC++のクラステンプレートで上手く表現出来ているライブラリです。Spidermonkeyも確かに扱うのは簡単なのですが、僕からすると若干C言語臭いというか、JavaScriptとC++との繋ぎが泥臭い感じがしています。
実際、Spidermonkeyを使ったembedがどの様になるかは先日phantomjsのGTK+版であるspecterjsを書いた時のコードを見てもらえると分かるかと思いますが、C言語からJavaScriptの型変換がいかにもフィルタ関数ぽく見えてしまいます。
今日はその辺りを上手く見せているv8で、どうやってembedを行うかを紹介して見ようと思います。
以下の手順にそっていけば、きっと貴方もv8で自作の埋め込みオブジェクトを実装出来る様になるでしょう。
まずv8をインストールしましょう。
v8 - V8 JavaScript Engine - Google Project Hostingsubversionで以下の様にコードを取得します。
V8 JavaScript EngineV8 is Google's open source JavaScript engine.
http://code.google.com/p/v8/
# svn checkout http://v8.googlecode.com/svn/trunk/ v8
ビルドにはSConsが必要です。ubuntuならばaptで、windowsならばオフィシャルからインストーラをダウンロードしてインストールするのが無難かと思います。SCons: A software construction tool参考にするのにまず最初に
What makes SCons better? Configuration files are Python scripts--use the power of a real programming...
http://www.scons.org/
samples/shell.cc
を見るのをお勧めします。JavaScriptの対話コンソールプログラムなのですが、グローバルやコンテキスト、関数wrap、スクリプト実行のやり方が分かるかと思います。
コンパイルは
# scons sample=shell
を実行します。ここにどうやって自分のオブジェクトを埋め込んでいくかが今回の本筋です。
まず、適当にクラスを作ります。これはv8には依存させません。
class FooImpl {
private:
std::string value;
public:
FooImpl() {}
~FooImpl() {}
void SetValue(const std::string v) {
this->value = v;
}
const std::string GetValue() const {
return this->value;
}
void ConcatValue(const std::string v) {
this->value += v;
}
};
簡単な文字列を内包するクラスです。GetValue()/SetValue()
というアクセサ、ConcatValue()
というメソッドを持っています。これをv8からFooというクラスで表現します。ベースは先ほどのshellに埋め込みます。
まずクラステンプレートを作ります。JavaScriptなのでクラスとは言えど関数テンプレートでしかありません。
// Make class template
v8::Local<v8::FunctionTemplate> clazz = v8::FunctionTemplate::New(Foo_Construct);
clazz->SetClassName(v8::String::New("Foo"));
ここでNew
に渡しているFoo_Construct
が、v8上で
var f = new Foo();
と実行した際に呼ばれるコールバック関数です(関数名は分かりやすくしているだけなのでより良い関数名を付けた方がいいでしょう)。こちらは後で説明します。次にインスタンステンプレートです。インスタンス化された際の形状を定義します。
// Make instance template
v8::Local<v8::ObjectTemplate> instanceTmpl = clazz->InstanceTemplate();
instanceTmpl->SetInternalFieldCount(1); // Store internal
instanceTmpl->SetAccessor(v8::String::New("value"), Foo_getValue, Foo_setValue); // Accessor
SetInternalFieldCount
は何を意味しているかというと、このクラスに内部フィールドを幾つ持たせられるかを教えています。そうです。FooImplのインスタンスですね。SetAccessorはアクセサ名とゲッタ・セッタ(
Foo_getValue()/Foo_setValue()
)を渡してあげます。こちらも先ほどのFoo_Construct
と同様にコールバック関数になっています。これについても後で説明します。そしてプロトタイプテンプレートを作ります。
// Make method
v8::Local<v8::ObjectTemplate> prototypeTmpl = clazz->PrototypeTemplate();
prototypeTmpl->Set("concat", v8::FunctionTemplate::New(Foo_concat));
global->Set(v8::String::New("Foo"), clazz);
インスタンスとプロトタイプがC++上でも綺麗に色分けされていますね。ここではconcat
というメソッドを実装しています。これまでと同様にFoo_concat
というコールバック関数を指定します。最後に
global
に対してFooという名前で関数テンプレートを登録します。v8との繋ぎ目は以上で完了です。
さて、個々のコールバックを実装して行きましょう。まずコンストラクタ。
static v8::Handle<v8::Value> Foo_Construct(const v8::Arguments& args) {
FooImpl* foo = new FooImpl();
printf("construct Foo: %p\n", foo);
v8::Local<v8::Object> thisObject = args.This();
thisObject->SetInternalField(0, v8::External::New(foo));
v8::Persistent<v8::Object> holder = v8::Persistent<v8::Object>::New(thisObject);
holder.MakeWeak(foo, Foo_Dispose);
return thisObject;
}
FooImplのインスタンスをSetInternalField
で設定しています。このthisをargsから取る辺りも、JavaScriptに似せてあって良いですね。ちなみに
Arguments::Callee()
も存在します。さて、ここで
v8::Persistent::MakeWeak()
を呼び出していますね。これはこのインスタンスを保持する予定のホールダーを意味していて、そのホールダーに対してインスタンスが弱参照である事を教えています。またその際、デストラクタをコールバックとして指定しています。ではデストラクタを実装しましょう。
static void Foo_Dispose(v8::Persistent<v8::Value> handle, void* parameter) {
FooImpl* foo = static_cast<FooImpl*>(parameter);
printf("destruct Foo: %p\n", foo);
delete foo;
handle.Dispose();
}
MakeWeak呼び出しの第一引数がparameterで渡ってきますので、FooImplのインスタンスを得ます。ここでは単にdeleteしていますが、特別な後処理があるならばここで実装します。同様の手順でアクセサコールバックを実装します。
static v8::Handle<v8::Value> Foo_getValue(v8::Local<v8::String> propertyName, const v8::AccessorInfo& info) {
FooImpl* foo = static_cast<FooImpl*>(v8::Local<v8::External>::Cast(info.Holder()->GetInternalField(0))->Value());
return v8::String::New(foo->GetValue().c_str());
}
static void Foo_setValue(v8::Local<v8::String> propertyName, v8::Local<v8::Value> value, const v8::AccessorInfo& info) {
FooImpl* foo = static_cast<FooImpl*>(v8::Local<v8::External>::Cast(info.Holder()->GetInternalField(0))->Value());
v8::String::Utf8Value utf8str(value);
foo->SetValue(*utf8str);
}
GetInternalField
からコンストラクタで登録していたFooImplのインスタンスを取得します。中身は普通のC++の処理です。ここまで来るとconcatメソッドも同じですね。
static v8::Handle<v8::Value> Foo_concat(const v8::Arguments& args) {
FooImpl* foo = static_cast<FooImpl*>(v8::Local<v8::External>::Cast(args.This()->GetInternalField(0))->Value());
v8::String::Utf8Value utf8str(args[0]);
foo->ConcatValue(*utf8str);
return v8::Undefined();
}
第一引数をStringとしてFooImpl::concatを呼び出しています。以上です。ただこのshellはコンソールから文字列を読み取って実行するループが行われていますのでv8からすると常にbussy状態になっています。共有ライブラリとしてビルド(USING_V8_SHAREDを指定してビルド)していない限りはメインスレッド上で実行されてしまいます。つまりオブジェクトの生死管理が実行されません。なおUSING_V8_SHAREDを指定してビルドされていれば
# ./shell --isolate --shell
として実行すれば別スレッドとして実行されます。以前は
v8::internal::Heap::CollectAllGarbage();
という関数で強制GCを実施出来ましたが、最近ヘッダファイルから無くなりました。これはおそらく、スレッドが入り乱れても動作しなければならないv8上ではGCのタイミングはユーザが決めるべきではないとの判断だと思っています。その代わりにv8の開発ML上では以下のコードがFAQの様に良く出てきます。
while (!v8::V8::IdleNotification());
v8::V8::IdleNotification
の関数コメントには以下の様に書いてあります。
/**
* Optional notification that the embedder is idle.
* V8 uses the notification to reduce memory footprint.
* This call can be used repeatedly if the embedder remains idle.
* Returns true if the embedder should stop calling IdleNotification
* until real work has been done. This indicates that V8 has done
* as much cleanup as it will be able to do.
*/
static bool IdleNotification();
訳すと
組み込み側がアイドル状態であることを意味するオプションの通知。なのでGCの動作確認を行うのであればshellのRunShellに以下の行を入れる事になります。
V8 はメモリの証跡を減らす為に通知を使用します。
これは、組み込み側がアイドル状態である間は何度でも呼び出す事が出来ます。
実際の処理が完了するまでの間、組み込み側がIdleNotification
を呼び出すべきでない場合には true を返します。
これは V8 が可能な限り多くのクリーンアップを、この時点で行っていた事を意味します。
// The read-eval-execute loop of the shell.
void RunShell(v8::Handle<v8::Context> context) {
printf("V8 version %s\n", v8::V8::GetVersion());
static const int kBufferSize = 256;
// Enter the execution environment before evaluating any code.
v8::Context::Scope context_scope(context);
while (true) {
char buffer[kBufferSize];
printf("> ");
char* str = fgets(buffer, kBufferSize, stdin);
if (str == NULL) break;
v8::HandleScope handle_scope;
ExecuteString(v8::String::New(str),
v8::String::New("(shell)"),
true,
true);
// Notify idle
while (!v8::V8::IdleNotification());
}
printf("\n");
}
さぁ実行しましょう。
# ./shell
V8 version 3.2.4.1
> var a = new Foo
construct Foo: 0x9526c98
> a.value
> a.value = "foo"
foo
> a.concat("bar")
> a.value
foobar
> a = null
null
destruct Foo: 0x9526c98
>
ちゃんと動いてますね。デストラクタもきちんと走ってます。ここまでのコードを以下のGistに貼り付けておきます。
mattn's gist: 885688 — Gist以上が v8 を使った基本的な embed となります。どうでしょうか?貴方も作れそうな気がしませんか?
shell.cc
https://gist.github.com/885688
ぜひチャレンジしてみて下さい。なお、だいぶ昔ですが v8 から twitter や wassr を操作出来る shell の改良物を作った事があったので参考URLとして記しておきます。
/lang/cplusplus/twitter-v8 – CodeRepos::Share – Trac面白い物が出来たらぜひ公開してみて下さい。
twitter-v8
http://coderepos.org/share/browser/lang/cplusplus/twitter-v8