语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载Hermes 是一款面向 React Native 的 JavaScript 引擎其运行时中大量实体JS 对象、隐藏类、属性表等都分配在由垃圾收集器GC托管的内存堆中GC 既会回收不再被引用的对象也会因堆压缩compaction等原因自动搬移对象。这意味着 C 代码持有的裸指针随时可能失效——这正是本文要解决的核心问题。本指南以 Hermes 官方文档 doc/GCSafeCoding.md 为骨架结合引擎源码深入讲解 GC safepoint 规则、Handle/PseudoHandle/PinnedValue等根root类型的设计原理与实操模式帮助你写出既正确又高性能的 GC 安全原生代码。为什么 GC 让 C 编码变难在 Hermes 中GC 负责管理 GC 堆中所有实体的生命周期当一个对象不再被任何根引用时它会被回收当发生堆压缩时对象会被搬到新地址。因此C 代码正在访问的 GC 对象可能被释放use-after-free或被搬移dangling pointer。编写正确且高性能的 GC 安全代码必须深刻理解 GC 的触发时机与根root机制。GC 安全原生代码的根本规则GC 只能在GC safepoint安全点判断对象是否不可达、或搬移对象。没有这一保证编写 GC 安全的 C 代码将无从谈起。因此GC 安全代码的根本要求是C 代码使用的所有指向 GC 托管实体的活指针在 GC safepoint 之前必须存放在 GC 已知的位置并在 safepoint 之后从这些位置重新加载。存放在 GC 已知位置能确保 GC 认为这些对象可达而不会释放它们safepoint 之后重新加载能保证若 GC 搬移了对象原生代码拿到的是更新后的指针值。该规则必须被 C 代码严格执行——所有指向堆值的指针都必须在 safepoint 前存储、safepoint 后重新加载。实践中值通常活在 GC 已知的位置C 代码每次使用时加载编译器足够聪明若底层值不可能改变会优化掉连续加载。识别 GC safepoint命名约定与参数类型在 Hermes 代码库中一个 GC safepoint 要么是一次分配allocation要么是一次可能传递性到达 safepoint 的函数调用——并非所有 C 调用都能到达 safepoint。代码库通过命名约定、参数类型和显式文档注释来传递这一信息带Runtime 或PointerBase 参数两者都能访问 GC的函数默认被认为可以到达 GC safepoint除非显式文档说明或函数名带_noalloc、_nogc后缀。带_RJS后缀的函数可能递归调用 JavaScriptRecursive JavaScript因此必然到达 GC safepoint。例如JSObject::getNamed_RJS()、toString_RJS()、toNumber_RJS()。既不接受Runtime /PointerBase 、也没有_RJS后缀的函数通常在不保护指针的情况下调用是安全的。判断一个函数是否可能触发 GC 是 GC 安全编码的第一步只要某个函数可能到达 safepoint调用前就必须确保所有活指针都已被扎根rooted。GC 根Roots与堆值Heap Values的区别GC 需要起点来发现哪些对象是存活的这个起点就是根集合roots——指向 GC 托管对象、但存活在 GC 堆之外、且 GC 被显式告知的位置。根包括寄存器栈register stack——字节码解释器使用的 VM 操作数栈Locals——通过LocalsRAII注册的栈上分配的PinnedValue字段GCScope 链——由GCScope管理的动态分配的PinnedHermesValue槽位legacyRuntime 字段——Runtime对象内部本身持有的PinnedHermesValue字段如众所周知的原型对象arrayPrototype、objectPrototype等。这些根都不在 GC 堆中它们存活在 C 栈或Runtime的固定位置。GC 了解每一类根并在回收时全部遍历——见 Runtime::markRoots() 的实现它按 RootSectionRegisters、Locals、Runtime 字段、SymbolRegistry 等分阶段标记其中 Locals 段会遍历vmLocals链表中每个Locals结构的所有PinnedHermesValue{ MarkRootsPhaseTimer timer(*this, RootAcceptor::Section::Locals); acceptor.beginRootSection(RootAcceptor::Section::Locals); for (Locals *locals vmLocals; locals; locals locals-prev) { for (size_t i 0, e locals-numLocals; i e; i) { auto phv locals-locals()[i]; // ... 标记每个 PinnedHermesValue 为根 } } acceptor.endRootSection(); }从这些根出发GC 追溯它找到的每一个指针。GC 堆中的一个对象存活当且仅当它从至少一个根传递可达任何不可达对象都可以被释放。若 GC 搬移对象堆压缩它会更新自己知道的所有指针——既包括根也包括存于其他 GC 堆对象内部的指针。对 C 代码而言关键洞察是如果你持有指向 GC 对象的裸指针且该指针没有存放在根中GC 就不知道它的存在指向的对象可能被释放use-after-free或被搬移悬垂指针。这就是为什么所有活指针在任意 GC safepoint 前必须存入根。HermesValueGC 操作的基本值类型HermesValue是暴露给 GC 的基本值类型——它是一个 NaN-boxed 的 64 位值可以容纳 JS 原始值数字、布尔、undefined、null、symbol更重要的是可以容纳指向 GC 托管对象的指针。定义见 include/hermes/VM/HermesValue.h并有static_assert保证其 trivial 性以便高效复制。GC 基于HermesValue工作利用 NaN-boxing 的 tag 位判断每个值是指针还是原始值。当值是指针时GC 追溯它并在所指对象被搬移时更新它。PinnedHermesValue扎根的语义标记PinnedHermesValue是HermesValue的子类其唯一目的是表明这个HermesValue实例是一个GC 根——它存活在不属于 GC 堆因此 GC 不会搬移它且被 GC 作为根知晓的内存中。源码注释明确写道A HermesValue which is stored in non-moveable memory and is known to the garbage collector见 HermesValue.h。需要注意声明为PinnedHermesValue并不会神奇地自动注册为根——它只是语义标记如果一个值存放在 GC 堆之外并通过Locals、GCScope或Runtime字段注册为根就应把它声明为PinnedHermesValue来传达其根性。PinnedHermesValue对编写 GC 安全代码至关重要前文提到的GC 已知的位置全部是PinnedHermesValue根。概念上C 代码在 safepoint 前把所有需要的 GC 对象指针存入PinnedHermesValuesafepoint 后重新加载。实践中存在建立在PinnedHermesValue之上的便捷 C 抽象如PinnedValue、Handle。Handle 与 MutableHandle传递 GC 值的标准方式HandleT是指向不可变PinnedHermesValueconst PinnedHermesValue *的指针包装其中PinnedHermesValue已知包含类型T的值number、bool、JSObject等。大多数内部 API 接受Handle而非裸指针。Handle即HandleHermesValue是未类型化的句柄可容纳任意HermesValueMutableHandleT与HandleT类似但指向可变的PinnedHermesValue允许更新存储的值HandleT可平凡复制它只是一个指针设计上按值传递重要特性PinnedValueT可隐式转换为HandleT因此函数接受HandleT时可以直接传入PinnedValueT。PseudoHandle未扎根的临时值PseudoHandleT持有 GC 托管值但不通过根保护它。它是 move-only 类型——一旦被移走原对象即失效调试模式下访问失效的PseudoHandle会触发断言见 Handle.h 中invalidate()与带断言的get()。PseudoHandle的存在是为了性能很多情况下函数产生一个值后调用者会立刻把它存入根。将值包装进PseudoHandle就在类型系统中编码了该值未扎根必须在任何 GC safepoint 前存到安全位置这一语义。许多内部 API 因此返回CallResultPseudoHandleT。PseudoHandle的常见用法// 存入 PinnedValue移动并失效 PseudoHandle lv.obj std::move(*result); // 转换为 Handle在 GCScope 中分配 —— legacy auto handle runtime.makeHandle(std::move(pseudoHandle));CallResult 与异常处理CallResultT是可能抛出 JS 异常的操作的标准返回类型它要么是类型T的值要么是ExecutionStatus::EXCEPTION。典型用法auto result someOperation_RJS(runtime, args); if (LLVM_UNLIKELY(result ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } // 使用 *result 或 result.getValue() 获取值。 lv.obj std::move(*result);大多数可能触发 GC 的函数返回CallResultPseudoHandleT或CallResultHermesValue。调用者必须在使用值前检查异常并且必须在下一个 GC safepoint 前把值存入根。GCScope 与 GCScopeMarkerRAIILegacy APIGCScope是基于 RAII 的可变大小PinnedHermesValue容器总是以栈式方式实例化。它把自己维持在一个单链表中——该链表的头为 GC 所知。GC 在回收时爬取所有GCScope使其中存储的所有PinnedHermesValue成为根。从源码看HandleRootOwner.hGCScope内部按CHUNK_SIZE 16的块分配句柄槽位最初使用内联存储耗尽后再通过SmallVector动态分配新块块本身永不移动。它是一个高效动态容器但为防止无限增长配置了固定上限调试构建默认HERMESVM_DEBUG_MAX_GCSCOPE_HANDLES即文档所述的 48 个句柄可通过构造函数第三参handlesLimit覆盖不想设限传UINT_MAX。典型用法是在函数入口声明GCScope按需在内部分配值保留句柄函数退出时GCScope自动销毁。重要GCScope中分配的值在作用域销毁后绝不可再使用。这个错误很少见但会发生——典型场景是试图返回一个分配在即将销毁的GCScope中的Handle。隐式 GCScope 分配runtime.makeHandle()与runtime.makeMutableHandle()声明见 HandleRootOwner.h以及Handle/MutableHandle构造函数会隐式在最顶层即最近创建的GCScope中分配一个槽位。这意味着如果一个函数创建了Handle/MutableHandle且不把它们返回给调用者它很可能需要自己的GCScope或者至少一个GCScopeMarkerRAII在不再需要时释放槽位——只分配一两个句柄时优先用GCScopeMarkerRAII如果函数调用返回Handle的 API这些句柄同样分配在最顶层GCScope中逻辑同上。注意这只适用于接受Runtime 或PointerBase 的函数——只有它们能访问GCScope并分配新槽位像vmcast这样不接收这些参数的函数只是转换已有句柄不分配新槽位。没有局部GCScope或GCScopeMarkerRAII句柄会累积在调用者的GCScope中导致其超出槽位上限。循环与 GCScopeMarkerRAII 的问题Legacy使用GCScope时一个常见问题是在循环中分配句柄导致扎根的PinnedHermesValue槽位可能无限增长——这正是GCScope设置句柄上限的原因。一个解决方案是在循环体内创建GCScope可行但偏重——GCScope会预分配 16 个句柄的空间且每次迭代都要在链表中注册/注销自己。GCScopeMarkerRAII是轻量替代方案创建时保存GCScope状态销毁时释放之后分配的所有句柄。典型模式是放在循环体顶部GCScope gcScope(runtime); // ... for (...) { GCScopeMarkerRAII marker(gcScope); // 此处分配的句柄在每次迭代结束时释放。 }或者更高效在循环外创建 marker显式调用flush()GCScope gcScope(runtime); // ... auto marker gcScope.createMarker(); for (...) { gcScope.flushToMarker(marker); // ... }Locals 与 PinnedValue 推荐的现代 API所有新代码必须使用LocalsPinnedValueT代替GCScopemakeHandle()来为局部 GC 值扎根。不要引入新的GCScope实例或makeHandle()调用现有代码正在逐步迁移。Locals配合PinnedValueT是为局部 GC 值创建扎根存储的首选方式。HandleT仍是函数间传递 GC 值的标准类型——PinnedValueT可隐式转换为HandleT两者天然协作。工作原理声明一个继承自Locals的匿名结构体为每个需要保持存活的 GC 值声明PinnedValueT字段然后创建LocalsRAII把该结构体注册到 runtime。LocalsRAII将结构体压入链表runtime.vmLocalsGC 时Runtime::markRoots()遍历该链表将每个Locals结构中的每个PinnedHermesValue标记为根。从源码看Runtime.hLocalsRAII构造时会计算numLocals(sizeof(T) - Locals::localsOffset()) / sizeof(PinnedHermesValue)把locals-prev指向旧的vmLocals再把自己压栈析构时要求严格逆序LocalsRAII must be destroyed in the reverse order of creation并弹栈。在HERMES_SLOW_DEBUG下构造时还会断言所有指针/symbol 类型的 PinnedValue 已初始化为默认值防止 GC 访问悬垂指针。// Locals 结构体存活在 C 栈上。PinnedValue 字段是 // PinnedHermesValueGC 会把它们标记为根。 struct : public Locals { PinnedValueJSObject obj; PinnedValueStringPrimitive str; PinnedValue genericValue; // 未类型化——可容纳任意 HermesValue } lv; LocalsRAII lraii(runtime, lv);为什么 Locals 优于 GCScopeGCScope HandleLocals PinnedValue动态分配PinnedHermesValue槽位字段是结构体的一部分——无动态分配HandleT是指向PinnedHermesValue的指针——一层间接PinnedValueT本身就是PinnedHermesValue——直接访问容易在循环中意外分配句柄需要GCScopeMarkerRAII字段集合固定——循环不会导致无界增长每个作用域 48 句柄上限调试断言无限制——字段数量编译期决定赋值模式// 从 PseudoHandle 赋值CallResult 之后的典型场景 lv.obj std::move(*callResult); // 从 CallResult 中已知为特定类型的 HermesValue 赋值 lv.obj.castAndSetHermesValueJSObject(callResult.getValue()); // 从裸指针赋值 lv.obj someJSObjectPtr; // 从 GCPointer 赋值 lv.obj someGCPointer.get(runtime); // 清空避免不必要地保持对象存活 lv.obj nullptr;将 PinnedValue 传给期望 Handle 的函数PinnedValueT可隐式转换为HandleT因此可以直接传给接受句柄的函数struct : public Locals { PinnedValueJSObject O; } lv; LocalsRAII lraii(runtime, lv); lv.O.castAndSetHermesValueJSObject(*objRes); // getPrototypeOf 接受 HandleJSObject而 PinnedValueJSObject 可转换 return getPrototypeOf(runtime, lv.O);从 PinnedValue 读取// 获取底层类型化值 JSObject *rawPtr lv.obj.get(); // 或 *lv.obj JSObject *rawPtr *lv.obj; // 解引用访问成员 lv.obj-someMethod(); // 作为 HermesValue 获取 HermesValue hv lv.obj.getHermesValue();这些接口与源码一一对应Handle.h 中PinnedValue提供了get()、operator*、operator-、getHermesValue()、castAndSetHermesValue()、clear()等。注意PinnedValue的拷贝/移动构造函数被删除以避免意外按值传递PinnedValue——其位置必须始终为 GC 所知。模板函数中的 template 关键字在模板上下文中调用PinnedValue的成员函数模板如castAndSetHermesValueT时由于PinnedValue类型是依赖类型dependent typeC 要求在成员名之前使用template关键字template typename T void doSomething(Runtime runtime, HermesValue value) { struct : public Locals { PinnedValueT obj; } lv; LocalsRAII lraii(runtime, lv); // 因为 PinnedValueT 是依赖类型必须写 template lv.obj.template castAndSetHermesValueT(value); }缺少template关键字时编译器会把解析为小于运算符而非模板实参列表的开始导致编译错误。这条规则适用于在依赖类型参数的PinnedValue上调用任何模板成员函数。真实案例arrayConstructor来自 lib/VM/JSLib/Array.cpp 的arrayConstructor完整展示了 Locals PinnedValue 的实战形态CallResultHermesValue arrayConstructor(void *, Runtime runtime) { NativeArgs args runtime.getCurrentFrame().getNativeArgs(); struct : public Locals { PinnedValueJSObject selfParent; PinnedValueJSArray self; } lv; LocalsRAII lraii(runtime, lv); if (LLVM_LIKELY(!args.isConstructorCall() || ...)) { CallResultPseudoHandleJSArray selfRes JSArray::create(runtime, runtime.arrayPrototype); if (LLVM_UNLIKELY(selfRes ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; lv.self std::move(*selfRes); } else { CallResultPseudoHandleJSObject thisParentRes NativeConstructor::parentForNewThis_RJS(runtime, ...); if (LLVM_UNLIKELY(thisParentRes ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; lv.selfParent std::move(*thisParentRes); auto arrRes JSArray::create(runtime, lv.selfParent); if (LLVM_UNLIKELY(arrRes ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; lv.self std::move(*arrRes); } // ...函数其余部分都使用 lv.self ... return lv.self.getHermesValue(); }注意其中的模式每个::create()结果都先检查异常再立即通过std::move(*res)存入PinnedValue扎根之后无论发生多少次分配lv.self都是安全的。Locals 与循环Locals天然解决了循环问题——PinnedValue字段是固定集合每次迭代直接复用即可无需GCScopeMarkerRAII。不过部分现有代码会为那些返回Handle的 API需要GCScope在Locals旁搭配一个GCScopeGCScopeMarkerRAII。此时Locals持有长生命周期值GCScope管理 API 调用产生的短生命周期临时值struct : Locals { PinnedValueJSObject O; PinnedValue elem; PinnedValueStringPrimitive sep; } lv; LocalsRAII lraii(runtime, lv); GCScope gcScope(runtime); // ... auto marker gcScope.createMarker(); for (uint32_t i 0; i len; gcScope.flushToMarker(marker), i) { // 循环内创建的临时句柄每次迭代都被冲刷。 // 存储在 lv.* 中的长生命周期值跨迭代保持。 auto strRes toString_RJS(runtime, lv.elem); if (LLVM_UNLIKELY(strRes ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; lv.sep std::move(*strRes); }常见错误与规避在 GC safepoint 间持有裸指针// 错误——分配之后 rawPtr 可能悬垂 JSObject *rawPtr someHandle-getObject(); auto newObj JSObject::create(runtime); // GC safepoint! rawPtr-doSomething(); // rawPtr 可能已无效 // 正确——存入 PinnedValuesafepoint 后重新加载 struct : public Locals { PinnedValueJSObject obj; } lv; LocalsRAII lraii(runtime, lv); lv.obj someHandle-getObject(); auto newObj JSObject::create(runtime); // GC safepoint lv.obj-doSomething(); // 安全PinnedValue 是根在 GC safepoint 间持有 PseudoHandlePseudoHandleT不是GC 根——它和裸指针一样持有未扎根的值在 GC safepoint 之后使用同样危险。这很容易被忽略因为PseudoHandle看起来像个安全的智能指针但它并不扎根。尤其要警惕多步创建模式——两次::create()调用连续发生时// 错误——第二次 create() 后 ctor PseudoHandle 已过期 auto ctor NativeConstructor::create(runtime, ...); auto proto JSObject::create(runtime); // GC safepoint —— ctor 已过期 lv.ctor std::move(ctor); // 太晚了——ctor 已经过期 // 正确——在下一次分配前就把 PseudoHandle 扎根 lv.ctor NativeConstructor::create(runtime, ...); // 立即扎根。 lv.proto JSObject::create(runtime); // GC safepoint —— lv.ctor 安全。这类 bug 往往难以察觉因为过期值大多数时候仍然有效——GC 只在堆压缩罕见时搬移对象。启用HERMESVM_SANITIZE_HANDLESASAN 构建默认开启可以通过在每次分配后搬移堆来确定性捕获这类 bug。返回指向已销毁 GCScope 或 Locals 的 Handle// 错误——Handle 指向已销毁的 GCScope HandleJSObject createThing(Runtime runtime) { GCScope gcScope(runtime); auto handle runtime.makeHandle(JSObject::create(runtime)); return handle; // 此处 gcScope 销毁——handle 悬垂 } // 错误——返回指向即将销毁的 PinnedValue 的 Handle HandleJSObject createThing(Runtime runtime) { struct : public Locals { PinnedValueJSObject obj; } lv; LocalsRAII lraii(runtime, lv); lv.obj ...; return lv.obj; // 此处 lraii 销毁——Handle 悬垂 }正确做法是返回PseudoHandle复制出值本身而非指向根的指针// 正确——返回 PseudoHandle CallResultPseudoHandleJSObject createThing(Runtime runtime) { struct : public Locals { PinnedValueJSObject obj; } lv; LocalsRAII lraii(runtime, lv); lv.obj ...; return PseudoHandleJSObject::create(*lv.obj); } // 正确——写入调用者提供的 MutableHandle void createThing(Runtime runtime, MutableHandleJSObject result) { // 无需本地扎根——直接写入调用者的根。 auto res JSObject::create(runtime); result res.get(); } // 正确——返回 HermesValue调用者扎根它有风险 CallResultHermesValue createThing(Runtime runtime) { struct : public Locals { PinnedValueJSObject obj; } lv; LocalsRAII lraii(runtime, lv); lv.obj ...; return lv.obj.getHermesValue(); }忘记检查异常// 错误——未检查异常就使用值 auto result someOperation_RJS(runtime, args); lv.obj std::move(*result); // 可能解引用一个异常 // 正确 auto result someOperation_RJS(runtime, args); if (LLVM_UNLIKELY(result ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; lv.obj std::move(*result);原型链遍历的空原型处理遍历原型链时始终检查 nullauto protoRes JSObject::getPrototypeOf(obj, runtime); if (LLVM_UNLIKELY(protoRes ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; if (!*protoRes) { lv.O nullptr; // 原型链终点 } else { lv.O.castAndSetHermesValueJSObject(protoRes-getHermesValue()); }小结GC 安全编码决策速查识别 safepoint接受Runtime /PointerBase 的函数、带_RJS后缀的函数、任何分配操作都可能触发 GC。一切活指针都要扎根safepoint 前存入PinnedValue现代 API或GCScope句柄legacysafepoint 后重新加载。PseudoHandle不是根它是未扎根的临时值必须在下一次 safepoint 前存入根。新代码用LocalsPinnedValue固定字段、无动态分配、无句柄上限天然规避循环增长问题GCScope仅用于兼容 legacy 代码或调用返回Handle的 API。异常先行任何返回CallResult的调用都要先检查EXCEPTION再取值。不要返回指向局部根的Handle改用PseudoHandle、调用者提供的MutableHandle或HermesValue。善用调试工具HERMESVM_SANITIZE_HANDLES与 ASAN 构建能在开发期确定性暴露跨 safepoint 的悬垂句柄问题。遵循这些规则你的 C 代码就能在 Hermes 的 GC 环境中既正确又高效地运行。相关文档与源码可继续参考 doc/GCSafeCoding.md、include/hermes/VM/Handle.h、include/hermes/VM/HandleRootOwner.h 与 lib/VM/Runtime.cpp。赞分享语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载相关推荐Hermes 引擎 GC 安全 C 编码实践从 GC safepoint 到 Locals PinnedValue 的完整指南Hermes 引擎 GC 安全 C 编码实践从 GC safepoint 到 Locals PinnedValue 的完整指南 导读 本文基于 Her语言运行时编译器移动开发Numba jit与njit完全指南快速掌握延迟编译与急切编译的正确姿势Numba jit与njit完全指南快速掌握延迟编译与急切编译的正确姿势 NumPy 加速神器 Numba 是一款使用 LLVM 的动态 Python 编编译器高性能计算curl_multi_add_handle 详解将 easy handle 加入 multi 会话的正确姿势curl_multi_add_handle 详解将 easy handle 加入 multi 会话的正确姿势 导读 curl_multi_add_handleCLI网络通信创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
