Hatena::Groupjavascript

JavaScriptで遊ぶよ

 | 

2010-04-07

Embedder's Guide - V8 JavaScript Engine

16:44

自分用に訳して自分用のブログに書いてたやつだけど、需要がありそうなので公開する。

C/C++ で書かれたプログラムの V8 バインディングを書きたい人向けの文書で、Getting Started の次に読むものという位置付けのもの。V8 の内部の仕組みをもっと知りたい人は Design Elements へ進む感じになっている。

日本語ではこのあたりが詳しい。

世間では V8 ブームは1年〜1年半前に来てたんですね。僕のブームは今なんですが。


以下、一部意訳。各セクションはほぼ独立しているので、どこから読んでも OK。


Audience

この文書は、C++ アプリケーションに JavaScript エンジンを埋め込みたい C++ プログラマー向けである。C++ のオブジェクトメソッドJavaScript から使えるようにしたり、JavaScriptオブジェクトや関数を C++ から使えるようにしたりする助けになるだろう。

Handles and Garbage Collection

handle は、JavaScript オブジェクトのヒープ内での位置への参照を提供するものだ。V8 の GC は二度と使われることのないメモリを回収する。GC は、そのプロセス中に、オブジェクトをヒープ内の別の位置に移動する。GC がオブジェクトを移動するとき、GC はそのオブジェクトを参照している handle を新しい位置にアップデートする。

あるオブジェクトは、それが JavaScript からアクセスできなくなり、どの handle もそのオブジェクトを参照していないとき、ゴミとされる。時々 GC はゴミと考えられるオブジェクトを全て取り除く。V8 の GC メカニズムは V8 のパフォーマンスのキーとなっている。もっと知りたければ Design Elements を読むといい。

handle には2種類ある。

  • local handle はスタックに保持され、適切なデストラクターが呼ばれたときに削除される。これらの handle の寿命は handle の scope (しばしば関数呼び出しの最初に作られる) によって決められる。handle の scope が消されるとき、GC は handle scope 内の handle に参照されたオブジェクトを、それがもう JavaScript からアクセスできなければ自由に解放できる。Getting Started の例ではこのタイプの handle が使われていた。
    local handle は Local<SomeType> というクラスを持ち、Handle<SomeType> という上位型として宣言された変数に保存することもできる。
    注: handle のスタックは C++ のコールスタックの一部ではないが、handle scope は C++ のスタックに埋め込まれている。handle scope は必ずスタックに割り当てられ、new によって割り当ててはいけない
  • persistent handle はスタックに保持されず、自分で取り除かない限り削除されない。persistent handle は local handle のように、ヒープに割り当てられたオブジェクトへの参照を提供する。persistent handle は、一回の関数呼び出しよりも長く参照を持っていたいときや、handle の寿命が C++ のスコープと同じではないときに使う。例えば Google Chrome は persistent handle を DOM ノードを参照するのに使っている。persistent handle は Persistent::New によって作られ、Persistent::Dispose によって捨てられる。Persistent::MakeWeak を使えば、persistent handle は weak persistent handle となり、オブジェクトへの参照が weak persistent handle からだけになった時に GC からコールバックが呼ばれるようにすることができる。
    persistent handle は Persistent<SomeType> クラスを持ち、Handle<SomeType> という上位型として宣言された変数に保存することもできる。

もちろん、オブジェクトを作るたびに handle を作ると、たくさんの handle ができてしまう。そこで handle scope というのが便利になる。handle scope は、たくさんの handle を保持する容器と考えればいい。handle scope のデストラクターが呼ばれたとき、scope 内に作られた handle はすべてスタックから削除される。察するとおり、これで handle が指すオブジェクトが GC によってヒープから削除されることになる。

Getting Started の簡単な例に戻ってみる。次の図中に handle スタックとヒープに割り当てられたオブジェクトが見えるだろう。Context::New() は handle スタックに属さない persistent handle を作ることに注意。

f:id:edvakf:20100405072512p:image

