
Gc Safe Coding
- 40 installs
- 11.2k repo stars
- Updated August 4, 2026
- facebook/hermes
Helps with ai & agent building tasks.
About
gc-safe-coding is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- gc-safe-coding
- AI & Agent Building
- AI-coding skill
Gc Safe Coding by the numbers
- 40 all-time installs (skills.sh)
- Ranked #8,266 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/facebook/hermes --skill gc-safe-codingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 11.2k |
| Last updated | August 4, 2026 |
| Repository | facebook/hermes ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
1. 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>. 2. Use Locals, not GCScope. New code must not introduce GCScope or makeHandle(). Declare a struct : public Locals with PinnedValue fields and a LocalsRAII. 3. Check every CallResult. Never dereference a CallResult without first checking == ExecutionStatus::EXCEPTION. 4. 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. 5. Null prototype checks. When traversing prototype chains, check for null before calling castAndSetHermesValue. 6. 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. 7. Handles allocate in the topmost GCScope. 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. 8. `flushToMarker` invalidates handles allocated after the marker. Any value 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.
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 unrooted SymbolID held across an allocation.