mcpField
G
gc-safe-coding

gc-safe-coding

github.com/facebook開発11.4k47 回インストール今日更新
リポジトリ

gc-safe-coding について

Hermes JavaScript エンジンの C++ コードを作成・レビューする開発者向けの GC セーフティガイドです。セーフポイント、値のルート化、例外確認、ローカル値の管理を扱います。

主な機能

  1. 01GC セーフポイントとオブジェクトのルート化
  2. 02新規コードで Locals と PinnedValue を使用
  3. 03CallResult の例外確認
  4. 04Handle を使用できる条件の説明
  5. 05GC 問題の診断ヒント

活用シーン

  1. 01GC オブジェクトを扱う C++ コードの作成
  2. 02GC セーフポイントをまたぐポインターのレビュー
  3. 03CallResult と例外処理の確認
  4. 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 セーフなコードを書く/レビューするためのチェックリスト

  1. 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> を使ってピン留めしてください。
  2. GCScope ではなく Locals を使ってください。 新しいコードでは GCScope や makeHandle() を導入しないでください。PinnedValue フィールドを持つ struct : public Locals を宣言し、LocalsRAII を使ってください。
  3. すべての CallResult を確認してください。 事前に == ExecutionStatus::EXCEPTION を確認せずに CallResult を逆参照してはいけません。
  4. ローカルルートから Handle を返さないでください。 破棄される PinnedValue または GCScope 内を指す Handle<T> を返さないでください。代わりに CallResult<PseudoHandle<T>> または CallResult<HermesValue> を返してください。
  5. null プロトタイプを確認してください。 プロトタイプチェーンをたどるときは、castAndSetHermesValue を呼び出す前に null かどうかを確認してください。
  6. Locals を使えばループも安全です。 PinnedValue フィールドは反復ごとに再利用されるため、無制限に増加することはありません。Handle を返すレガシー API のために GCScope が引き続き必要な場合は、GCScopeMarkerRAII または flushToMarker を使ってください。
  7. Handle は最上位の GCScope に割り当てられます。 makeHandle()、makeMutableHandle()、Handle<> および MutableHandle<> のコンストラクター、ならびに Runtime &/PointerBase & を受け取り Handle<> を返す関数の呼び出しは、すべて最上位の GCScope にスロットを割り当てます。Handle を作成または受け取るものの、返さない関数には、独自の GCScope または GCScopeMarkerRAII(1 つか 2 つの Handle であればこちらを推奨)が必要です。Runtime & を受け取らない vmcast<> などの関数は、既存の Handle を割り当てなしでキャストするだけです。
  8. flushToMarker はマーカーの後に割り当てられた Handle を無効にします。 そのような Handle から取り出した値(生ポインター、単独の SymbolID など)は、フラッシュ後にルートされなくなります。その値が後で必要になる場合は、フラッシュする前に PinnedValue にピン留めしてください。

デバッグのヒント

  • IdentifierTable::materializeLazyIdentifier が (entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy" をアサートする場合、ほとんどの場合、そのエントリーはフリーリストのスロットです。呼び出しスタックを調べ、割り当てをまたいで保持されている、ルートされていない SymbolID を探してください。

コメント