デストラクター HandleScope::~HandleScope が呼ばれると handle scope は削除される。削除された handle scope の中で handle に参照されていたオブジェクトは、他にそれを参照しているものが無ければ、次の GC 時に削除される。GC は、source_objscript_obj がどの handle からも参照されていなかったり JavaScript からアクセスすることができなければ、それらをヒープから取り除くこともできる。context handle は persistent handle なので、handle scope から抜けても取り除かれない。context handle を取り除く唯一の方法は明示的に Dispos を呼ぶことだ。

注: この文書において、handle は local handle を意味している。 persistent handle と書かれたときだけ persistent handle を意味する。

Contexts

V8 において context とは、互いに関係しない別個の JavaScript アプリケーションを、ひとつの V8 インスタンスの中で実行する環境のことだ。JavaScirpt コードを実行するときは、明示的に実行する context を指定しないといけない。

なぜこんなことが必要か。それは、JavaScript が書き換え可能な built-in ユーティリティ関数を持っているからだ。例えば、もし互いに関係しない2つの JavaScirpt のコードが global オブジェクトを変更したら、当然予期しない結果が起こるだろう。

作られる built-in オブジェクトの数を考えると、毎回新しい context を作るのは CPU 時間とメモリにとって高くつくのではないかと思うだろう。しかし V8 の extensive caching によって、最初の context は高くても、次からの context は遥かに安いことが保証される。これはつまり、最初の context は built-in オブジェクトを作って built-inJavaScript コードをパースしないといけないが、次からの context は時分用の build-in オブジェクトを作るだけでいいからだ。V8 snapshot 機能 (ビルドオプション snapshot=yes で有効になる。デフォルトで有効) を使えば、snapshot がコンパイル済みの built-inJavaScript のコードを含むシリアル化されたヒープを持つため、最初の context を作る時間は高度に最適化される。extensive caching は GC と同様に V8 のパフォーマンスのキーである。もっと知りたければ Design Elements を見ること。

一度 context を作ると、何回でもその中に入ったり出たりできる。context A にいながら別の context B に入ることもできる。言わば、A を B で置き換えるのだ。B を出るときに A が現在の context に戻る。これを下に図示した。

f:id:edvakf:20100407163203p:image

各 context の built-in のユーティリティ関数とオブジェクトは別々に保持される。context を作るとき、オプションとして security token をセットすることもできる。下の Security Model の項を参照のこと。

V8 において context を使う理由は、ブラウザの window と iframe がそれぞれ別々の新しい JavaScript 環境を持てるようにするためだ。

Templates

template は JavaScript の関数とオブジェクトの雛形のことだ。template を使えば C++ の関数やデータ構造を JavaScriptオブジェクトにラップして、JavaScript コードから操作されることができる。例えば Google Chrome では C++ の DOM ノードを JavaScript オブジェクトでラップしたり、グローバルな名前空間に関数を置いたりしている。template を作り、新たな context を作るごとに同じのを利用することができる。template はいくつでも欲しいだけ作ることができるが、template のインスタンスは context で一つしか作ることができない。

JavaScript には関数とオブジェクトの強い二元性 (duality) がある。Java や C++ で新しい型のオブジェクトを作るには、普通は新しいクラスを作るが、JavaScript では新しい関数を作り、それをコンストラクターとして新しいインスタンスを作る。JavaScript オブジェクトのレイアウトと機能は、コンストラクターとなったオブジェクトに綿密に繋がっている。V8 の template はそれを反映している。テンプレートには典型的なものが2種類ある。

  • function template
    • function template は関数の雛形だ。JavaScript の関数のインスタンスを作るには、それを作りたい context で function template の GetFunction メソッドを呼ぶ。function template に C++ のコールバックを関連付けて、JavaScript の関数が呼ばれたときに呼び出されるようにすることもできる。
  • object template

次のコードはグローバルオブジェクトの template を作り、built-in のグローバル関数をセットする例を示している。

// Create a template for the global object and set the
// built-in global functions.
Handle<ObjectTemplate> global = ObjectTemplate::New();
global->Set(String::New("log"), FunctionTemplate::New(LogCallback));

// Each processor gets its own context so different processors
// do not affect each other.
Persistent<Context> context = Context::New(NULL, global);

この例は process.ccJsHttpProcessor::Initializer から取った。

Accessors

