V8 APIリファレンスガイド
V8はGoogleのオープンソースJavaScriptエンジンである。
この一連の文書は、include/サブディレクトリにあるV8ヘッダーファイルから生成された参考資料を提供する。
その他の文書については、https://v8.dev/。
V8_*接頭辞は、V8パブリックAPIで定義されたマクロのために予約され、エンベッダーのコードと名前の衝突がないことを前提としています。
V8パブリックC++ API
概要
V8のパブリックC++ APIは、次の4つのユースケースをサポートすることを目的としています。:
- V8を組み込むアプリケーション(エンベッダーと呼ばれます)が、1つまたは複数のV8インスタンスを設定し、実行できるようにします。
- ECMAScriptのような機能をエンベッダーに公開する。
- APIオブジェクトを公開することで、エンベッダーがECMAScriptと対話できるようにする。
- V8デバッガ(インスペクタ)へのアクセスを提供する。
V8インスタンスの設定と実行
V8は、スレッド上で作業をスケジューリングしたり、メモリを割り当てたりする機能など、特定のOSレベルのプリミティブにアクセスする必要がある。
エンベッダは、v8::Platform
インターフェースを通して、これらのプリミティブへのアクセス方法を定義することができます。V8は基本的な実装をバンドルしていますが、エンベッダ自身が v8::Platform
を実装することを強く推奨します。
現在、v8::ArrayBuffer::Allocator
は v8::Isolate
ファクトリーメソッドに渡される。しかし、すべてのV8インスタンスが1つのアロケータを共有する必要があるので、概念的にはv8::Platform
の一部でもあるはずだ。
v8::Platform
が設定されると、v8::Isolate
を作成することができる。 それ以降の V8 とのやり取りはすべて、参照する v8::Isolate
を明示的に参照しなければならない。全てのAPIメソッドは最終的に v8::Isolate
パラメータを取るべきである。
V8のインスタンスが不要になったとき、それぞれの v8::Isolate
を破棄することで破棄できる。. エンベッダがv8::Isolate
に関連する全てのメモリを解放したい場合、まず、そのv8::Isolate
に関連する全てのグローバルハンドルをクリアしなければなりません。
ECMAScriptのような機能
一般的に、C++ APIは、V8で動作するスクリプトで利用できない機能を有効にすべきではありません。 経験上、このようなAPIメソッドを長期的に維持することは不可能だ。 しかし、スクリプトでも利用できる機能、つまりECMAScript標準に定義されている機能は、そのまま残りますし、エンベッダーに安全に公開することができます。
また、C++ APIは使い心地がよく、新しいパラダイムを学ぶ必要もないはずだ。 スクリプトに公開されるAPIが人間工学的に優れたものを提供することを目指しているのと同様に、このAPIサーフェスに対しても、合理的な開発者体験を提供することを目指すべきである。
ECMAScriptは例外を多用しますが、V8のC++コードはC++の例外を使いません。 そのため、例外を投げることができるすべての API メソッドは、 v8::Maybe<>
または v8::MaybeLocal<>
の結果を返し、どのコンテキストで例外が発生するかを示す v8::Local<v8::Context>
パラメータを取ることでそのことを示すべきである。
APIオブジェクト
V8では、エンベッダーに特別なオブジェクトを定義し、スクリプトに追加の機能やAPIを公開することができます。最も顕著な例は、BlinkにおけるHTML DOMの公開です。他の例としては、node.jsなどがあります。このAPIサーフェスを通じてどのような機能を公開したいかは、あまり明確ではない。経験則として、私たちはWebIDLとHTML仕様で定義された操作を公開したいと考えています。これらの要件はある程度安定しており、node.jsを含む他のエンベッダーの要件のスーパーセットであると想定しています。
理想的には、これらの仕様で定義されたAPIサーフェスがECMAScript仕様にフックされることで、APIの長期的な安定性が保証される。
V8インスペクター
V8のすべてのデバッグ機能は、インスペクタ・プロトコルで公開されるべきです。ただし、v8-profiler.hを介して公開されるプロファイリング機能は例外です。インスペクタのプロトコルを変更する場合は、後方互換性と保守性を確保する必要があります。
Oilpan: C++のガベージコレクション
Oilpanは、C++用のオープンソースのガベージコレクションライブラリであり、スタンドアロンで、またはV8のJavaScriptガベージコレクタと連携して使用することができる。Oilpanは、(オブジェクトのサブセットに対して)限定的なコンパクションを伴うマークアンドスイープガベージコレクション(GC)を実装する。
主要特性
- トレースベースのガベージコレクション
- インクリメンタルマーキングとコンカレントマーキング
- インクリメンタルおよびコンカレント・スイープ
- 正確なオンヒープ・メモリ・レイアウト
- 保守的なオンスタック・メモリ・レイアウト
- スタックを考慮した収集と考慮しない収集が可能
- 選択されたスペースに対する非インクリメンタルかつ非同時のコンパクション
C++コードの管理にOilpanを使い始める方法については、Hello Worldの例を参照。
OilpanはV8のプロジェクト組織に従っており、コントリビュートを受け入れ、安定したAPIを提供する方法などを参照。
スレッディング・モデル
Oilpanはスレッドローカルガベージコレクションを特徴としており、ヒープはスレッド間で共有されないと仮定している。言い換えると、オブジェクトは、それらを割り当てたのと同じスレッド上のガベージコレクタによってアクセスされ、最終的に取り戻される。これによりOilpanは、他のスレッドで実行されているミューテータと並行してガベージコレクションを実行できる。
他のスレッドのヒープに属するオブジェクトへの参照は、クロススレッドルートを使用してモデル化される。これは、オンヒープからオンヒープへの参照にも当てはまる。
Oilpanのヒープは、特に断りのない限り、通常、異なるスレッドからアクセスすることはできません。
ヒープ分割
Oilpanのヒープはスペースに分割される。オブジェクトのためのスペースは、いくつかの基準によって選択される。例えば:
- 64KiB以上のオブジェクトはラージオブジェクトスペースに割り当てられる。
- オブジェクトは専用のカスタムスペースに割り当てることができる。カスタムスペースはコンパクトにすることもできます。
- その他のオブジェクトは、サイズに応じてバケット化された通常のページスペースのいずれかに割り当てられます。
正確で保守的なガベージコレクション
Oilpanは2種類のGCをサポートする:
- 保守的GC。GCが保守的と呼ばれるのは、通常のネイティブスタックが空でない間に実行される場合である。この場合、ネイティブスタックは、Oilpanのヒープ内のオブジェクトへの参照を含むかもしれない。GCはネイティブスタックをスキャンし、ネイティブスタックを介して発見されたポインタをルートセットの一部として扱う。この種のGCは不正確であると考えられている。なぜなら、スタック上の参照以外の値が、誤ってヒープ上のオブジェクトへの参照として現れる可能性があり、これは、実際の参照としてアプリケーションから到達できないにもかかわらず、これらのオブジェクトが生かされることを意味するからである。
- 正確なGC。正確なGCは、プラットフォームを介してエンベッダによって制御されるイベントループの終了時にトリガされます。この時点で、Oilpanのヒープを指すオンスタック参照がないことが保証される。これは、他の値型を参照と混同するリスクがないことを意味する。Oilpanは、ヒープ上のオブジェクトレイアウトに関する正確な知識を持っているため、メモリ上のどこにポインタがあるかを正確に知っている。Oilpanは、通常のルートセットからマークを開始するだけで、すべてのガベージを正確に収集できる。
アトミック、インクリメンタル、コンカレント・ガベージコレクション
Oilpanには3つの動作モードがある:
- アトミックGC。すべてのフェーズ(例えば、マーキングとスイープを参照)を含むGCサイクル全体が、1回の休止で前後に実行される。この動作モードは、Stop-The-World(STW)ガベージコレクションとしても知られている。この動作モードは、(1回の長い休止のために)最もジャンクが多くなりますが、全体的には最も効率的です(例えば、書き込みバリアが不要です)。
- インクリメンタルGC。ガベージ・コレクション作業は、ミューテーターとインターリーブされた複数のステップに分割される。各ステップは、ミューテーター・タスクの間の専用タスクとして、あるいは必要に応じてミューテーター・タスクの間に実行される小さな作業の塊です。インクリメンタルGCを使用すると、オブジェクト・グラフへの変更を記録する書き込みバリアが必要になる。インクリメンタルなステップの後には、ガベージコレクションを確定するために、より小さなアトミックな休止時間が続く。作業チャンクが小さくなるため、休止時間が短くなり、ジャンクの低減に役立つ。
- コンカレントGC。これはGCの最も一般的なタイプである。これはインクリメンタルGCの上に構築され、ガベージコレクション作業の多くをミューテーター・スレッドからバックグラウンド・スレッドにオフロードします。コンカレントGCを使用することで、ミューテーター・スレッドはGCに費やす時間を減らし、実際のミューテーターにより多くの時間を費やすことができます。
マーキング段階
マーキング段階は以下のステップからなる:
1.ルートセット内のすべてのオブジェクトにマークを付ける。 2.2. 各オブジェクトに定義された Trace()
メソッドを呼び出して、ルートセットから遷移的に到達可能なすべてのオブジェクトをマークする。 3.到達不可能なオブジェクトへの弱いハンドルをすべて消去し、弱いコールバックを実行する。
マーキングフェーズは、3つのステップを次々に実行するストップ・ザ・ワールド方式でアトミックに実行することができる。
また、インクリメンタル/コンカレントに実行することもできる。インクリメンタル/コンカレント・マーキングでは、ステップ1はミューテーターが制御を回復するまでの短い休止の間に実行される。ステップ 2 は、ミューテーターとインターリーブされた形で繰り返し実行される。GCがファイナライズする準備ができたとき、すなわちステップ2が(ほぼ)終了したとき、別の短い休止がトリガされ、その間にステップ2が終了し、ステップ3が実行される。
ユーザーアフターフリー(UAF)の問題を防ぐために、Oilpanはオブジェクトグラフのすべてのエッジについて知っている必要がある。これは、オンスタックポインタを除くすべてのポインタが、Oilpanのハンドル(すなわち、Persistent<>
、Member<>
、WeakMember<>
)でラップされなければならないことを意味する。オンヒープオブジェクトへの未加工ポインタは、Oilpanが観察できないエッジを作り、UAF問題を引き起こす。 したがって、未加工ポインタは、オンヒープオブジェクトを参照するために使用してはならない(ネイティブスタック上の未加工ポインタを除く)。
スイープフェーズ
スイープ段階は以下のステップで構成される。:
- プリファイナライザーを起動する。この時点では、デストラクタは起動されておらず、メモリも回収されていない。プリファイナライザは、デストラクションされる可能性のあるオブジェクトであっても、他のすべてのオンヒープ・オブジェクトにアクセスすることが許される。
- スイープは、死んだ(到達不可能な)オブジェクトのデストラクタを呼び出し、将来の割り当てで再利用されるメモリを取り戻す。
デストラクタの順番や実行のタイミングについて仮定してはならない。デストラクタが呼び出される順番は保証されていない。そのため、デストラクタは(すでにデストラクトされているかもしれない)他のオンヒープ・オブジェクトにアクセスしてはならない。あるデストラクタがやむを得ず他のオンヒープ・オブジェクトにアクセスする必要がある場合、それはプリファイナライザに変換されなければならない。プリファイナライザは、他のオンヒープ・オブジェクトにアクセスすることが許される。
ミューテーターは、すべてのデストラクターが実行される前に再開されます。例えば、XがYのクライアントで、Yがクライアントのリストを保持している場合を考えてみよう。XのデストラクタがXをリストから削除することに依存するコードでは、Yがリストを反復してXのメソッドを呼び出し、他のオンヒープ・オブジェクトに触れる可能性があります。これはuse-after-freeを引き起こす。Xのデストラクタに依存しない方法(例えばプリファイナライザ)でミューテータが実行を再開する前に、Xが明示的にリストから削除されるように注意しなければなりません。
マーキングと同様に、掃引はアトミックなstop-the-world方式で実行することも、インクリメンタル/同時実行することもできます。インクリメンタル/同時スイープの場合、ステップ2はミューテーターとインターリーブされる。インクリメンタル/並行掃引は、別のGCサイクルをトリガーする必要がある場合に備えて、アトミックに終了させることができる。並行掃引の場合でも、C++のセマンティクスを維持するために、デストラクタはオブジェクトが割り当てられたスレッドで実行されることが保証されている。
備考
- 弱い処理が実行されるのは、WeakMemberのホルダー・オブジェクトがポインテッド・オブジェクトより長生きしているときだけです。ホルダー・オブジェクトとポインテッド・オブジェクトが同時に死んだ場合、弱い処理は実行されない。弱い処理が常に実行されることを前提にコードを書くのは間違っている。
- プリファイナライザが重いのは、どのプリファイナライザを呼び出すべきかを決定するために、スレッドが掃引フェーズごとにすべてのプリファイナライザをスキャンする必要があるからです(スレッドは、死んだオブジェクトのプリファイナライザを呼び出す必要があります)。頻繁に生成されるオブジェクトにプリファイナライザを追加することは避けるべきです。