gc-safe-coding について
Hermes JavaScript エンジンの C++ コードを作成・レビューする開発者向けの GC セーフティガイドです。セーフポイント、値のルート化、例外確認、ローカル値の管理を扱います。
主な機能
- 01GC セーフポイントとオブジェクトのルート化
- 02新規コードで Locals と PinnedValue を使用
- 03CallResult の例外確認
- 04Handle を使用できる条件の説明
- 05GC 問題の診断ヒント
活用シーン
- 01GC オブジェクトを扱う C++ コードの作成
- 02GC セーフポイントをまたぐポインターのレビュー
- 03CallResult と例外処理の確認
- 04識別子テーブルのアサーションの調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ facebook/ hermes --skill gc-safe-codingインストール後、Claude Code で/gc-safe-codingを入力して読み込まれたか確認します。
詳細
完全な説明と根拠については、doc/GCSafeCoding.mdを参照してください。
GC セーフポイント
GC セーフポイントとは、GC ヒープの割り当て、または間接的にそれに到達する可能性のある関数呼び出しです(mallocなどの通常のCヒープ割り当てはセーフポイントではありません)。Runtime &またはPointerBase &を受け取る関数は、特に記載がない限り、または名前に_noalloc/_nogcが付いていない限り、GCを発生させる可能性があります。_RJSサフィックスの付いた関数はJavaScriptを再帰的に呼び出すため、必ずGCを発生させます。
GCセーフポイントの前に、GCオブジェクトへのすべての生ポインターとPseudoHandleをルート化する必要があります。 PseudoHandle<T>はルートではありません。セーフポイントをまたぐ場合、生ポインターと同じように危険です。同じことは、一意化されていないソースから取り出した単独のSymbolID値にも当てはまります(たとえば、新しく割り当てられたStringPrimitiveに対してgetSymbolHandleFromPrimitiveが返すHandle<SymbolID>から取り出したSymbolID)。それをルートするものがなくなると、スイープ中にfreeUnmarkedSymbolsによってルックアップテーブルのスロットが解放されます。PinnedValue<SymbolID>を使ってピン留めしてください。
ローカル値のルート化:Locals + PinnedValueを使用する(新しいコードでは必須)
新しいコードでは、必ずLocals + PinnedValue<T>を使用してください。新たにGCScopeインスタンスやmakeHandle()呼び出しを追加しないでください。
struct : public Locals {
PinnedValue<JSObject> obj;
PinnedValue<StringPrimitive> str;
PinnedValue<SymbolID> sym;
PinnedValue<> genericValue;
} lv;
LocalsRAII lraii(runtime, &lv);
代入パターン
- PseudoHandleから:
lv.obj = std::move(*callResult); - 型が既知のHermesValueから:
lv.obj.castAndSetHermesValue<JSObject>(hv); - 生ポインターから:
lv.obj = somePtr; - クリア:
lv.obj = nullptr; - テンプレートコンテキストで:
lv.obj.template castAndSetHermesValue<T>(hv);
関数への引き渡し
PinnedValue<T>はHandle<T>に暗黙的に変換されます。Handle<T>を受け取る関数には、そのまま渡してください。
CallResultによるエラー処理
値を使用する前に、必ず例外の有無を確認してください。
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.obj = std::move(*result);
Handleの使用が問題ない場合(指摘しない)
Handle<>の使用をすべてPinnedValueに変換する必要があるわけではありません。「GCScopeではなくLocalsを使用する」というルールが適用されるのは、新しいルート値の作成、つまりmakeHandle()やHandle<>コンストラクターによる新しいPinnedHermesValueスロットの割り当てです。
以下は新しいハンドルを割り当てるものではないため、変換する必要はありません。
vmcast<>(handle)— 既存のハンドルを別の型にキャストします。Runtime &を受け取らず、GCScopeスロットも割り当てません。結果は入力と同じPinnedHermesValueを指します。args.getArgHandle(n)— レジスタースタック内を指すハンドルを返します。レジスタースタックはすでにルートです。新しい割り当てはありません。Handle<>パラメーターの受け渡しまたは受け取り — ハンドルは呼び出し元によって割り当てられており、呼び出し先はそれを使用するだけです。
新しいPinnedHermesValueスロットが(makeHandle()、makeMutableHandle()、またはRuntime &を受け取るHandle<>/MutableHandle<>コンストラクターを通じて)割り当てられている場合に限り、ハンドルの使用を指摘してください。
GC セーフなコードを書く/レビューするためのチェックリスト
- GC セーフポイントをまたいで生ポインターや PseudoHandle を保持しないでください。
PseudoHandle<T>に保持された値を含め、GC オブジェクトへのすべてのポインターは、Runtime &を受け取る呼び出しや_RJSの呼び出しの前にPinnedValueに格納する必要があります。複数段階で作成するパターンに注意してください。Foo::create()がPseudoHandleを返し、次の行でBar::create(runtime)を呼び出す場合、2 回目の割り当て後、最初のPseudoHandleは無効になります。 同様に、逆参照によるキャプチャにも注意してください。auto *x = vmcast<T>(*pinned)は、ピン留めされた場所から生ポインター(例:napi_valueなどのPinnedHermesValue *)を取り出します。ピン留めされたスロットは GC セーフなままですが、ローカルの生ポインターはそうではありません。使用箇所ごとに*pinnedを再度逆参照するか、PinnedValue<T>を使ってピン留めしてください。 - GCScope ではなく Locals を使ってください。 新しいコードでは
GCScopeやmakeHandle()を導入しないでください。PinnedValueフィールドを持つstruct : public Localsを宣言し、LocalsRAIIを使ってください。 - すべての CallResult を確認してください。 事前に
== ExecutionStatus::EXCEPTIONを確認せずにCallResultを逆参照してはいけません。 - ローカルルートから Handle を返さないでください。 破棄される
PinnedValueまたはGCScope内を指すHandle<T>を返さないでください。代わりにCallResult<PseudoHandle<T>>またはCallResult<HermesValue>を返してください。 - null プロトタイプを確認してください。 プロトタイプチェーンをたどるときは、
castAndSetHermesValueを呼び出す前に null かどうかを確認してください。 - Locals を使えばループも安全です。
PinnedValueフィールドは反復ごとに再利用されるため、無制限に増加することはありません。Handleを返すレガシー API のためにGCScopeが引き続き必要な場合は、GCScopeMarkerRAIIまたはflushToMarkerを使ってください。 - Handle は最上位の GCScope に割り当てられます。
makeHandle()、makeMutableHandle()、Handle<>およびMutableHandle<>のコンストラクター、ならびにRuntime &/PointerBase &を受け取りHandle<>を返す関数の呼び出しは、すべて最上位のGCScopeにスロットを割り当てます。Handle を作成または受け取るものの、返さない関数には、独自のGCScopeまたはGCScopeMarkerRAII(1 つか 2 つの Handle であればこちらを推奨)が必要です。Runtime &を受け取らないvmcast<>などの関数は、既存の Handle を割り当てなしでキャストするだけです。 flushToMarkerはマーカーの後に割り当てられた Handle を無効にします。 そのような Handle から取り出した値(生ポインター、単独のSymbolIDなど)は、フラッシュ後にルートされなくなります。その値が後で必要になる場合は、フラッシュする前にPinnedValueにピン留めしてください。
デバッグのヒント
IdentifierTable::materializeLazyIdentifierが(entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy"をアサートする場合、ほとんどの場合、そのエントリーはフリーリストのスロットです。呼び出しスタックを調べ、割り当てをまたいで保持されている、ルートされていないSymbolIDを探してください。