facebook/gc-safe-coding
> Rules for writing and reviewing GC-safe C++ code in the Hermes VM runtime. Use when writing, modifying, or reviewing C++ runtime VM code that uses internal Hermes VM APIs (as opposed to code using JSI). This includes working with GC-managed types (HermesValue, Handle, PinnedValue, JSObject, StringPrimitive, etc.), Locals, GCScope, PseudoHandle, CallResult, or any function with _RJS suffix. Typically in lib/VM/, include/hermes/VM/, API/hermes/, or API/napi/.
npx skills add https://github.com/facebook/hermes --skill gc-safe-coding
For the full explanation and rationale, see doc/GCSafeCoding.md.
A GC safepoint is either a GC heap allocation or a function call that might
transitively reach one (regular C heap allocations like malloc are not
safepoints). Any function that takes Runtime & or PointerBase &
may trigger GC, unless documented otherwise or named with _noalloc/_nogc.
Functions with _RJS suffix invoke JavaScript recursively and always trigger
GC.
**All raw pointers and PseudoHandles to GC objects must be rooted before any
GC safepoint.** PseudoHandle<T> is *not* a root — it is just as dangerous as
a raw pointer across a safepoint. The same applies to bare SymbolID values
extracted from a non-uniqued source (e.g., the SymbolID pulled out of the
Handle<SymbolID> returned by getSymbolHandleFromPrimitive for a
freshly-allocated StringPrimitive): once nothing roots it, the lookup-table
slot is reclaimed by freeUnmarkedSymbols during sweep. Pin via
PinnedValue<SymbolID>.
All new code must use Locals + PinnedValue<T>. Do not introduce new
GCScope instances or makeHandle() calls.
struct : public Locals {
PinnedValue<JSObject> obj;
PinnedValue<StringPrimitive> str;
PinnedValue<SymbolID> sym;
PinnedValue<> genericValue;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.obj = std::move(*callResult);lv.obj.castAndSetHermesValue<JSObject>(hv);lv.obj = somePtr;lv.obj = nullptr;lv.obj.template castAndSetHermesValue<T>(hv);PinnedValue<T> implicitly converts to Handle<T>. Pass directly to functions
that accept Handle<T>.
Always check for exceptions before using the value:
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.obj = std::move(*result);
Not every use of Handle<> needs to be converted to PinnedValue. The rule
"use Locals, not GCScope" applies to creating new rooted values — allocating
new PinnedHermesValue slots via makeHandle() or Handle<> constructors.
The following are not allocating new handles and do not need conversion:
vmcast<>(handle) — casts an existing handle to a different type. It doesnot take Runtime & and does not allocate a GCScope slot. The result points
to the same PinnedHermesValue as the input.
args.getArgHandle(n) — returns a handle pointing into the registerstack, which is already a root. No new allocation.
Handle<> parameter — the handle was allocated bythe caller; the callee is just using it.
Only flag handle usage when a new PinnedHermesValue slot is being
allocated (via makeHandle(), makeMutableHandle(), or Handle<>/
MutableHandle<> constructors that take Runtime &).
a GC object — including values held in PseudoHandle<T> — must be stored in
a PinnedValue before any call that takes Runtime & or is _RJS.
Watch for multi-step creation patterns: if Foo::create() returns a
PseudoHandle and the next line calls Bar::create(runtime), the first
PseudoHandle is stale after the second allocation.
Equally watch for capture-via-deref: auto *x = vmcast<T>(*pinned) extracts
a raw pointer from a pinned location (e.g., a PinnedHermesValue * such as
a napi_value). The pinned slot stays GC-safe, but the local raw pointer
does not. Re-deref *pinned at each use site, or pin via PinnedValue<T>.
GCScope ormakeHandle(). Declare a struct : public Locals with PinnedValue fields
and a LocalsRAII.
CallResult without firstchecking == ExecutionStatus::EXCEPTION.
Handle<T> pointinginto a PinnedValue or GCScope that is about to be destroyed. Return
CallResult<PseudoHandle<T>> or CallResult<HermesValue> instead.
before calling castAndSetHermesValue.
PinnedValue fields are reused eachiteration — no unbounded growth. If a GCScope is still needed for legacy
APIs that return Handle, use GCScopeMarkerRAII or flushToMarker.
makeHandle(),makeMutableHandle(), Handle<> and MutableHandle<> constructors, and
calls to functions that take Runtime &/PointerBase & and return
Handle<>, all allocate a slot in the topmost GCScope. Functions that
create or receive handles without returning them need their own GCScope or
GCScopeMarkerRAII (preferred for one or two handles). Functions like
vmcast<> that do not take Runtime & just cast existing handles without
allocating.
flushToMarker invalidates handles allocated after the marker. Anyvalue extracted from such a Handle (raw pointer, bare SymbolID) is
unrooted after the flush. Pin into a PinnedValue *before* the flush if
the value is needed later.
IdentifierTable::materializeLazyIdentifier asserts(entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy",
the entry is most often a free-list slot — look up the call stack for an
unrooted SymbolID held across an allocation.
Take facebook/gc-safe-coding from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.