accessor はオブジェクトプロパティJavaScript からアクセスされたときに呼ばれる C++ のコールバックだ。accessor は object template の SetAccessor メソッドを通して設定される。このメソッドの引数は、プロパティ名と、JavaScript がそのプロパティを読み書きするときに使う2つのコールバックだ。

accessor は、操作するデータによって2種類ある。

  • グローバルな変数にアクセスするとき
  • ダイナミックな変数にアクセスするとき

の2つだ。

グローバルな変数にアクセスする

ある JavaScript の context に xy という2つの整数のグローバル変数があるとしよう。これを実現するには、これらの変数が読み書きされるときに呼ばれる C++ の accessor 関数が必要だ。これらの accessor 関数は、C++ の整数を Integer::New を使って JavaScirpt の整数に変換したり、JavaScript の整数を C++ の変数Int32Value を使って変換したりするものだ。下に例を示す。

  Handle<Value> XGetter(Local<String> property, 
                        const AccessorInfo& info) {
    return Integer::New(x);
  }
    
  void XSetter(Local<String> property, Local<Value> value,
               const AccessorInfo& info) {
    x = value->Int32Value();
  }
       
  // YGetter/YSetter are so similar they are omitted for brevity
  
  Handle<ObjectTemplate> global_templ = ObjectTemplate::New();
  global_templ->SetAccessor(String::New("x"), XGetter, XSetter);
  global_templ->SetAccessor(String::New("y"), YGetter, YSetter);
  Persistent<Context> context = Context::New(NULL, global_templ);

これらのオブジェクトは context と同じ時に作られることに注意。template さえ事前に作ったら、何個でも別の context で使っていい。


ダイナミックな変数にアクセスする

前の例では変数は静的でグローバルだった。もしデータが動的に操作されたらどうなるだろうか? DOM ツリーはその一例だ。xy が C++ の Point クラスのメンバー変数だとしよう。

  class Point {
   public:
    Point(int x, int y) : x_(x), y_(y) { }
    int x_, y_;
  }

C++ の Point インスタンスをいくつでも JavaScript から使えるようにするには、C++ の各 Point につき、JavaScriptオブジェクトと C++ のインターフェイスを繋げないといけない。これは external object (external value) と internal field (internal object field) によって実現することができる。

まず Point ラッパーオブジェクトobject template を作る。

  Handle<ObjectTemplate> point_templ = ObjectTemplate::New();

JavaScript の各 Point オブジェクトは C++ オブジェクトへの参照をラップした internal field を持つ。internal field は JavaScript からはアクセスできず、C++ からしかアクセスできないためにこう呼ばれる。1つのオブジェクトはいくつでも internal field を持つことができ、その数は次のようにして object template にセットする。

  point_templ->SetInternalFieldCount(1);

ここでは internal field count を1にセットした。つまり、このオブジェクトは internal field を1つ持ち、そのインデックス0は1つの C++ のオブジェクトを指している。

template に xy の accessor を付加する。

  point_templ.SetAccessor(String::New("x"), GetPointX, SetPointX);
  point_templ.SetAccessor(String::New("y"), GetPointY, SetPointY);

次に、template の新しいインスタンスを作り、その0番目の internal field を p の外部ラッパーとする。

  Point* p = ...;
  Local<Object> obj = point_templ->NewInstance();
  obj->SetInternalField(0, External::New(p));

external object は単なる void * のラッパーだ。external object は internal field への参照を保存するためだけに使われる。JavaScriptオブジェクトは C++ のオブジェクトへの参照を直接は持てないので、external valueJavaScript と C++ の「橋」として使われる。この意味では、external value は handle の逆と言える。handle は C++ が JavaScriptオブジェクトへの参照を持てるようにするからだ。

下に xgetset accessor の定義を示す。y の accessor は、xy に変わる以外はまったく同じだ。

  Handle<Value> GetPointX(Local<String> property,
                          const AccessorInfo &info) {
    Local<Object> self = info.Holder();
    Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
    void* ptr = wrap->Value();
    int value = static_cast<Point*>(ptr)->x_;
    return Integer::New(value);
  }
  
  void SetPointX(Local<String> property, Local<Value> value,
                 const AccessorInfo& info) {
    Local<Object> self = info.Holder();
    Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
    void* ptr = wrap->Value();
    static_cast<Point*>(ptr)->x_ = value->Int32Value();
  }

