V8をアプリケーションに組み込むためのガイド
https://v8.dev/docs/embed を読み進めていったときのメモです。
概要
C++アプリケーション内に、V8 JavaScriptエンジンを組み込むためのガイド。
次の2つの方法を紹介する
- C++のオブジェクトやメソッドをJavaScriptで使えるようにする
- JavaScriptのオブジェクトやメソッドをC++で使えるようにする
重要な概念
Isolate(アイソレート)
自らのヒープをもった、VMインスタンス。
Local Handle(ローカルハンドル)
オブジェクトを参照するもの。V8のオブジェクトはすべてハンドルを介して利用される(GCの仕組みと関係がある)。
Handle Scope(ハンドルスコープ)
複数のハンドルの集合。使い終わったハンドルを一つ一つ削除するのではなく、スコープごと削除する、といったことができる。
Context(コンテキスト)
実行環境。複数の(関係のない)JavaScriptコードを一つのV8インスタンス内で走らせることを可能にする。
どのコンテキスト内でJavaScriptコードを走らせたいのか、常に明示的に指定する必要がある。
ハンドルとGC
ハンドルは、(ヒープ内にある)JavaScriptのオブジェクトの位置への参照を提供する。
V8のGCは、「今後アクセスされないオブジェクトによって使われているメモリ」を再利用できるようにする。その過程で、オブジェクトがヒープ内の別の位置に移動されることがあるが、そういった場合にハンドルは更新される。
オブジェクトがJavaScript内で参照されていない、かつどのハンドルからも参照されていないとき、オブジェクトはGarbageとみなされる。そして、(適当なタイミングで)GCによって削除される。
ハンドルの種類
ハンドルにはいくつか種類がある。
Local Handle(ローカルハンドル)
ローカルハンドルはスタックに保持され、適当なデストラクタによって削除される。
このハンドルのライフタイムは、「ハンドルスコープ」によって決定される。ハンドルスコープは、一般的には関数呼び出し時に作成される。
ハンドルスコープが削除されるとき、ハンドルスコープ内のハンドルによって参照されていたオブジェクト群は不要とみなされ、それらよって使われていたメモリは再利用可能となる。(JavaScriptコードまたは他のハンドルから参照されている場合にはオブジェクトは不要にならない。)
ローカルハンドルに対応するV8のクラスは Local<SomeType>
である
注: ハンドルスタックはC++のコールスタックの中にはないが、ハンドルスコープはC++のコールスタックの中にある。ハンドルスコープはスタックにしかアロケートされない(new
を用いてアロケートしない)。
Persistent Handle(永続ハンドル)
ローカルハンドルと同様、ヒープに置かれたJavaScriptオブジェクトへの参照を提供する。
2つのバリエーションが有り、それによって参照のライフタイムが異なる。
Persistent Handleは、以下のような場合に用いる。
- オブジェクトへの参照を一つの関数を超えて保持したい場合
- C++のスコープに対応しない形でオブジェクトを用いたい場合
例えばGoogle Chromeは、DOMノードへの参照を保持するのにPersistent Handleを用いている。
Persistent HandleはWeakにすることもできる。「オブジェクトがWeakなPersistent Handleからしか参照されていない場合には、GCによるコールバックを発動させる」といったことができる。
Persistent Handleの2つのバリエーションは、以下の通りである。
UniquePersistent
対象オブジェクトのライフタイムがC++のコンストラクタとデストラクタに依存する。
Persistent
コンストラクタによって作成できるが、 Persistent::Reset
によって明示的にクリアしないといけない。
メモ
PersistentとUniquePersistentはコピーすることができず、C++11以前の標準ライブラリのコンテナ(std::vectorなど)とともに用いることには向いていない。そのため、PersistentValueMap
とPersistentValueVector
が用意されている。
その他
Eternal
削除されることのないオブジェクトのためのPersitent Handle。オブジェクトが生きているかをGCが判定する手間を省くことができるため、軽量。
ハンドルスコープ
オブジェクトを作成するときに、毎回ローカルハンドルを作っていては、ハンドルの数が膨大になってしまう。
そこで便利なのが、ハンドルスコープである。ハンドルスコープのデストラクタが呼ばれたとき、そのスコープ内に作られた全てのハンドルがスタックから削除される。(そして、参照されていたオブジェクトはGCによってヒープから削除することができるようになる。)
ハンドル・ハンドルスコープの例
HandleScope handle_scope(isolate);
Local<Context> context = Context::New(isolate);
Persistent<Context> persistent_context(isolate, context);
Context::Scope context_scope(context);
Local<String> source_obj = String::NewFromUtf8(isolate, argv[1]);
Local<Script> script_obj = Script::Compile(source_obj);
Local<Value> local_result = script_obj->Run();
注目すべき点は、
Context::New()
はローカルハンドルを返しており、それを用いて永続ハンドルを作っている。- handle_scopeのデストラクタが呼ばれたとき、source_objやscript_objは不要になる
- persistent_contextは永続ハンドルなので、ハンドルスコープから抜けても削除されない
- persistent_contextを削除するには、persistent_context->Reset()をする必要がある
注
(ハンドルスコープを宣言している)関数内で定義したローカルハンドルを返り値として用いることはできない。
それに対処するためには、HandleScopeの代わりにEscapableHandleScopeクラスを用いる。そして以下のように、用いたいオブジェクトのローカルハンドルを引数として、Escapeメソッドを用いる。
Local<Array> NewPointArray(int x, int y, int z) { v8::Isolate* isolate = v8::Isolate::GetCurrent(); EscapableHandleScope handle_scope(isolate); Local<Array> array = Array::New(isolate, 3); if (array.isEmpty()) return Local<Array>(); array->Set(0, Integer::New(isolate, x)); array->Set(1, Integer::New(isolate, y)); array->Set(2, Integer::New(isolate, z)); return handle_scope.Escape(array); }
Escapeメソッドは、引数に渡された値を、現在のスコープを含んでいるスコープにコピーし、ローカルハンドルを全て削除する。そして新しいハンドルを返す。
コンテキスト
コンテキストは、実行環境を意味する。V8の一つのインスタンス内で、独立した(関連性のない)複数のJavaScriptのアプリケーションを実行するために用意されている。
JavaScriptコードを実行するときには、どのコンテキストで実行するのかを明示的に指定する必要がある。
JavaScriptでは、ビルトインのユーティリティ関数やオブジェクトを、コード内で変更することができる。もしも2つの全く関係ないJavaScriptの関数が、(共有している)グローバルオブジェクトを同じように変更しようとしたら、予期しない結果を引き起こすはずである。こういった問題が、コンテキストによって解消される。
多くのビルトインオブジェクトがその中に含まれることを考えると、CPUやメモリへの負荷が大きいと思うかもしれない。 しかし、V8のキャッシュ機構のおかげで、初回以降のコンテキストの作成は初回のコンテキストの作成よりもかなり軽量である。ビルトインのJavaScriptコードのパースは、初回のみで十分なためである。 さらに、V8のスナップショット機能(snapshot=yes ビルドオプションで有効になる。デフォルトで有効)は、ビルトインのJavaScriptコードを予めコンパイルし、シリアル化されたヒープを含んだスナップショットを作成する。これを用いれば、初回のコンテキストの作成も高速化される。
コンテキストは一度作った後、何度も入ったり出たりできる。
注: コンテキストAからコンテキストBに入り、その後にコンテキストBを抜けると、コンテキストAに復帰する。
それぞれのウィンドウ、そしてiframeがそれぞれ個別の(他に干渉されない)JavaScriptの環境を作りたいというモチベーションにより、V8はコンテキストの仕組みを持っている。
テンプレート
テンプレートは、コンテキスト内での関数やオブジェクトの設計図のようなものである。 テンプレートでC++の関数やデータ構造をラップすることで、JavaScriptから扱えるオブジェクトを作ることができるようになる。 例えばGoogle Chromeは、テンプレートを用いてC++のDOMノードをラップし、JavaScriptオブジェクトとして利用できるようにしている。 テンプレートは一度作れば、複数のコンテキストで用いることができる。 また、テンプレートは必要なだけ作成することができるが、一つのコンテキスト内で、各テンプレートはそれぞれ一つのインスタンスしか持つことができない。
JavaScriptでは、関数とオブジェクトに強い二重性がある。 C++やJavaでは、新しい種類のオブジェクトを作成する際にはクラスを定義する。 一方、JavaScriptでは関数を作り、その関数をコンストラクタとしてインスタンスを作成する。そのため、JavaScriptオブジェクトのレイアウトや機能は、それを作成した関数に強く結びついている。この特徴は、V8のテンプレートの動作に反映されている。
テンプレートには2つの種類がある。
Function Template(関数テンプレート)
Function Templateは、関数のための下書きである。コンテキスト内でGetFunction
メソッドを呼ぶことで、JavaScriptの関数をインスタンス化することができる。JavaScriptの関数(インスタンス)が呼ばれたときにC++コールバックが呼び出されるようにすることができる。
Object Template(オブジェクトテンプレート)
Function Templateはそれぞれ、対応するObject Templateを持っている。関数をコンストラクタとして、オブジェクトを作成するように設計されている。Object Templateには(以下の)2種類のC++のコールバックを関連付けることができる。
Accessor Callbacks(アクセッサコールバック)
特定のオブジェクトのプロパティがアクセスされた際に呼ばれるコールバック
Interceptor Callbacks(インターセプタコールバック)
オブジェクトのプロパティがアクセスされた際に必ず呼ばれるコールバック
以下の例では、グローバルオブジェクトのテンプレートを作成し、ビルトインのグローバル関数を設定している。
Local<ObjectTemplate> global = ObjectTemplate::New(isolate); global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback)); Persistent<Context> context = Context::New(isolate, NULL, global);
Accessors(アクセッサ)
JavaScriptによって、オブジェクトのプロパティがアクセスされたときに、計算を行い値を返すC++のコールバックのことをアクセッサという。 アクセッサは(SetAccessorメソッドを用いて)オブジェクトテンプレートで設定される。 SetAccessorメソッドは、関連付けたいプロパティの名前と、読み/書きの際にそれぞれ呼ばれる2つのコールバックを引数として受け付ける。
スタティクグローバル変数へのアクセス
C++内で2つの整数変数x, yをコンテキスト内のグローバル変数としてJavaScriptからアクセス可能にしたいケースを考える。
この場合には、スクリプトがそれらの値を読んだり書いたりする際に、C++のアクセッサを呼ぶ必要がある。
アクセッサ関数(読み/書きの2つ)は、C++の整数を(Integer::New
を用いて)JavaScriptの整数に変換し、JavaScriptの整数を(Int32Value
)を用いて、C++の整数に変換する。
コードは以下のようになる。(yについては省略)
void XGetter(Local<String> property, const PropertyCallbackInfo<Value>& info) { info.GetReturnValue().Set(x); } void XSetter(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void>& info) { x = value->Int32Value(); } Local<ObjectTemplate> global_templ = ObjectTemplate::New(isolate); global_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), XGetter, XSetter); Persistent<Context> context = Context::New(isolate, NULL, global_templ);
注: 上のコードで、オブジェクトテンプレートはコンテキストと同じタイミングで作られているが、「テンプレートは事前に作っておいて、それを後に複数のコンテキストで用いる」ということもできる。
ダイナミック変数へのアクセス
上の例では、変数が静的でグローバルなものだった。もしもブラウザのDOMツリーのように、操作されるデータがダイナミックだったらどうなるのであろうか。 まず、xとyが、C++のPointクラスのフィールドとしよう。
class Point { public: Point(int x, int y) : x_(x), y_(y) { } int x_, y_; }
C++のpointインスタンスを(ほしい分だけ)JavaScriptで使えるようにしたい。 そこで、C++のpointインスタンスそれぞれに対してひとつのJavaScriptオブジェクトをつくり、それらの間の関連付けをしないといけない。 これは、external valueとinternal object fieldsによって実現される。
まず、pointのラッパーオブジェクトのオブジェクトテンプレートを作成する。
Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);
各JavaScriptのpointオブジェクトは、それをラップし、内部フィールドを持っているC++オブジェクトへの参照を保持するようになっている。 内部フィールドというのは、JavaScriptからはアクセスできずC++からアクセスできるフィールドである。 オブジェクトはいくつでも内部フィールドを持つことができ、その数は以下のように(オブジェクトテンプレートにおいて)設定できる。
point_templ->SetInternalFieldCount(1);
ここで、内部フィールドの数は1に設定されており、その内部フィールドのインデックス0は、C++のオブジェクトを指している。
次に、xとyのアクセッサをテンプレートに追加する。
point_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), GetPointX, SetPointX); point_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), GetPointY, SetPointY);
そして以下のように、オブジェクトテンプレートからインスタンスを作成し、それを内部フィールドに設定してラップする。
Point* p =...;
Local<Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, External::New(isolate, p));
Externalオブジェクトはvoid*のラッパーである。Externalオブジェクトは内部フィールドに参照を保存するためにのみ用いられる。 JavaScriptオブジェクトはC++オブジェクトへの参照を直接持つことはできず、橋渡しとして、Externalバリューが用いられる。 HandleがC++からJavaScriptオブジェクトへの参照を提供していたのに対し、ExternalはJavaScriptからC++オブジェクトへの参照を提供しているという点で、HandleとExternalは対照的である。
以下はxのための読み込み/書き込みアクセッサの定義である。
void GetPointX(Local<String> property, const PropertyCallbackInfo<Value>& 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_; info.GetReturnValue().Set(value); } void SetPointX(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void>& 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(); }
アクセッサはJavaScriptオブジェクトにラップされているpointオブジェクトへの参照を取り出し、対象のフィールドを読み書きしている。 このようにして、何個のpointオブジェクトに対しても使えるジェネリックなアクセッサを作成できる。
Interceptor (インターセプター)
オブジェクトのどのプロパティがアクセスされたときにも呼び出されるようなコールバックを指定することができる。これはインターセプターと呼ばれている。 インターセプターには2つの種類が存在する。
Named Property Interceptor (名前付きプロパティのインターセプタ)
文字列の名前を持ったプロパティがアクセスされたときに呼ばれるインターセプタ。
(文字列の名前を持ったプロパティの)例として、ブラウザでの document.theFormName.elementName
などがある。
Indexed Property Intercepter (インデックスプロパティのインターセプタ)
インデックスプロパティがアクセスされたときに呼ばれるインターセプタ。
(インデックスプロパティの)例として、ブラウザでの document.forms.elements[0]
などがある。
V8のソースコードにサンプルとして含まれているprocess.ccはインターセプターを使用している。 以下のコードで、SetNamedPropertyHandlerはMapGetとMapSetをインターセプタとして指定している。
Local<ObjectTemplate> result = ObjectTemplate::New(isolate); result->SetNamedPropertyHandler(MapGet, MapSet);
MapGetインターセプタは以下のようになっている
void JsHttpRequestProcessor::MapGet(Local<String> name, const PropertyCallbackInfo<Value>& info) { map<string, string> *obj = UnwrapMap(info.Handler()); string key = ObjectToString(name); map<string, string>::iterator iter = obj->find(key); if (iter == obj->end()) return; const string &value = (*iter).second; info.GetReturnValue().Set(String::NewFromUtf8(value.c_str(), String::kNormalString, value.length())); }
セキュリティモデル
ブラウザでいう、same-origin policyは、あるオリジンから得たドキュメントやスクリプトが、他のオリジンから得たドキュメントを読み込んだり書き換えたりすることを防ぐためのものである。 ここで、オリジンというのは、ドメイン名、プロトコル(httpsなど)、そしてポートの組み合わせとして定義されている。つまり、www.example.com:81とwww.example.comは異なったオリジンであり、同じオリジンとして扱われるには、ドメイン名、プロトコル、ポートの全てが一致している必要がある。 このポリシーが無いと、悪意のあるウェブページが(ブラウザ内で)他のウェブページを書き換えたりすることができてしまう。
V8では、オリジンはコンテキストとして定義されている。呼び出し元のコンテキスト以外のコンテキストへのアクセスはデフォルトで禁止されている。もしも呼び出し元のコンテキスト以外のコンテキストにアクセスしたい場合には、セキュリティトークンまたはセキュリティコールバックを用いる必要がある。 セキュリティトークンは任意の値を取れるが、シンボル(他で使われていないカノニカルな文字列)を用いることが多い。 コンテキストを用意する際に、SetSecurityTokenを通して明示的にセキュリティトークンを指定することができる。もしもそうでない場合には、V8が自動で作成してくれる。
グローバル変数へのアクセスが試みられたとき、V8のセキュリティシステムは(アクセスの対象となっている)グローバルオブジェクトのセキュリティトークンと、(アクセスをしようとしている)コードのセキュリティトークンをチェックしている。 もしも一致していた場合、アクセスは許可される。もしも一致していない場合には、V8はアクセスを許可すべきか否かを判定するためのコールバックを呼び出す。 (オブジェクトテンプレートの)SetAccessCheckCallbacks関数を用いてオブジェクトに対してセキュリティコールバックを設定することで、オブジェクトへのアクセスを許可するか否かを指定することができる。 V8のセキュリティシステムは、(アクセスの対象となっている)オブジェクトのセキュリティコールバックを取得し、それを呼ぶことで他のコンテキストがそれにアクセス可能か否かを判定する。 このコールバックには、「アクセスされているオブジェクト」「アクセスされているプロパティ」そして「行われようとしている操作の種類(書き込み、読み込み、削除など)」が与えられ、「アクセスを許可するか」を返す。
例えばGoogle Chromeでは次のものに対して特別なコールバックが使用されている。window.focus(), window.blur(), window.close(), window.location, window.open(), history.forward(), history.back(), history.go()
例外
V8は、エラーが発生した際に例外を発生させる。例えば、スクリプトや関数が、存在しないプロパティを読み込んだり、関数じゃないものを関数として用いたりしたときである。 V8は、操作が成功しなかった場合に空のハンドルを返す。そのため、コード内で関数の返り値が空のハンドルで無いか逐一確かめる必要がある。それは、LocalクラスのIsEmpty関数を用いることによって可能である。 また、(以下のように)TryCatchを用いて例外をキャッチすることができる。
TryCatch trycatch(isolate); Local<Value> v = script->Run(); if (v.IsEmpty()) { Local<Value> exception = trycatch.Exception(); String::Utf8Value exception_str(exception); printf("Exception: %s\n", *exception_str); }
もしも返り値として空のハンドルが返ってきて、TryCatchが存在しなかった場合、コードの実行はは終了する必要がある。もしもTryCatchが存在しており(例外がキャッチされているのであれば)、コードは終了しなくても良い。
継承
JavaScriptは、クラスのないオブジェクト指向言語である。伝統的なクラス継承の仕組みではなく、プロトタイプを用いた継承を用いる。C++やJavaなどの、一般的なオブジェクト指向の言語に慣れているプログラマにとって混乱しやすいかもしれない。
(JavaやC++などの)クラスに基づいたオブジェクト指向プログラミング言語は、クラスとインスタンスという異なった2つの概念をベースにしている。 プロトタイプベースのJavaScriptにはこの区別がない(オブジェクトがあるだけである)。 JavaScriptはクラスのヒエラルキーの宣言に対応していない。しかし、JavaScriptはプロトタイプの仕組みによって、カスタムのプロパティやメソッドをオブジェクトに追加することを簡単にしている。 例えば、以下のようにしてオブジェクトにカスタムのプロパティを追加することができる。
function bicycle() {} var roadbile = new bicycle(); roadbike.wheels = 2;
このようにして追加されたカスタムプロパティは、当該オブジェクトにのみ存在する。もしほかのbicycle()インスタンス(mountainbikeとする)を作ったときには、mountainbike.wheelsはundefinedとなる。
このような動作が好ましいときもあるが、全てのオブジェクトインスタンスに共通してカスタムプロパティを追加したい場合も存在する。 その際に便利なのが、JavaScriptのプロトタイプオブジェクトである。プロトタイプオブジェクトは、以下のようにして利用することができる。
function bicycle() {} bicycle.prototype.wheels = 2;
これにより、bicycleインスタンスはすべてwheelsプロパティを持つようになった。
このような機能は、V8のテンプレートにも用いられている。関数テンプレートにはPrototypeTemplateというメソッドがあり、それによってテンプレートがプロトタイプの機能を持つことができる。 プロパティを追加し、そしてそれらにPrototypeTemplate関数を用いてC++の関数を関連付けることで、対応するFunctionTemplateのインスタンスからプロパティが利用できるようになる。 例としては、次のようになる。
Local<FunctionTemplate> biketemplate = FunctionTemplate::New(isolate);
biketemplate->PrototypeTemplate().Set(
String::NewFromUtf8(isolate, "wheels"),
FunctionTemplate::New(isolate, MyWheelsCallback)->GetFunction()
);
これにより、biketemplateは、プロトタイプチェーンにwheelsメソッドを持つようになり、それが呼ばれたときにはC++の関数MyWheelsMethodCallbackが呼ばれるようになる。
そしてV8のFunctionTemplateクラスは、メンバー関数としてInheritを持っている。これにより次の例のように、ある関数テンプレートを継承して関数テンプレートを作ることができる。
void Inherit(Local<FunctionTemplate> parent);