| 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);
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 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 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 &).
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
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>.
- Use Locals, not GCScope. New code must not introduce
GCScope or
makeHandle(). Declare a struct : public Locals with PinnedValue fields
and a LocalsRAII.
- Check every CallResult. Never dereference a
CallResult without first
checking == ExecutionStatus::EXCEPTION.
- Never return Handle from local roots. Do not return
Handle<T> pointing
into a PinnedValue or GCScope that is about to be destroyed. Return
CallResult<PseudoHandle<T>> or CallResult<HermesValue> instead.
- Null prototype checks. When traversing prototype chains, check for null
before calling
castAndSetHermesValue.
- Loops are safe with Locals.
PinnedValue fields are reused each
iteration — no unbounded growth. If a GCScope is still needed for legacy
APIs that return Handle, use GCScopeMarkerRAII or flushToMarker.
- Handles allocate in the topmost GCScope.
makeHandle(),
makeMutableHandle(), Handle<> and MutableHandle<> constructors, and
calls to functions that take Runtime &/ and return
, all allocate a slot in the topmost . Functions that
create or receive handles without returning them need their own or
(preferred for one or two handles). Functions like
that do not take just cast existing handles without
allocating.
Debugging tips
- If
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.