accessor は JavaScript オブジェクトにラップされた Point オブジェクトへの参照を取り出し、関連付けられたフィールドを読み書きする。このようにすることで、これらの汎用 accessor は、いくつものラッパー Point オブジェクトを扱える。

Interceptors

JavaScript であるオブジェクトのどのプロパティにアクセスしたときにも呼ばれるコールバックを指定することもできる。これらは interceptor と呼ばれる。効率のために2種類の interceptor が用意されている。

named property interceptor
文字列でプロパティにアクセスするときに呼ばれる。
ブラウザでの例は、document.theFormName.elementName のような場合だ。
indexed property interceptor
番号でプロパティにアクセスするときに呼ばれる。
ブラウザでの例は、document.forms.elements[0] のような場合だ。

process.cc のサンプルには interceptors を使うサンプルがある。次のSetNamedPropertyHandler スニペットでは MapGetMapSet interceptor を指定している。

Handle<ObjectTemplate> result = ObjectTemplate::New();
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGet interceptor は以下のようになる。

Handle<Value> JsHttpRequestProcessor::MapGet(Local<String> name,
                                             const AccessorInfo &info) {
  // Fetch the map wrapped by this object.
  map<string, string> *obj = UnwrapMap(info.Holder());

  // Convert the JavaScript string to a std::string.
  string key = ObjectToString(name);

  // Look up the value if it exists using the standard STL idiom.
  map<string, string>::iterator iter = obj->find(key);

  // If the key is not present return an empty handle as signal.
  if (iter == obj->end()) return Handle<Value>();

  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string &value = (*iter).second;
  return String::New(value.c_str(), value.length());
}

プロパティがアクセスされたときに指定されたコールバックが呼ばれるのは accessor と同じだが、accessor と違うのは、interceptor はすべてのプロパティを扱うのに対し、accessor は特定のプロパティに関連付けられていることだ。

Security Model

"same origin polycy" (Netscape Navigator 2.0 で最初に導入された) とは、ある "origin" からロードされたドキュメントやスクリプトが他の "origin" 由来のドキュメントのプロパティを読み書きしたりするのを禁止するものだ。"origin" という単語はここでは、ドメイン名 (www.example.com)、プロトコル (http か https)、そしてポート (つまり www.example.com:81 は www.example.com と同じではない) によって定義されている。これらすべてが2つのウェブページで同じなら same origin と言える。このプロテクションがなかったら、危険なサイトが他のウェブページの安全を侵すことができてしまう。

V8 では "origin" は context で定義さている。ある context から他の context にアクセスするのはデフォルトで禁止されている。今呼んでいる context とは別の context にアクセスするには、security token や security callback を使わなくてはいけない。security token はどんな値でもいいが、通常は symbol (他に存在しない一意の string) を使う。context を作るときに security token を SetSecurityToken を付けて指定することができる。security token を指定しなければ自動的に作られる。

グローバル変数にアクセスしようとすると、V8 のセキュリティシステムはまずグローバルオブジェクトセキュリティトークンが、アクセスしようとしているコードのグローバルオブジェクトと同じかチェックする。一致すればアクセスは許される。もし一致しなければ、アクセスが許されるかどうかのコールバックを呼ぶ。オブジェクトへのアクセスが許されるかどうかは、object template の SetAccessCheckCallbacks メソッドオブジェクトの security callback をセットすることによって指定できる。そうすることで、V8 のセキュリティシステムはアクセスされるオブジェクトの security callback を呼んで、他の context がアクセスしてもいいかを確認する。security callback は引数として、アクセスされるオブジェクト、アクセスされるプロパティの名前、それとアクセスの種別 (読み、書き、削除、など) を渡され、アクセスを許可するか否かを返すことになる。

このメカニズムは Google Chrome では、security callback によって window.focus()window.blur()window.close()window.locationwindow.open()hisotry.forward()history.back()history.go() のみにアクセスを許可するように実装されている。

Exceptions

V8 はエラーが起こったときは例外を投げる。例えば、スクリプトや関数が存在しないプロパティを読もうとしたときや、関数でないものを関数として呼ぼうとしたときなどだ。

