
gc-safe-coding
✓ Official★ 11,200by facebook · part of facebook/hermes
For the full explanation and rationale, see doc/GCSafeCoding.md .
For the full explanation and rationale, see doc/GCSafeCoding.md .
Inspect the full instructions your agent will receiveExpandCollapse
This is the exact playbook injected into your agent when the skill activates — shown here so you can audit it before installing. You don't need to read it to use the skill.
name: gc-safe-coding description: > 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/.
For the full explanation and rationale, see doc/GCSafeCoding.md.
GC safepoints
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>.
Rooting local values: use Locals + PinnedValue (required for new code)
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);Assignment patterns
- From PseudoHandle:
lv.obj = std::move(*callResult); - From HermesValue with known type:
lv.obj.castAndSetHermesValue<JSObject>(hv); - From raw pointer:
lv.obj = somePtr; - Clear:
lv.obj = nullptr; - In template context:
lv.obj.template castAndSetHermesValue<T>(hv);
Passing to functions
PinnedValue<T> implicitly converts to Handle<T>. Pass directly to functions
that accept Handle<T>.
Error handling with CallResult
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);Checklist for writing / reviewing GC-safe code
- No raw pointers or PseudoHandles across GC safepoints. Every pointer to
a GC object — including values held in
PseudoHandle<T>— must be stored in aPinnedValuebefore any call that takesRuntime &or is_RJS. Watch for multi-step creation patterns: ifFoo::create()returns aPseudoHandleand the next line callsBar::create(runtime), the firstPseudoHandleis 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., aPinnedHermesValue *such as anapi_value). The pinned slot stays GC-safe, but the local raw pointer does not. Re-deref*pinnedat each use site, or pin viaPinnedValue<T>. - Use Locals, not GCScope. New code must not introduce
GCScopeormakeHandle(). Declare astruct : public LocalswithPinnedValuefields and aLocalsRAII. - Check every CallResult. Never dereference a
CallResultwithout first checking== ExecutionStatus::EXCEPTION. - Never return Handle from local roots. Do not return
Handle<T>pointing into aPinnedValueorGCScopethat is about to be destroyed. ReturnCallResult<PseudoHandle<T>>orCallResult<HermesValue>instead. - Null prototype checks. When traversing prototype chains, check for null
before calling
castAndSetHermesValue. - Loops are safe with Locals.
PinnedValuefields are reused each iteration — no unbounded growth. If aGCScopeis still needed for legacy APIs that returnHandle, useGCScopeMarkerRAIIorflushToMarker. - Handles allocate in the topmost GCScope.
makeHandle(),makeMutableHandle(),Handle<>andMutableHandle<>constructors, and calls to functions that takeRuntime &/PointerBase &and returnHandle<>, all allocate a slot in the topmostGCScope. Functions that create or receive handles without returning them need their ownGCScopeorGCScopeMarkerRAII(preferred for one or two handles). Functions likevmcast<>that do not takeRuntime &just cast existing handles without allocating. flushToMarkerinvalidates handles allocated after the marker. Any value extracted from such a Handle (raw pointer, bareSymbolID) is unrooted after the flush. Pin into aPinnedValuebefore the flush if the value is needed later.
Debugging tips
- If
IdentifierTable::materializeLazyIdentifierasserts(entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy", the entry is most often a free-list slot — look up the call stack for an unrootedSymbolIDheld across an allocation.
npx skills add https://github.com/facebook/hermes --skill gc-safe-codingRun this in your project — your agent picks the skill up automatically.
When Handle usage is fine (do not flag)
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 does not takeRuntime &and does not allocate a GCScope slot. The result points to the samePinnedHermesValueas the input.args.getArgHandle(n)— returns a handle pointing into the register stack, which is already a root. No new allocation.- Passing or receiving a
Handle<>parameter — the handle was allocated by the 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 &).
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.