ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Hermes VM 架构详解:值表示、运行时与对象模型

Hermes VM 架构详解:值表示、运行时与对象模型 Hermes VM 架构详解值表示、运行时与对象模型【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址: https://gitcode.com/gh_mirrors/hermes/hermes本篇技术指南围绕 Hermes 引擎一个为 React Native 优化的 JavaScript 引擎的虚拟机VM核心展开系统讲解其值表示HermesValue/HermesValue32/ 字符串、运行时Runtime、RuntimeModule、Domain、IdentifierTable以及基于 VTable 的对象模型并给出 GC 安全编码与 REPL 使用的实战指引。读完本文你将理解 Hermes 如何用 NaN-boxing 把类型和指针塞进一个 64 位寄存器、如何通过压缩指针降低堆内存、以及 GC 句柄体系为何是编写 VM 内代码的正确姿势。值表示Value RepresentationHermesValue一个寄存器大小的 JS 值VM 的核心诉求之一是让 JS 值能紧凑地塞进一个寄存器。Hermes 用名为HermesValue的类来完成这一封装——它在保留类型信息的同时仍然只是一个uint64_t可以直接在寄存器间传递、入栈。其底层编码方式称为NaN-taggingNaN 装箱值存放在uint64_t的低位比特中。当这个uint64_t被当作double解释时凡是 IEEE 754 规定的 NaN 值都拥有大量可自由使用的有效位因此带 tag 的 NaN 就可以承载非 double 类型的数据。具体做法是VM 预留出标准的 quiet NaN静默 NaN作为 NaN 的表示形态之后还剩下51 个可自由设置的比特位。即使在 64 位系统上实际指针地址最多只用 48 位所以这 51 位足以同时装下一个完整指针和一个类型 tag。在 include/hermes/VM/HermesValue.h 中可以看到Tag枚举定义了 16 位的类型标签First、EmptyInvalid、UndefinedNull、BoolSymbol以及指针类标签FirstPointer、Str、BigInt、ObjectkNumDataBits表示高 16 位之外的可用数据位数kDataMask用于在取指针时把 tag 位清零validatePointer()专门断言指针高位不为零确保指针能安全编码进数据区类尾部有static_assert(std::is_trivialHermesValue::value, ...)从编译期保证HermesValue是平凡类型可以自由复制进出寄存器与调用栈。一个HermesValue可以承载以下类型值它们靠类型 tag 区分Empty空值通常表示槽位未使用UndefinedundefinedNullnullBoolean布尔值Symbol符号Native value用于簿记的原生值可存一个 int 或指针编码后“看起来像一个 double”String pointer字符串指针Object pointer对象指针注意 BigInt 在现代源码中也被纳入了指针类标签Tag::BigInt与文档中描述的类型集合一脉相承。HermesValue在 VM 中广泛用于存储和传递 JS 值但在不同内存位置使用不同的派生形式PinnedHermesValue存放在不可移动non-moveable的内存中主要用于寄存器栈以及 GC 已知位置的根root引用。它的赋值运算符直接调用setNoBarrier()不做写屏障因为该内存不会被 GC 搬移。GCHermesValue存放在 GC 托管堆上。对它的set()赋值必须携带GC 参数并执行写屏障针对大对象支持 large allocation还有专门的GCHermesValueInLargeObj变体以及GCHermesValueUtil::copyToPinned()用于把堆上值拷回 JS 栈。此外 include/hermes/VM/HermesValueTraits.h 维护着一张IsGCObject白名单JSObject、JSArray、JSFunction、StringPrimitive、Domain、HiddenClass、Callable等只有名单内的类型才允许被HermesValue以对象指针形式管理这是类型系统层面的安全约束。HermesValue32压缩指针下的 32 位紧凑编码当开启压缩指针compressed pointers时部分值会用紧凑的 32 位表示称为HermesValue32。把指针压缩进这种格式需要额外工作因此 Hermes 刻意避免在频繁访问的值如寄存器栈上使用它而是选择性地用于那些占用堆内存比例大、但对性能不敏感的“大块头”对象上从而在内存占用与访问开销之间取得平衡。HermesValue32支持HermesValue的全部类型唯独去掉了 native value——因为原生指针可能指向 GC 托管堆之外压缩机制无法覆盖它们且这类值相对罕见。与 NaN 装箱不同HermesValue32利用Hermes 堆的 8 字节对齐来存放 tag指针的低 3 位在压缩后依然恒为零正好用来存放标签。HermesValue32对 double 有特殊处理最多29 位的小整数可以直接内联存储无法用该形式表示的 double必须作为独立的 double 对象放到堆上根据 Hermes 观察到的实际负载真正的 double 使用频率很低因此这种间接存储的开销可以接受。字符串StringsJS 字符串在 VM 中由StringPrimitive表示它存储不可变的 UTF16 编码字符串StringPrimitive *可以放进HermesValue构成 JS String 值。其内部实现分两类见 include/hermes/VM/StringPrimitive.hDynamicStringPrimitive存储在 GC 堆中的动态字符串ExternalStringPrimitive作为指针存储、指向 VM 外部例如直接指向字节码文件内的字符串区可以避免复制。在 include/hermes/VM/CellKinds.def 的 CellKind 枚举中能看到更细的划分DynamicUTF16StringPrimitive、DynamicASCIIStringPrimitive、ExternalUTF16StringPrimitive、ExternalASCIIStringPrimitive、BufferedUTF16StringPrimitive等字符串根据字符宽度ASCII/UTF16、存储位置动态/外部/缓冲分别对应不同的 cell 类型。运行时RuntimeRuntime类是 VM 的主驱动器位于 include/hermes/VM/Runtime.h。它持有当前执行环境environment与堆heap要执行的代码全局对象、IdentifierTable、属性缓存、调试器等基础设施。Runtime负责执行RuntimeModule后者由BytecodeModule通过Runtime::runModule()构造而来。可以认为编译器把 JS 源码编成字节码模块BytecodeModuleVM 侧再把它“实例化”成RuntimeModule供解释器执行。RuntimeModule字节码文件的 VM 表示RuntimeModule是字节码文件在 VM 中的运行时表示它存放在 GC 堆之外通过new构造因此不属于 GC 管理对象。从 include/hermes/VM/RuntimeModule.h 可以看到它的职责拥有一个BytecodeModule并从中复制字符串 ID 映射表stringIDMap_把字节码里的顺序字符串 ID 映射为SymbolID和函数映射表每个CodeBlock都持有指向所属RuntimeModule的引用JSFunction创建/销毁时会通过CodeBlock更新RuntimeModule的引用计数所有关联同一Runtime的RuntimeModule会被串进一个链表便于统一做内存管理任务RuntimeModuleFlags中的persistent位控制模块是否常驻内存永不释放这用于懒标识符场景——其字符串内容直接指向字节码模块的字符串存储区因此不能释放但懒编译模块不能设置该标志因为BytecodeModule可能在底层被替换。Domain字节码文件的分段与模块集合为了支持字节码文件分段segmentation以及跨段requireHermes 用Domain类把一组RuntimeModule收集在一起。可以这样理解Domain就是同一次编译器调用中编译出来的全部字节码文件的集合。关键的所有权关系见 include/hermes/VM/Domain.h每个JSFunction共享同一个Domain的所有权Domain拥有提供这些函数的RuntimeModule因此当所有依赖某个Domain内文件的JSFunction都被回收时该Domain及其RuntimeModule也会随之被回收——RuntimeModule只对Domain持弱引用WeakRootDomain而JSFunction对Domain持强 GC 指针。从源码看Domain本身是一个GCCellclass Domain final : public GCCell它还承担了CommonJS 模块表的管理cjsModules_ArrayStorage按索引存储各模块的cachedExports、module 对象、函数索引cjsModuleTable_DenseMapSymbolID, uint32_t支持基于文件名的慢速require解析requireFast则直接按索引访问。RequireContext继承JSObject作为require()调用时的this携带当前Domain与dirname用于路径解析。Runtime Identifiers标识符表Runtime内含一张IdentifierTable用于为字符串分配唯一 ID。它的核心用途是在StringPrimitive与SymbolID之间双向转换见 include/hermes/VM/IdentifierTable.h表中每个字符串的顺序整数索引是一个 32 位值SymbolID::RawType外部以轻量包装类SymbolID使用限制不安全操作SymbolID在 VM 中被高效地用作属性名和字符串字面量的编码表启动时预填充一组“预定义字符串”predefined strings即内置函数必需的字符串集合定义在PredefinedStrings.def中对应源码文件为 include/hermes/VM/PredefinedStrings.def 与 include/hermes/VM/PredefinedStringIDs.h。垃圾收集Garbage Collection当前 VM 默认使用HadesGC——一种并发垃圾收集器目标是把相比上一代收集器 GenGC 大幅降低的停顿时间落到实处详见 Hades 文档。其关键设计堆非连续non-contiguous避免预先保留大块地址空间并能在更细粒度上把内存归还给操作系统精确式 GCpreciseGC 始终知道哪些值包含有效指针不需要在堆里扫描“疑似指针”。由于收集器会搬移对象从而让HermesValue失效VM 提供了几类自动更新的机制Handle与HandleTGC 感知的句柄若两次连续访问之间发生了一次回收句柄会被自动更新到对象新位置。因此 VM 内正确性的前提是用句柄代替在函数间直接传递裸HermesValueGCScope在栈上构造负责跟踪当前所有HermesValue句柄按 chunk 分配空间析构离开作用域时释放其分配的所有 chunk它内部生成PinnedHermesValue再存入Handle/HandleTPseudoHandleT显式地不是句柄。它用于明确表示存储的是裸指针或HermesValue作为参数传给“可能需要把该参数转成Handle、但不必每次都付出句柄分配成本”的函数。PseudoHandle没有拷贝构造函数move 出去即失效从而防止在一次分配性函数调用之后被重复使用。GC 安全编码规则编写正确且高性能的 GC 安全 C 代码涉及句柄用法、错误处理模式与常见陷阱完整的规范请阅读 GCSafeCoding.md其中也包含更推荐的LocalsPinnedValueAPI。对象模型Object ModelHermes 当前采用基于 VTable 的方案所有可能的 JS 值都继承自一个基础的 GC VTable。这些对象统称cell全部 cell 类型定义在 include/hermes/VM/CellKinds.def 中CELL_KIND(...)宏展开出一系列CellKind枚举值。对象有专门的ObjectVTable可调用对象Callable有CallableVTable等等。对象Objects每个 JS 对象由Object或其派生类表示。JS 对象拥有一组名/值对name/value pairs以及可选的“索引存储”indexed storage。Object的完整工作机制可阅读ObjectModel.h对应头文件为 include/hermes/VM/JSObject.h 与 include/hermes/VM/ObjectModel.h后者位于 include/hermes/VM/ 目录。Runtime内还有一个全局对象global object用来承载全局作用域。数组Arrays数组、arguments对象等直接继承自Object它们只是通过 VTable 各自实现了*OwnIndexed系列方法索引读写因此并不需要一套独立的数组对象体系。函数Functions函数与原生函数native functions继承自Callable。Callable暴露了executeCall*系列方法让 VM 可以用内部 API 统一运行 JS 函数与宿主注入的原生函数。JSFunction字节码函数与NativeFunction都挂在这棵继承树上。装箱基本类型Boxed PrimitivesVM 中有专门的类来容纳通过对应 JS 构造函数构造出来的Boolean、String、Number例如JSString就是装箱后的 String 对象。这些装箱对象JSBoolean、JSString、JSNumber见 include/hermes/VM/PrimitiveBox.h 与HermesValueTraits.h中的白名单使 JS 语义如new String(x)返回对象得以忠实实现。REPL交互式求值HermesVM 在bin/hermes中提供了 REPL交互模式下直接调用Runtime的全局eval()函数执行用户输入。从 tools/hermes/CMakeLists.txt 可以看到hermes可执行文件由hermes.cpp与repl.cpp组成链接hermesvm_a与libreadline。行为细节tools/hermes/hermes.cpp 中当没有传入任何输入文件时cl::InputFilenames.size() 0进入repl(getReplRuntimeConfig())启动交互会话tools/hermes/repl.cpp 提供--prompt等命令行选项默认提示符为tools/hermes/evaluate-line.js 是 REPL 的“行求值器”作为字符串字面量在编译期嵌入 C运行时对每一行输入做求值并美化打印结果还预先把全局Promise保存下来防止用户在 REPL 里重绑定后破坏内部逻辑。延伸阅读Hades.md默认并发垃圾收集器的工作原理GenGC.md上一代分代收集器的设计GCSafeCoding.mdGC 安全编码完整指南Design.mdHermes 整体设计概览Modules.md模块系统与分段加载机制。【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址: https://gitcode.com/gh_mirrors/hermes/hermes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表