- 编程语言
- 编译器
- 开发工具
【免费下载链接】boa
Boa is an embeddable Javascript engine written in Rust.
$boa是 Boa JavaScript 引擎(Rust 编写的可嵌入式 JS 引擎)通过 CLI 注入到运行上下文中的一个全局调试对象,它把字节码查看、函数指令跟踪、指令流图生成、GC 强制回收、对象 Shape 检查、运行时限制调节等调试能力全部以 JavaScript API 的形式暴露出来。本文以 docs/boa_object.md 为骨架,结合 cli/src/debug 与 core/engine/src/vm/runtime_limits.rs 的源码实现,完整讲解$boa的 8 个功能模块及其每一个 API 的用法、输出格式与底层原理,读者读完可以熟练地在 Boa REPL 中做引擎级的函数与对象内省。
启用方式:--debug-object命令行标志
$boa默认不会出现在全局环境中,需要通过命令行标志显式注入。在 Boa CLI(cli/src/main.rs)中,debug_object字段对应的启动参数定义如下:
/// Inject debugging object `$boa`. #[arg(long)] debug_object: bool,因此启动 REPL 或执行脚本时带上该标志即可:
boa --debug-object在 cli/src/main.rs 的初始化流程中,当检测到args.debug_object为真时,会调用debug::init_boa_debug_object(context)把$boa注册为全局属性。查看 cli/src/debug/mod.rs 的init_boa_debug_object实现可以发现,它本质上是执行一次context.register_global_property(js_string!("$boa"), boa_object, ...),将整个调试对象以普通全局变量的形式挂到全局对象上,属性特性为WRITABLE | NON_ENUMERABLE | CONFIGURABLE(可写、不可枚举、可配置,因此它不会出现在for...in或Object.keys()中,但可以被读取和覆盖)。
从create_boa_object的源码结构可以看到,$boa被划分为 8 个按职责分离的子模块,每个模块由独立的 Rust 文件构建:
| 模块 | 对应源码 | 职责 |
|---|---|---|
$boa.function | cli/src/debug/function.rs | 字节码、指令跟踪、指令流图 |
$boa.object | cli/src/debug/object.rs | 对象内存地址与索引存储类型 |
$boa.shape | cli/src/debug/shape.rs | 对象 Shape 的地址、类型与比较 |
$boa.optimizer | cli/src/debug/optimizer.rs | 优化器开关与统计 |
$boa.gc | cli/src/debug/gc.rs | 强制触发垃圾回收 |
$boa.realm | cli/src/debug/realm.rs | 跨 Realm 行为测试 |
$boa.limits | cli/src/debug/limits.rs | 运行时限制(循环、栈、递归、回溯) |
$boa.string | cli/src/debug/string.rs | 字符串内部存储与编码内省 |
下面按模块逐一讲解。
模块$boa.gc:强制触发垃圾回收
该模块目前只提供一个方法:
$boa.gc.collect()它强制触发 GC 对堆进行扫描并回收垃圾。其底层实现非常直接——cli/src/debug/gc.rs 中的collect函数只有一行核心逻辑:
boa_gc::force_collect();也就是说它调用的是 Boa 自研垃圾回收器boa_gc的强制回收入口(对应 core/gc crate),执行一次完整的堆扫描与回收,然后返回undefined。这在排查内存泄漏、验证对象是否被正确回收时非常有用,也是手动控制 GC 时机(而非等待自动回收)的唯一调试手段。
模块$boa.function:函数级字节码与指令跟踪
这是$boa中功能最丰富的模块,包含 4 个函数,全部围绕 Boa 的字节码 VM(core/engine/src/vm)展开。
$boa.function.bytecode(func):导出函数的编译字节码
传入一个函数,返回其编译后字节码的字符串形式。以普通函数为例:
>> function add(x, y) { return x + y } >> $boa.function.bytecode(add) " ------------------------Compiled Output: 'add'------------------------ Location Count Handler Opcode Operands 000000 0000 none CreateMappedArgumentsObject 000001 0001 none PutLexicalValue 2: 0 000004 0002 none GetArgument 0 000006 0003 none PutLexicalValue 2: 1 000009 0004 none GetArgument 1 000011 0005 none PutLexicalValue 2: 2 000014 0006 none PushDeclarativeEnvironment 2 000016 0007 none GetName 0000: 'x' 000018 0008 none GetName 0001: 'y' 000020 0009 none Add 000021 0010 none SetAccumulatorFromStack 000022 0011 none CheckReturn 000023 0012 none Return 000024 0013 none CheckReturn 000025 0014 none Return Constants: 0000: [ENVIRONMENT] index: 1, bindings: 1 0001: [ENVIRONMENT] index: 2, bindings: 3 0002: [ENVIRONMENT] index: 3, bindings: 0 Bindings: 0000: x 0001: y Handlers: <empty> "输出中的每一行指令都对应一条 VM 操作码:Location是字节码偏移,Count是指令序号,Opcode是操作码名,Operands是对应的参数(如词法绑定下标、环境引用、GetName的常量索引等)。尾部还会附带Constants(常量表)、Bindings(变量名绑定)与Handlers(异常处理器)三个区段,相当于一份完整的函数编译产物清单。
从源码看,cli/src/debug/function.rs 的bytecode实现会先把参数downcast_ref::<OrdinaryFunction>()转为普通函数对象,取到其codeblock()后直接println!("{code}")打印到终端并返回undefined。这也意味着该函数要求传入的是普通函数对象,对非对象参数会抛出TypeError: expected object类错误,对非普通函数对象则报TypeError: expected an ordinary function object。
$boa.function.trace(func, this, ...args):单次跟踪函数执行
只跟踪指定的函数;若该函数内部调用了其他函数,被调用者的指令不会被跟踪。通过第三个及以后的参数可以自由传入调用参数,第二个参数用于指定this值:
>> const add = (a, b) => a + b >> $boa.function.trace(add, undefined, 1, 2) 5μs DefInitArg 0000: 'a' 2 4μs DefInitArg 0001: 'b' <empty> 0μs RestParameterPop <empty> 3μs GetName 0000: 'a' 1 1μs GetName 0000: 'b' 2 2μs Add 3 1μs Return 3 3 >>输出的每一行左侧是该指令执行所耗费的微秒数(如5μs),随后是指令操作码与操作数,最后一行3是函数调用的返回值。this值可以被替换(例如传入某个对象),参数列表也可以自由增减。
其实现(cli/src/debug/function.rs 的trace函数)体现了"临时标记"的设计:先把目标函数标记为可跟踪(set_trace_flag_in_function_object(&callable, true)),随后用给定的this和参数真实调用它(callable.call(this, arguments, context)),调用结束后立刻取消标记。因此跟踪只发生在这一次调用过程中,不会污染后续调用。
$boa.function.traceable(func, mode):持久标记可跟踪函数
将单个函数标记为"在未来的所有执行中都可跟踪",第二个参数传true启用、false关闭。它的价值在于两点:一是可以同时把多个函数标记为可跟踪,观察跨函数调用链;二是可以跟踪会挂起执行并恢复的函数——异步函数、生成器(generator)、异步生成器。普通trace只能覆盖一次同步调用,无法覆盖生成器多次next()之间的恢复执行,而traceable可以。
以下是一个生成器示例(输入):
function* g() { yield 1; yield 2; yield 3; } $boa.function.traceable(g, true); var iter = g(); iter.next(); iter.next(); iter.next();对应的输出:
1μs RestParameterPop <empty> 1μs PushUndefined undefined 2μs Yield undefined 4μs GetName 0000: 'a' 1 0μs Yield 1 1μs GeneratorNext undefined 1μs Pop <empty> 15μs GetName 0000: 'b' 2 1μs Yield 2 1μs GeneratorNext undefined 1μs Pop <empty> 4μs GetName 0000: 'c' 3 1μs Yield 3可以看到每次iter.next()都会触发一次从恢复点继续执行并遇到Yield指令的过程,GeneratorNext指令负责把控制权交回生成器内部。这正是traceable相比trace的核心优势场景。
$boa.function.flowgraph(func, options):生成指令流图
该函数用于获取函数的指令流图(instruction flowgraph),等价于 CLI 的--flowgraph标志,但作用在函数级别,无需退出 Boa shell 再附加命令行标志,非常适合在 REPL 中交互式地对任意函数出图。
它接受两个参数:第一个是目标函数;第二个可以是字符串(表示流图格式),也可以是一个对象。若为对象,支持以下字段(以下为未指定时的默认值):
{ format: 'mermaid', direction: 'LeftRight' // 或 'LR' 简写 }用法示例:
$boa.function.flowgraph(func, 'graphviz') $boa.function.flowgraph(func, { format: 'mermaid', direction: 'TopBottom' })format支持mermaid与graphviz两种格式,分别输出 Mermaid 语法与 Graphviz DOT 语法(可进一步渲染为可视化图)。direction支持四向:LeftRight/LR、RightLeft/RL、TopBottom/TB、BottomTop/BT(大小写不敏感,字符串会被转小写后匹配)。
从源码 cli/src/debug/function.rs 可以看到默认值逻辑:format 未指定时默认Mermaid,direction 未指定时默认LeftToRight;若传入对象则从对象的format/direction属性中读取,若传入字符串则只解析 format;未知的格式或方向字符串会抛出TypeError(如Unknown format type 'xxx')。流图数据来自code.to_graph(...)与Graph类型(位于 core/engine/src/vm/flowgraph),最终通过to_graphviz_format()或to_mermaid_format()序列化后以字符串返回。
补充一点:CLI 级的--flowgraph标志(cli/src/main.rs)默认格式是Graphviz,并提供独立的--flowgraph-direction参数指定方向;而$boa.function.flowgraph的默认格式是Mermaid,二者默认值不同,使用时可留意。
模块$boa.object:对象内部信息内省
该模块提供两个用于获取对象内部信息的函数。
$boa.object.id(object):对象内存地址
返回给定对象在内存中的地址,以字符串形式给出:
let o = { x: 10, y: 20 } $boa.object.id(o) // '0x7F5B3251B718' // 获取 $boa 对象自身的内存地址 $boa.object.id($boa) // '0x7F5B3251B5D8'源码实现(cli/src/debug/object.rs 的id)是取object.as_ref()的原始指针并格式化为0x{:X}大写十六进制字符串。由于 GC 对象是移动不动的(non-moving heap),该地址在对象存活期内保持稳定,可以用来判断两个变量是否指向同一个对象。
$boa.object.indexedStorageType(object):索引存储类型
返回对象当前使用的索引属性(indexed properties)存储类型。这是观察 Boa 对象内部存储策略随数据变化而"升级"的绝佳工具:
let a = [1, 2]; $boa.object.indexedStorageType(a); // 'DenseI32' a.push(0xdeadbeef); $boa.object.indexedStorageType(a); // 'DenseI32' a.push(0.5); $boa.object.indexedStorageType(a); // 'DenseF64' a.push("Hello"); $boa.object.indexedStorageType(a); // 'DenseElement' a[100] = 100; // 制造一个空洞 $boa.object.indexedStorageType(a); // 'SparseElement' // 非简单属性描述符(例如不可写) Object.defineProperty(a, 2, { value: 10, writable: false }); $boa.object.indexedStorageType(a); // 'SparseProperty'从源码看,该函数读取object.borrow().properties().index_properties()并匹配IndexProperties枚举(定义于 core/engine/src/object 的 property 存储体系),共有五种取值,含义如下:
| 返回值 | 含义 |
|---|---|
DenseI32 | 密集存储,元素全部为 32 位整数 |
DenseF64 | 密集存储,元素包含浮点数 |
DenseElement | 密集存储,元素包含任意值(如字符串) |
SparseElement | 稀疏存储(索引出现空洞),元素为普通值 |
SparseProperty | 稀疏存储,且属性带有非简单描述符(如writable: false) |
这个 API 能让开发者直观地看到 Boa 对数组的存储优化策略:整数数组保持紧凑的DenseI32,一旦混入浮点数自动升级为DenseF64,混入字符串升级为DenseElement,索引出现空洞则退化为SparseElement,出现非普通属性描述符则进一步退化为SparseProperty。
模块$boa.optimizer:优化器开关与统计
该模块包含启用/禁用优化的 getter 与 setter 访问器属性。
$boa.optimizer.constantFolding
这是一个访问器属性,getter 在常量折叠优化启用时返回true,否则返回false;setter 用于启用/禁用该优化:
$boa.optimizer.constantFolding = true $boa.optimizer.constantFolding // true源码(cli/src/debug/optimizer.rs)中 getter 检查context.optimizer_options().contains(OptimizerOptions::CONSTANT_FOLDING),setter 则通过options.set(OptimizerOptions::CONSTANT_FOLDING, value)修改后再写回 context。也就是说它直接操作的是引擎级OptimizerOptions位标志(位于 core/engine/src/optimizer),对后续所有编译的代码生效。
$boa.optimizer.statistics
同样是访问器属性,getter 返回优化统计是否开启,setter 用于启用/禁用。启用后优化器会把统计信息打印到stdout:
>> $boa.optimizer.constantFolding = true >> $boa.optimizer.statistics = true >> 1 + 1 Optimizer { constant folding: 1 run(s), 2 pass(es) (1 mutating, 1 checking) } 2 >>可以看到输入1 + 1后,优化器先输出一段统计摘要(常量折叠运行次数、pass 次数、其中变更型与检查型 pass 各多少),随后才打印求值结果2。这印证了统计信息的打印发生在优化流水线执行完毕之后,且与表达式求值结果相互独立。
模块$boa.realm:跨 Realm 行为测试
Realm 是 ECMAScript 规范中的抽象,包含全局对象、内置对象集合与加载代码的机制。$boa.realm提供跨 Realm 行为的测试能力,目前只有一个函数:
$boa.realm.create
创建一个带有全新内置对象集合的新 Realm,并返回它的全局对象:
let global = $boa.realm.create(); Object != global.Object; // true新 Realm 的Object构造器与当前 Realm 的不相同,因此Object != global.Object为true。从源码(cli/src/debug/realm.rs)看,create的实现是直接Context::default()新建一个独立的引擎上下文,并返回其global_object()。由于每个Context都拥有独立的全局对象与内置对象集合,这天然就是一个"新 Realm"。这个 API 常用于验证跨 Realm 对象比较、原型隔离等规范行为。
模块$boa.shape:对象 Shape 内省
在 Boa 中,Shape(对象形状)描述了对象的属性结构(属性名与顺序),相同结构的对象共享同一 Shape,这是属性访问优化的基础(详见 docs/shapes.md)。$boa.shape提供三个函数用于获取对象的 Shape 信息。
$boa.shape.id(object)
返回对象 Shape 在内存中的指针,以十六进制字符串表示:
$boa.shape.id(Number) // '0x7FC35A073868' $boa.shape.id({}) // '0x7FC35A046258'源码实现(cli/src/debug/shape.rs)通过object.borrow().shape()取到 Shape 后调用shape.to_addr_usize()得到地址并格式化。注意内置对象(如Number)的 Shape 地址与普通空对象的 Shape 地址不同——它们分属不同的 Shape 实例。
$boa.shape.type(object)
返回对象的 Shape 类型,目前只有两种取值:
$boa.shape.type({x: 3}) // 'shared' $boa.shape.type(Number) // 'unique'shared:共享 Shape,普通对象字面量创建的 Shape 是可共享的,多个结构相同的对象共用同一 Shape。unique:唯一 Shape,每个实例持有专属的 Shape(典型如内置构造器对象)。
源码中通过shape.is_shared()判断后返回对应的字符串。
$boa.shape.same(o1, o2)
判断两个对象是否拥有相同的 Shape,相同返回true。注意:属性的值无关紧要,只有属性的结构(名称与顺序)参与比较:
// 属性的值不重要! let o1 = { x: 10 } let o2 = {} $boa.shape.same(o1, o2) // false o2.x = 20 $boa.shape.same(o1, o2) // true o2.y = 200 $boa.shape.same(o1, o2) // falseo1 = {x: 10}与空的o2结构不同,比较为false;o2补上x属性后,两者结构一致(属性名x、顺序一致),即使值不同(10 与 20)也返回true;再添加y属性后结构再次分叉,返回false。源码实现是对比两个对象 Shape 的地址(shape.to_addr_usize()是否相等),这从实现层面再次确认了"同构即同 Shape、Shape 即结构"的语义。
模块$boa.limits:运行时限制的动态调节
该模块提供 4 组 getter/setter 访问器,用于在运行时读取和修改引擎的执行限制,是研究 Boa 运行时保护机制的入口。这些限制的默认值定义在 core/engine/src/vm/runtime_limits.rs 的RuntimeLimits中:
impl Default for RuntimeLimits { fn default() -> Self { Self { loop_iteration: u64::MAX, // 循环迭代上限,u64::MAX 表示无限制 recursion: 512, // 递归深度上限 backtrace_limit: 50, // 异常回溯栈帧上限 stack_size: 1024 * 10, // 值栈大小上限 } } }$boa.limits.loop
getter 返回抛出错误前的循环迭代上限;setter 用于设置该上限:
$boa.limits.loop = 10; while (true) {} // RuntimeLimit: Maximum loop iteration limit 10 exceeded当循环迭代次数超过上限时,VM 抛出RuntimeLimit错误(错误消息中会带上具体上限值)。RuntimeLimits源码注释明确说明:值为u64::MAX表示不设限制,且提供了disable_loop_iteration_limit()专门用于关闭该限制。这在测试 core/engine/src/vm/tests.rs 中也有大量印证(例如context.runtime_limits_mut().set_loop_iteration_limit(10)后配合循环用例验证报错行为)。
$boa.limits.stack
getter 返回值栈大小上限;setter 用于设置栈大小限制:
$boa.limits.stack = 10; function x() { return; } x(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // RuntimeLimit: exceeded maximum call stack length注意这里的stack对应RuntimeLimits的stack_size(默认1024 * 10),限制的是 VM 值栈的大小;栈空间被耗尽时抛出exceeded maximum call stack length。setter 在 cli/src/debug/limits.rs 中会把参数先to_length转换,再try_into到usize,若参数超过usize::MAX会抛出RangeError: Argument {value} greater than usize::MAX。
$boa.limits.recursion
getter 返回递归深度上限;setter 用于设置递归限制:
$boa.limits.recursion = 100; function x() { return x(); } x(); // RuntimeLimit: Maximum recursion limit 100 exceeded默认递归上限为 512(见上面RuntimeLimits默认值),设置后无终止条件的递归会在超过深度时被截断并报错。同样的模式在 core/engine/src/vm/tests.rs 中通过set_recursion_limit(10)等用例验证。
$boa.limits.backtrace
getter 返回抛出错误时回溯(backtrace)的最大帧数;setter 用于设置回溯上限:
$boa.limits.backtrace = 100; function x() { function y() { function z() { throw "Hello"; } z(); } y(); } x(); // Uncaught "Hello" // at z (test.js:6:13) // at y (test.js:8:6) // at x (test.js:10:4) // at <main> (test.js:12:2)默认回溯上限为 50 帧(RuntimeLimits::default()中backtrace_limit: 50)。当抛出错误时,VM 会收集调用栈信息并生成带文件名与行列号的回溯(at z (test.js:6:13)),该限制决定回溯最多保留多少帧。
这 4 组访问器在 cli/src/debug/limits.rs 中统一由FunctionObjectBuilder构造 getter/setter 函数对,再通过ObjectInitializer::accessor注册为访问器属性,底层分别读写context.runtime_limits()/context.runtime_limits_mut()的对应字段。也就是说,$boa.limits的每一次读写都实时作用于当前Context的RuntimeLimits结构。
模块$boa.string:字符串内部表示内省
该模块提供三个函数用于查看字符串的内部存储与编码信息,帮助你理解 Boa 对字符串的两种存储位置和两种编码方案。
$boa.string.storage(str)
返回字符串的内部存储类型:如果该字符串是 Boa 中众所周知的、存储在STATIC_STRINGS数组中的字符串,则返回"static",否则返回"heap":
$boa.string.storage("push") // "static" $boa.string.storage("specialFunction") // "heap"源码(cli/src/debug/string.rs)通过string.is_static()判断。STATIC_STRINGS是 Boa 内部预注册的静态字符串表(字符串驻留池),属性名等高频字符串会被驻留以加速查找,而其他字符串则在堆上分配。这也解释了为什么"push"(数组方法名,属高频字符串)返回static,而任意的"specialFunction"返回heap。
$boa.string.encoding(str)
返回字符串的内部编码,取值只有两种:
$boa.string.encoding("Greeting") // "latin1" $boa.string.encoding("挨拶") // "utf16"纯 ASCII/单字节字符范围内的字符串采用latin1(单字节紧凑编码),而包含非 Latin-1 字符(如日语"挨拶")的字符串采用utf16。源码中通过str.variant()匹配JsStrVariant枚举(定义于 core/string crate)的Latin1与Utf16两个变体,这体现了 Boa 字符串类型(docs/string.md)为节省内存而做的双编码设计。
$boa.string.summary(str)
返回一个包含字符串存储与编码概要的对象:
$boa.string.summary("Greeting") // { storage: "heap", encoding: "latin1" }注意与storage的差异:"Greeting"并非驻留字符串,因此storage为"heap";而编码为latin1。从源码看,summary会新建一个普通对象,把storage与encoding两个字段以Attribute::all()(完全属性)写入并返回。这个 API 适合一次性获取字符串的两个核心内部属性,避免两次调用。
实战组合:一个完整的引擎调试工作流
将上述模块组合起来,可以在 Boa REPL 中完成一套"函数级 + 对象级 + 引擎级"的调试流程:
- 用
--debug-object启动 REPL,获得$boa; - 用
$boa.function.bytecode(add)查看add的编译产物,确认操作码序列是否符合预期; - 用
$boa.function.trace(add, undefined, 3, 4)观察单次执行的指令耗时分布; - 对生成器等挂起函数改用
$boa.function.traceable(g, true)做持续跟踪; - 用
$boa.function.flowgraph(add, 'graphviz')导出指令流图并渲染成可视化的控制流图; - 用
$boa.object.indexedStorageType(arr)验证数组在不同数据形态下的存储升级路径; - 用
$boa.shape.same(o1, o2)验证 Shape 共享与分叉逻辑; - 用
$boa.limits.loop/$boa.limits.recursion等设置较小的限制,快速验证RuntimeLimit错误路径; - 用
$boa.gc.collect()手动触发回收,配合$boa.object.id观察对象是否被回收。
这套流程全部发生在 JavaScript 层,无需退出 REPL、无需重新附加 CLI 标志,也不要求直接阅读 Rust 内部结构,是研究与调试 Boa 引擎行为(尤其是字节码编译器、VM 执行、GC、Shape 系统与运行时限制机制)的便捷入口。若想进一步理解这些 API 背后依赖的 VM 指令与执行模型,可继续阅读 docs/vm.md 与 docs/bytecompiler.md;要深入掌握$boa.shape依赖的 Shape 系统,可参考 docs/shapes.md。
- 编程语言
- 编译器
- 开发工具
【免费下载链接】boa
Boa is an embeddable Javascript engine written in Rust.
相关推荐
Authelia `debug expression` 命令实战:对任意用户离线验证 CEL 用户属性表达式
Authelia debug expression 命令实战:对任意用户离线验证 CEL 用户属性表达式 本篇介绍 Authelia CLI 中 autheli
编程语言编译器开发工具如何快速上手SharpShooter:5分钟创建你的第一个恶意Payload
如何快速上手SharpShooter:5分钟创建你的第一个恶意Payload SharpShooter 是一个功能强大的Payload生成框架,专门用于创建和执
Boa引擎WASM支持:在WebAssembly环境中运行JavaScript的完整指南
Boa引擎WASM支持:在WebAssembly环境中运行JavaScript的完整指南 Boa是一个用Rust编写的实验性JavaScript引擎,支持Web
编程语言编译器开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考