尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
从命令行到主循环:Hermes引擎源码执行链路全解析
1. 从命令行出发Hermes到底是怎么跑起来的标题里写了“从命令行到一次对话”这个“对话”其实指的是引擎和你的交互过程——你敲下一行hermes xx.js引擎从读取源码、编译成字节码到最终执行并打印结果整个链条就是一次完整的“对话”。很多人一上来就埋头读lib/VM里的上万行代码很容易迷路。我的建议是反过来先顺着入口往下走搞清楚一条执行链上的骨架再回头补血肉。先给不熟悉的同学交代一下背景Hermes 是一个针对移动端优化的 JavaScript 引擎主打启动快、内存占用低很多跨端框架拿它做脚本引擎。它支持直接跑 JS 源码也能跑预先编译好的 HBC 字节码文件还带一个 REPL 交互模式。源码以 C 为主目录结构大致是lib/核心实现、include/对外头文件、tools/命令行工具、API/JS 和 C API 层。今天我们只干一件事找到入口顺着主循环走一遍。适合谁看想读引擎源码但不知道从哪下手的同学或者工作中需要定位为什么脚本没跑起来这类问题的同学。不需要提前精通 C能看懂函数调用和基本指针就行遇到关键的模板和智能指针我会顺手解释。2. 入口文件定位别急着翻代码先看命令行怎么分发2.1 命令行工具的注册与参数解析Hermes 的命令行入口不在lib/里而在tools/hermes目录下主文件叫Main.cpp。这符合一个工程化项目的常规布局核心库保持纯净命令行只是外层壳。你调用hermes命令时操作系统找到可执行文件进入int main(int argc, char **argv)这里就是源头。入口做了三件基础工作解析参数llvm::cl风格的选项注册根据参数模式执行文件/执行语句/进入 REPL走不同分支构造运行时并进入执行流程参数解析是重中之重。Hermes 用了一套声明式的选项注册比手写getopt要清晰得多每个选项对应一个全局变量。比如-O控制优化等级-dump-bytecode控制是否导出字节码-exec直接执行一行语句-repl进入交互模式。源码里你会看到一大片cl::opt的定义你完全可以把它理解为“命令行选项的登记表”。这里有个容易忽略的点参数解析的时机。不是main一进来就解析而是先做一些平台级的环境初始化比如设置 locale、处理信号再进解析。因为某些解析选项本身依赖环境状态。第一次读源码的时候不必纠结这些细节你只需要记住main拿到argc/argv后面所有行为都来自这里的“选择”。2.2 三种执行模式的差异和选择逻辑顺着解析完的参数往下走入口通常会根据输入参数进入三条路径直接执行脚本文件hermes app.js最常用执行内联语句hermes -exec print(hi)适合快速验证交互式 REPLhermes -repl一行一行喂给引擎这三条路径最终的落点不同但对新手来说理解它们的差异能帮你快速定位代码。直接执行文件最省事读文件、编译、执行、退出。执行内联语句则是把字符串当作源码直接编译。REPL 最复杂它需要一个“会话状态”的概念——全局对象要持续保留每次输入都基于上一次的执行结果继续。这就是标题里“一次对话”的本质REPL 的循环就是对话循环每一轮输入都是一次完整的编译加执行。我建议读入口时按这个顺序先看“直接执行文件”分支因为最短再跳进 REPL 分支理解主循环最后回头来看-exec因为这个分支恰好是“文件执行”和“REPL”的中间形态。2.3 main 函数的最小骨架伪代码下面这段伪代码不是逐行搬源码而是我提炼后的执行骨架方便你对照真实代码时心里有底int main(int argc, char **argv) { // 1. 初始化命令行选项 cl::ParseCommandLineOptions(argc, argv, Hermes\n); // 2. 根据参数选择执行模式 if (ShowHelp) { cl::PrintHelpMessage(); return 0; } if (!Repl) { // 文件执行或语句执行 return executeScript(argc, argv); } else { // REPL 模式进入主循环 return runRepl(argc, argv); } }注意真实的main里还有异常捕获、退出码归一化等细节。但你照着上面这个骨架去读基本能一路走到runRepl或executeScript。这俩函数才是真正的“对话发起者”。3. 主循环在哪里别在 Main.cpp 里死等很多朋友以为主循环会出现在main函数里找半天找不到。实际上main在完成模式分发后很快就把控制权交给了运行时。我们把“主循环”拆成两层来理解REPL 层的对话循环在工具层代码里表现为while (true)等待用户输入VM 层的执行循环在解释器里表现为for (;;)不断取指令、执行、跳转今天的标题重点讲“一次对话”所以 REPL 层的循环是我们观察的对象但真正干活的是 VM 层那个“取指-解码-执行”的循环。你可以这样理解REPL 循环是对话的“外壳”VM 解释器循环是对话的“内核”。一次对话发生时外壳循环每次收到一行输入都会委托内核循环把这一行执行完再回到外壳等待下一行。3.1 REPL 循环的实现位置REPL 循环通常不在Main.cpp里而是在tools/hermes/下的一个独立文件里比如Repl.cpp。它的大致逻辑是void runRepl() { // 初始化运行时 auto runtime HermesRuntime::create(...); // 准备全局对象 runtime-global(); while (true) { std::string line readLine(); // 读取一行输入 if (line quit || line exit) break; auto result runtime-executeScript(line); // 编译并执行这一行 // 如果结果是值类型打印出来 if (result.isValue()) { printValue(result.getValue()); } } }真实代码里还会处理多行输入花括号没闭合就继续读、异常回显、断点调试等但这些都不影响主骨架。你要抓住的关键模式是读一行执行一行显示结果再读一行。这就是“一次对话”的标准流程。读到这里你会发现入口层的工作比想象中少得多大部分时间它只是在“翻译用户意图”。真正的编译和执行发生在executeScript内部。3.2 解释器主循环的基本形态当executeScript把源码字符串交给编译器产出 HBC 字节码后运行时拿到字节码创建一个RuntimeModule最终在解释器里开始执行。解释器主循环的框架大致长这样for (;;) { Opcode opcode (Opcode)*ip; switch (opcode) { case Opcode::LoadConstString: // 处理加载字符串 break; case Opcode::Call: // 处理函数调用 break; case Opcode::Ret: // 处理返回 return; // 还有几百个 opcode... } }实际上 Hermes 的解释器早就不是这么朴素的switch了它用了 computed goto 和线程化解释器性能更好但阅读逻辑上你可以先当成switch看。理解这一步很重要主循环的本质就是不停地把“指令指针”往后推每推一次就执行一条字节码。4. 深入 executeScript编译与执行的完整链路4.1 从源码字符串到字节码我们以最简单的print(hello)为例。当你在命令行执行hermes -exec print(hello)编译链路依次是词法分析把字符串切成一串 token语法分析token 变成 AST抽象语法树语义分析做变量绑定、类型检查字节码生成AST 变成 HBC 指令流在 Hermes 里前几步通常由前端解析器完成位于lib/Parser和lib/AST字节码生成在lib/Bytecode。你会在源码里看到parseJS、generateBytecode之类的函数名。为了提升启动速度Hermes 允许直接加载预编译的.hbc文件跳过前三步。这时executeScript会走一条简化的加载路径。在实际执行脚本时运行时通常会先调用接口将字符串传入编译器std::string source print(hello);; auto compiled hermes::compileSource(source, interactive.js);compileSource返回的compiled对象包含字节码缓冲区和可能的警告/错误信息。这一步要注意编译的错误不等于执行的异常。语法错误在编译阶段就暴露运行时异常比如调用不存在的方法才在执行阶段抛出。看清这一点排查问题时就能少走弯路。4.2 运行时初始化与全局对象挂载执行字节码之前必须先有“环境”。Hermes 的运行时环境核心是一个Runtime对象它管理着堆、调用栈、全局对象、内建函数表等。初始化时引擎会往全局对象上挂载内建对象和函数——print、Object、Array、Math等。你调用print时看起来是个普通函数实际上它指向的是一个 C 原生函数。挂载全局对象不是一个for循环全部搞定的事而是分模块完成的。比如Math单独一个初始化函数JSON单独一个String.prototype上的一堆方法也是逐个定义。源码里会看到大量defineProperty和putNativeFunction调用它们本质上是往一个JSObject的“符号表”里加条目。这里有个新手容易踩的坑全局对象的“符号表”容量是预留的。Hermes 为了性能在创建全局对象时会预先分配一定数量的属性槽位避免运行期反复扩容。如果往全局对象上挂的属性过多触发了扩容性能会下降。这也是为什么源码里初始化的顺序不能乱调每个模块挂完属性后最好保持“槽位有余量”的状态。4.3 调用栈与 CallFrame对话的“记忆”执行字节码时最核心的结构是调用栈CallStack而栈上的每个单元叫CallFrame。可以把它类比成一次对话中的“上下文快照”当前执行到哪个函数、指令指到哪一行、局部变量存在哪、返回地址是什么全部打包在一个CallFrame里。当发生函数调用时引擎会压入一个新的CallFrame函数返回时弹出这个CallFrame恢复调用者的现场。解释器主循环里频繁操作的ip指令指针、sp栈指针、fp帧指针全都基于CallFrame的布局。读源码时你会看到类似这样的一段逻辑伪代码CallFrame *newFrame CallFrame::pushFrame(runtime, func); newFrame-setIP(entryPoint); newFrame-setThisVMOwner(runtime);这就是一次函数调用的开始。理解CallFrame之后你就理解了执行上下文切换的本质这对后续读字节码调试器、异常处理、垃圾回收都有帮助。4.4 executeScript 的最终落点把上面的块拼起来executeScript的完整流程可以概括为将源码交给编译器拿到字节码创建或复用RuntimeModule通过运行时的入口将首个字节码块的入口地址交给解释器解释器主循环开始运转直到执行到返回指令把最终结果包装成一个JSValue/HermesValue返回给调用层到这一步“一次对话”就走完了。你敲下的那行 JS经过编译、初始化环境、压栈、执行、出栈最终以返回值的形式回到命令行界面。理解这个链路后读任何一段 Hermes 源码都有了坐标轴。5. 核心数据结构与关键名词速查开始深入前先把后面会高频碰到的名词表列出来。我看源码时有一段时间被这些名词绕得头晕后来自己做了个速查表现在也分享给你名词作用类比Runtime管理堆、栈、全局对象、GC操作系统的进程HermesValue一种紧凑的值表示可存数字、指针、标记位一个带类型的联合体CallFrame一次函数调用的栈帧记录对话中的“当前语境”RuntimeModule一个字节码文件的运行时表示一个加载进内存的“可执行文件”CodeBlock某个函数对应的字节码块一个函数编译后的“机器指令”JSObjectJS 对象的内部表示含属性表一个带哈希表的字典容器Handle指向 GC 管理堆对象的“安全指针”一个防止被垃圾回收器搬走的遥控器其中Handle特别值得单独说两句。C 世界里的裸指针会被对象移动搞得失效Hermes 的 GC 可能压缩堆、搬动对象所以运行时中你很少直接持有裸JSObject*而是通过Handle来引用。你看到auto obj HandleJSObject::vmcast(value)这种代码时别以为它只是类型转换它背后还有 GC 安全性的考量。6. 命令行标志如何影响执行流程命令行参数不是只用来“选模式”的它还直接影响编译策略和执行行为。我在调试 Hermes 相关问题时经常用下面这几组参数定位性能或语义差异6.1 优化与调试类标志-O/-O0控制优化等级。优化等级不同生成的字节码差异非常大。比如尾调用优化TCO、函数内联inlining都会在-O下开启导致调用栈形态完全不同。如果你在看字节码时发现“怎么回事明明没递归栈却这么深”先检查优化等级。-g生成调试信息包括行号映射。这个标志对排查“源码行对应字节码哪一条”极其重要配合-dump-bytecode一起使用效果更好。-dump-bytecode输出字节码的反汇编。想看解释器到底执行了什么这是第一手资料。-l/-dump-ast输出 AST。适合验证解析器的行为。6.2 执行模式相关标志-exec直接执行后面的字符串。-repl进入交互模式。-x内部实验性参数一般不用。-rt/-gcGC 相关配置调堆参数时再动。有个技巧值得分享当你怀疑某个 bug 是“优化后的字节码行为不对”还是“源码语义理解不对”时用-O0和-O各跑一遍对比字节码差异问题往往水落石出。字节码级的信息远比运行时日志直观。7. 现场实操从零跟踪一次对话的完整过程这一节我会带你走一遍“实战流程”你可以直接照着操作在自己的机器上复现“从命令行到一次对话”的全过程。环境假设是 Linux/macOSHermes 已构建好可执行文件在build/bin/hermes。7.1 步骤一用最长链路监控执行先准备一个测试脚本function add(a, b) { return a b; } print(add(2, 3));然后执行hermes -O0 -dump-bytecode test.js你会看到终端输出一长串字节码。别急着全看先找两个关键点函数add对应的CodeBlock名和Call指令。字节码里会有一行类似Call2或Call1的指令后跟操作数寄存器索引。这个过程帮助你理解“函数调用在字节码里长什么样”。7.2 步骤二验证 REPL 主循环接着进入 REPLhermes -repl依次输入var x 1; x 1; function f(y) { return y * 2; } f(x);注意观察第一行没有输出语句没有返回值第二行输出了2第三行没有输出第四行输出了2。这说明 REPL 只在表达式语句有最终值时输出声明语句不输出。这个行为调试时会用到比如你怀疑某行没执行可以先把它改成带副作用的调用。7.3 步骤三用内联语句验证编译链在另一个终端执行hermes -exec print(hello hermes)如果一切正常输出hello hermes。此时你可以加上-dump-bytecode再看一遍输出会看到字符串拼接在字节码层面变成了AddString指令而不是Call这能帮你区分“运行时运算”和“函数调用”的执行路径。7.4 步骤四用调试器打断点如果你从源码编译的 Hermes 带调试信息可以在解释器主循环入口打断点gdb build/bin/hermes (gdb) break lib/VM/JIT.cpp:...实际操作中我更推荐在executeScript系列函数的出入口打断点因为那可以一次性覆盖“编译完成准备执行”和“执行结束准备返回”两个关键节点对初学者最友好。断点打下去后运行hermes -exec 11观察调用栈上的每一帧你会看到main→executeScript→interpretFunction这样一层层的调用关系。这一步跑完之后整个入口到主循环的链路就全部打通了。“命令行”和“对话”之间的关系不再是纸面上的理解。8. 常见问题与排查技巧实录8.1 问题一-repl模式下输入多行函数定义报语法错误很多新手在 REPL 里输入function foo() { return 1; }发现按回车后立刻报错原因很简单REPL 每次只编译一行第一行function foo() {尚未闭合被当成语法错误。解决方法是要么写成单行函数function foo() { return 1; }要么使用支持多行输入的包装命令。排查思路是看 REPL 循环是否有“累积输入直到语法完整”的逻辑。8.2 问题二-dump-bytecode输出太多找不到关键点字节码动辄上千行手翻不现实。我自己的办法是配合-exec只输出一行想要的代码hermes -dump-bytecode -O0 -exec function a(){ return 1 } a()这样输出只有几行一眼就能定位Function定义和Call指令。另外注意-O0和-O的字节码差异巨大排查时不要混看。8.3 问题三执行时出现CallFrame相关崩溃这类崩溃十有八九是调用栈溢出或栈帧布局不匹配。先检查是不是无意识写了死递归function f(x) { return f(x); } f(1);如果代码没问题再把优化等级切换到-O0复现一下。因为优化后的字节码可能复用栈帧崩溃表象和-O0不一样。排查时配合-verbose参数看执行日志通常能定位到具体函数。8.4 问题四想修改全局对象属性但运行时行为不变这大概率是你修改的是“属性定义”不是“属性值”。全局对象上的内建函数比如print是在初始化时绑定好的你后面重新print xxx只是改了全局对象上的同名属性不影响引擎内部已经缓存的函数引用。这是引擎内部缓存机制的正常表现不是 bug。理解这一点后你就知道为什么有些 JS 库的 polyfill 在 Hermes 上不生效了。9. 读完入口后的进阶路线建议如果你已经把从main到executeScript的链路走通下面这几条延伸路线按性价比排序供你参考读字节码反汇编对着-dump-bytecode输出把每条指令和源码对应起来这是最直接的“源码解析”练习读CallFrame的推栈与出栈逻辑理解函数调用的本质涉及lib/VM/CallFrame.h读HermesValue的位布局理解 JS 值在引擎内部是怎么用 64 位表示的很多人第一次看到HermesValue会惊叹“一个数字还能塞这么多标记位”读 GC 的堆遍历理解对象引用关系这能回答“为什么我用裸指针访问对象会崩”的经典问题每一层都有大量可挖的细节但记住一个原则不要试图一次性读完整条调用链的所有代码先按“入口-循环-结构”三点建立框架然后慢慢补血肉。10. 几个我踩过的坑与经验总结最后分享几个读源码过程中实打实的经验。第一源码阅读要配合运行时实验。只看代码很容易陷入“好像懂了其实没懂”。每读一个模块就动手跑一个能触发该模块的小例子用-dump-bytecode验证假设。比如读CallFrame之前先跑一次函数递归调用并打印字节码观察Call指令怎么排列再回来看代码就会顺畅得多。第二不要被模板代码吓住。Hermes 里有大量模板和宏比如解释器的指令分发。初读时你不需要理解每一个模板参数先把注意力放在“这个函数输入什么、输出什么”上。等框架立住了再回头啃模板展开细节。第三多利用反汇编输出作为“路标”。我的习惯是遇到一个执行行为先跑-dump-bytecode拿到字节码再从字节码指令名称反查源码里的Opcode定义最后跳转到对应实现。这个路径比顺着调用链往下追高效得多。第四要习惯把自己当“对话的另一方”。读主循环时想象引擎正在跟你对话你输入一行它编译一帧执行一帧输出一个结果。这个“对话模型”能帮你把抽象的“运行时”具象化。没有谁比你更清楚你敲的每一行 JS 经历了什么。如果你从这篇里只带走一件事那就是入口和主循环是读任何引擎代码的第一站但别指望在第一站解决所有问题。框架立住后面就是水到渠成的事。下一篇我会接着拆HermesValue和CallFrame那是真正理解“对话如何记住上下文”的地方。
RELATED

相关推荐

LobeChat本地部署:Docker、环境变量与反向代理实战

LobeChat本地部署:Docker、环境变量与反向代理实战

1. 先把聊天界面从模型里拆出来,这件事本身就值得做很多人第一次接触本地聊天机器人,顺序是反的:先去折腾模型权重、量化精度、显存占用,结果折腾了三天,模型跑起来了,却只能对着终端敲curl,连个…

📅 2026/10/25 3:44:15
用LLM做科研搭档:指令微调生成可验证研究想法

用LLM做科研搭档:指令微调生成可验证研究想法

1. 项目概述:当文献阅读变成研究创意的“自动播种机”你有没有过这种体验:花一整天精读三篇顶会论文,笔记记了二十页,合上电脑却只记得“好像很有启发”,但具体能延展出什么新问题、新方法、新实验路径,脑子…

📅 2026/10/25 3:44:15
3D高斯泼溅误差分离:Detector、Lifting与Representation三重归因

3D高斯泼溅误差分离:Detector、Lifting与Representation三重归因

1. 项目概述:这不是“调参”,而是一次对3D高斯泼溅底层误差结构的外科手术你最近是不是也刷到了那篇标题里带着“Semantic Lifting”“Detector Error”“Lifting Error”“Representation Error”的论文?标题长、术语密、动词冷峻——“Sepa…

📅 2026/10/25 3:44:15
MORE NEWS

更多资讯

📰

AI编曲工具全解析:低成本实现专业音乐制作

1. 低成本专业编曲的行业现状十年前要搭建一个专业编曲工作室,至少需要投入数万元购置硬件设备和正版音源。如今随着AI技术的发展,一台普通笔记本电脑加上合适的软件工具,就能达到过去专业工作室80%的制作水准。这个变化直接催生了一个新群体…

📰

熬夜修护精华广告文案盲测:人类与ChatGPT的能力边界

这几个月,我的工作台边上一直摆着一台“隐形竞争对手”——ChatGPT。从第一个客户把AI生成的slogan发到群里问我“要不要直接用”开始,我就决定做一件事:把同一个产品简报丢给人,也丢给AI,然后请同行和真实消费者盲测打…

📰

基于YOLOv8的玉米幼苗与杂草检测系统开发指南

1. 项目背景与核心价值在现代化农业生产中,杂草识别与作物监测一直是影响产量的关键因素。传统人工巡查方式效率低下且成本高昂,而基于计算机视觉的AI检测技术正在彻底改变这一局面。我们团队开发的这套玉米幼苗与杂草检测系统,采用当前最先进…

📰

智能体与工作流架构深度拆解:五种模式与工程落地指南

最近这几年,“智能体”这个词几乎被聊烂了,但真正能把架构思路讲明白的文章其实不多。Anthropic 发过一篇《Building Effective Agents》,我前前后后读了不少遍,每经历一个项目回头再看,感受都不一样。这篇笔记不算长&…

📰

PINN在悬臂梁挠度计算中的工程实践

1. 项目背景与核心价值悬臂梁挠度计算是工程力学中的经典问题,传统解法通常依赖有限元分析或解析法。物理信息神经网络(PINN)的出现为这类问题提供了全新解法。我在实际工程仿真项目中发现,PINN方法相比传统FEM具有三大独特优势:无需网格划分…

📰

震动跟着鼓点走:spoti.pw Music Haptics实时音频分析与触觉反馈实战

【免费下载链接】spoti.pw Liquid Glass UI for the Spotify iOS app, no jailbreak 项目地址: https://gitcode.com/gh_mirrors/sp/spoti.pw 点击查看 免费下载 spoti.pw 是一款免越狱的 Spotify iOS 修改插件(基于 Liquid Glass UI 的 Theos tweak&am…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