mcpField
G
gc-safe-coding

gc-safe-coding

github.com/facebook开发11.4k47 次安装今天更新
代码仓库

关于 gc-safe-coding

Hermes JavaScript 引擎代码库的 GC 安全编码指南,供编写或审查 C++ 代码的开发者使用。重点说明安全点、根引用、异常检查及局部值管理规则。

主要功能

  1. 01说明 GC 安全点与对象根引用
  2. 02新代码使用 Locals 和 PinnedValue
  3. 03要求检查 CallResult 异常
  4. 04列出 Handle 使用边界
  5. 05提供 GC 问题排查提示

适用场景

  1. 01编写涉及 GC 对象的 C++ 代码
  2. 02审查跨 GC 安全点的指针使用
  3. 03检查 CallResult 和异常处理
  4. 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 安全代码的检查清单

  1. 不要在 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> 固定。
  2. 使用 Locals,而不是 GCScope。 新代码不得引入 GCScope 或 makeHandle()。声明一个带有 PinnedValue 字段的 struct : public Locals,并使用 LocalsRAII。
  3. 检查每个 CallResult。 未先检查 == ExecutionStatus::EXCEPTION,绝不要解引用 CallResult。
  4. 绝不要从局部根返回 Handle。 不要返回指向即将销毁的 PinnedValue 或 GCScope 的 Handle<T>。改为返回 CallResult<PseudoHandle<T>> 或 CallResult<HermesValue>。
  5. 检查空原型。 遍历原型链时,在调用 castAndSetHermesValue 之前检查是否为空。
  6. 使用 Locals 时,循环是安全的。 每次迭代都会复用 PinnedValue 字段,不会无限增长。如果旧版 API 仍要求使用 GCScope 来返回 Handle,请使用 GCScopeMarkerRAII 或 flushToMarker。
  7. Handle 在最顶层的 GCScope 中分配。 makeHandle()、makeMutableHandle()、Handle<> 和 MutableHandle<> 构造函数,以及接受 Runtime &/PointerBase & 并返回 Handle<> 的函数调用,都会在最顶层的 GCScope 中分配槽位。不返回 Handle、但会创建或接收 Handle 的函数,需要有自己的 GCScope 或 GCScopeMarkerRAII(一个或两个 Handle 时优先使用后者)。像 vmcast<> 这样不接受 Runtime & 的函数只会转换现有 Handle,不会分配。
  8. flushToMarker 会使在标记之后分配的 Handle 失效。 从这类 Handle 中提取的任何值(裸指针、未封装的 SymbolID)在刷新后都不再受根保护。如果之后还需要该值,请在刷新 之前 将其固定到 PinnedValue 中。

调试提示

  • 如果 IdentifierTable::materializeLazyIdentifier 断言 (entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy", 那么该条目通常是一个空闲列表槽位——请检查调用堆栈,查找在分配期间未受根保护的 SymbolID。

评论

支持 Markdown