gc-safe-coding

For the full explanation and rationale, see doc/GCSafeCoding.md.

facebook/hermes34 installsMITSynced Aug 25

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI

Agent Skills format with YAML frontmatter. Claude Code reads it as-is.

---
name: "gc-safe-coding"
description: "For the full explanation and rationale, see doc/GCSafeCoding.md."
license: "MIT"
---

For the full explanation and rationale, see [doc/GCSafeCoding.md](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.

```cpp
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:

```cpp
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::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.

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY