关于 gc-safe-coding
Hermes JavaScript 引擎代码库的 GC 安全编码指南,供编写或审查 C++ 代码的开发者使用。重点说明安全点、根引用、异常检查及局部值管理规则。
主要功能
- 01说明 GC 安全点与对象根引用
- 02新代码使用 Locals 和 PinnedValue
- 03要求检查 CallResult 异常
- 04列出 Handle 使用边界
- 05提供 GC 问题排查提示
适用场景
- 01编写涉及 GC 对象的 C++ 代码
- 02审查跨 GC 安全点的指针使用
- 03检查 CallResult 和异常处理
- 04排查标识符表相关断言
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ facebook/ hermes --skill gc-safe-coding安装后在 Claude Code 中输入/gc-safe-coding确认已加载。
详细介绍
完整说明和理由请参阅 doc/GCSafeCoding.md。
GC 安全点
GC 安全点是指 GC 堆分配,或可能通过传递调用触发 GC 堆分配的函数调用(常规 C 堆分配,例如 malloc,不属于安全点)。除非另有说明,或函数名带有 _noalloc/_nogc,否则任何接受 Runtime & 或 PointerBase & 的函数都可能触发 GC。带有 _RJS 后缀的函数会递归调用 JavaScript,并且始终会触发 GC。
所有指向 GC 对象的裸指针和 PseudoHandle,都必须在任何 GC 安全点之前设为根。 PseudoHandle<T> 不是根——跨越安全点时,它和裸指针一样危险。对从非唯一化来源提取的裸 SymbolID 值也同样适用(例如,从为新分配的 StringPrimitive 返回的 Handle<SymbolID> 中提取的 SymbolID):一旦没有任何对象将其设为根,查找表槽位就会在清扫期间由 freeUnmarkedSymbols 回收。请使用 PinnedValue<SymbolID> 固定它。
为局部值设根:使用 Locals + PinnedValue(新代码必须遵循)
所有新代码都必须使用 Locals + PinnedValue<T>。不要引入新的
GCScope 实例或 makeHandle() 调用。
struct : public Locals {
PinnedValue<JSObject> obj;
PinnedValue<StringPrimitive> str;
PinnedValue<SymbolID> sym;
PinnedValue<> genericValue;
} lv;
LocalsRAII lraii(runtime, &lv);
赋值模式
- 来自 PseudoHandle:
lv.obj = std::move(*callResult); - 来自类型已知的 HermesValue:
lv.obj.castAndSetHermesValue<JSObject>(hv); - 来自裸指针:
lv.obj = somePtr; - 清除:
lv.obj = nullptr; - 在模板上下文中:
lv.obj.template castAndSetHermesValue<T>(hv);
传递给函数
PinnedValue<T> 会隐式转换为 Handle<T>。直接传给接受 Handle<T> 的函数即可。
使用 CallResult 进行错误处理
使用值之前,始终先检查是否有异常:
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.obj = std::move(*result);
可以使用 Handle 的情况(无需标记)
并非所有 Handle<> 的用法都需要转换为 PinnedValue。规则“使用 Locals,而不是 GCScope”适用于创建新的根值——通过 makeHandle() 或 Handle<> 构造函数分配新的 PinnedHermesValue 槽位。
以下情况不会分配新的句柄,无需转换:
vmcast<>(handle)——将现有句柄转换为其他类型。它不会接受Runtime &,也不会分配 GCScope 槽位。结果指向与输入相同的PinnedHermesValue。args.getArgHandle(n)——返回一个指向寄存器栈的句柄,该栈已经是根。不会发生新分配。- 传递或接收
Handle<>参数——该句柄由调用方分配;被调用方只是使用它。
仅当分配新的 PinnedHermesValue 槽位时,才标记句柄的用法(通过 makeHandle()、makeMutableHandle(),或接受 Runtime & 的 Handle<>/MutableHandle<> 构造函数)。
编写/审查 GC 安全代码的检查清单
- 不要在 GC 安全点之间保留裸指针或 PseudoHandle。 指向 GC 对象的每个指针——包括
PseudoHandle<T>中持有的值——都必须在任何接受Runtime &的调用或_RJS调用之前存入PinnedValue。 留意多步创建模式:如果Foo::create()返回一个PseudoHandle,而下一行调用Bar::create(runtime),那么第二次分配后,第一个PseudoHandle就会失效。 也要留意通过解引用捕获指针的情况:auto *x = vmcast<T>(*pinned)会从固定位置(例如napi_value这样的PinnedHermesValue *)提取裸指针。固定槽位仍然是 GC 安全的,但局部裸指针不是。每次使用时都重新解引用*pinned,或通过PinnedValue<T>固定。 - 使用 Locals,而不是 GCScope。 新代码不得引入
GCScope或makeHandle()。声明一个带有PinnedValue字段的struct : public Locals,并使用LocalsRAII。 - 检查每个 CallResult。 未先检查
== ExecutionStatus::EXCEPTION,绝不要解引用CallResult。 - 绝不要从局部根返回 Handle。 不要返回指向即将销毁的
PinnedValue或GCScope的Handle<T>。改为返回CallResult<PseudoHandle<T>>或CallResult<HermesValue>。 - 检查空原型。 遍历原型链时,在调用
castAndSetHermesValue之前检查是否为空。 - 使用 Locals 时,循环是安全的。 每次迭代都会复用
PinnedValue字段,不会无限增长。如果旧版 API 仍要求使用GCScope来返回Handle,请使用GCScopeMarkerRAII或flushToMarker。 - Handle 在最顶层的 GCScope 中分配。
makeHandle()、makeMutableHandle()、Handle<>和MutableHandle<>构造函数,以及接受Runtime &/PointerBase &并返回Handle<>的函数调用,都会在最顶层的GCScope中分配槽位。不返回 Handle、但会创建或接收 Handle 的函数,需要有自己的GCScope或GCScopeMarkerRAII(一个或两个 Handle 时优先使用后者)。像vmcast<>这样不接受Runtime &的函数只会转换现有 Handle,不会分配。 flushToMarker会使在标记之后分配的 Handle 失效。 从这类 Handle 中提取的任何值(裸指针、未封装的SymbolID)在刷新后都不再受根保护。如果之后还需要该值,请在刷新 之前 将其固定到PinnedValue中。
调试提示
- 如果
IdentifierTable::materializeLazyIdentifier断言(entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy", 那么该条目通常是一个空闲列表槽位——请检查调用堆栈,查找在分配期间未受根保护的SymbolID。