2011/03/25

Recent entries from same category

  1. RapidJSON や simdjson よりも速いC言語から使えるJSONライブラリ「yyjson」
  2. コメントも扱える高機能な C++ 向け JSON パーサ「jsoncpp」
  3. C++ で flask ライクなウェブサーバ「clask」書いた。
  4. C++ 用 SQLite3 ORM 「sqlite_orm」が便利。
  5. zsh で PATH に相対パスを含んだ場合にコマンドが補完できないのは意図的かどうか。

V8 JavaScript Engine 最近どのブログでも node.js ばかりでもう飽き飽きしてる皆さんこんばんわ。
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 Hosting

V8 JavaScript EngineV8 is Google's open source JavaScript engine.

http://code.google.com/p/v8/
subversionで以下の様にコードを取得します。 # 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();
訳すと
組み込み側がアイドル状態であることを意味するオプションの通知。
V8 はメモリの証跡を減らす為に通知を使用します。
これは、組み込み側がアイドル状態である間は何度でも呼び出す事が出来ます。
実際の処理が完了するまでの間、組み込み側が IdleNotification を呼び出すべきでない場合には true を返します。
これは V8 が可能な限り多くのクリーンアップを、この時点で行っていた事を意味します。
なのでGCの動作確認を行うのであればshellのRunShellに以下の行を入れる事になります。 // 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 == NULLbreak;
    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

shell.cc

https://gist.github.com/885688
以上が v8 を使った基本的な embed となります。どうでしょうか?貴方も作れそうな気がしませんか?

ぜひチャレンジしてみて下さい。なお、だいぶ昔ですが v8 から twitter や wassr を操作出来る shell の改良物を作った事があったので参考URLとして記しておきます。
/lang/cplusplus/twitter-v8 – CodeRepos::Share – Trac

twitter-v8

http://coderepos.org/share/browser/lang/cplusplus/twitter-v8
面白い物が出来たらぜひ公開してみて下さい。
Posted at by