操作が成功しなかったとき、V8 は empty handle を返す。だから、実行を続ける前に返り値が empty handle でないかを確認することが大切だ。empty handle をチェックするには Handle のパブリックメンバー関数 IsEmpty() を使う。

例外を捕捉するには TryCatch が使える。

  TryCatch trycatch;
  Handle v = script->Run();
  if (v.IsEmpty()) {  
    Handle<value> exception = trycatch.Exception();
    String::AsciiValue exception_str(exception);
    printf("Exception: %s\n", *exception_str);
    // ...
  }

もし返り値が empty handle で、TryCatch が無かったら、このコードはこのまま不当に走り続けてしまう。上のように TryCatch があり、例外が捕捉された場合は、このコードは晴れてきちんと続行できる。

Inheritance

JavaScript は「クラスのない」オブジェクト指向言語で、クラス的継承の変わりにプロトタイプ的継承をする。これは C++ や Java のような伝統的オブジェクト指向言語を習った人にとっては混乱しやすい。

Java や C++ のようなクラスベースのオブジェクト指向言語では、クラスとインスタンスという別々の概念の上に成り立っている。JavaScript はプロトタイプベースの言語なので、これらの違いはない。オブジェクトがあるだけだ。JavaScript はクラスによる継承をネイティブでサポートしないが、プロトタイプメカニズムによって、オブジェクトにカスタムプロパティを付加するのを簡単にできる。

JavaScript では、オブジェクトにカスタムプロパティを付加するのは次の例のようにする。

// Create an object "bicycle" 
function bicycle(){ 
} 
// Create an instance of bicycle called roadbike
var roadbike = new bicycle()
// Define a custom property, wheels, on roadbike 
roadbike.wheels = 2

この方法で付加されたカスタムプロパティはこのインスタンスだけに存在する。もし bicycle() の他のインスタンス (例えば mountainbike) を作ったら、明示的に付加しない限り mountainbike.wheelsundefined を返す。

これが望む挙動であることもあるが、オブジェクトの全てのインスタンスにカスタムプロパティを付けたいときもあるだろう (どうせすべての自転車は車輪を持ってるのだから)。こういうときに JavaScriptprototype オブジェクトは便利だ。prototype オブジェクトを使うには次のようにする。

// First, create the "bicycle" object
function bicycle(){ 
}
// Assign the wheels property to the object's prototype
bicycle.prototype.wheels = 2

これで bicycle() の全インスタンスは wheels プロパティを持つ。

V8 では template で同じアプローチが使われる。各 FunctionTemplatePrototypeTemplate メソッドを持ち、関数の prototype の template を与える。PrototypeTemplateプロパティ (とそれに関連する C++ コールバック関数) をセットすると、対応する FunctionTemplate の全インスタンスはそのプロパティを持つ。例えばこのように。

 Handle<FunctionTemplate> biketemplate = FunctionTemplate::New();
 biketemplate.PrototypeTemplate().Set(
     String::New("wheels"),
     FunctionTemplate::New(MyWheelsMethodCallback)
 )

これで biketemplate の全インスタンスは wheels メソッドをプロトタイプチェーンに持ち、それが呼ばれると C++ の MyWheelsMethodCallback 関数が呼ばれる。

V8 の FunctionTemplate クラスはパブリックメンバー関数 Inherit() を持ち、ある function template を別の function template から継承させたい時に使う。

void Inherit(Handle<FunctionTemplate> parent);

せっかくなので質問。

v8::AccessorInfo::This() と v8::AccessorInfo::Holder() って何が違うんでしょう? 上の文書には Arguments のときは This で AccessorInfo のときは Holder って書いてあって、ここでもそう説明されてるんだけど、こっちでは This() を使ってるし、僕が書いたやつでも Holder じゃなくて This を使わないとうまくいかない。

教えて詳しい人。


教えてもらった

V8 で JavaScript のクラス (コンストラクター) に相当するものは FunctionTemplate、PrototypeTemplate、InstanceTemplate の3つを定義して作るのだけど、InstanceTemplate にSetAccessor した場合は AccessorInfo の This と Holder が同じものを指すのに対し、PrototypeTemplate に SetAccessor した場合は ThisJavaScript でいう this を指し、Holder は JavaScript でいう __proto__ を指すらしい。

 |