
编程语言语言运行时嵌入式【免费下载链接】rhaiRhai - An embedded scripting language for Rust.项目地址https://gitcode.com/gh_mirrors/rh/rhai点击查看免费下载Rhai Grain 是 Rhai 脚本语言内置的字节码转译器transpiler与虚拟机VM它把 Rhai 的 AST 编译成扁平的指令流让资源受限的嵌入式设备可以只加载字节码产物、完全不解析源码甚至不构建语法树。本文以仓库中的工程指南 src/grain/AGENTS.md 为主线结合 src/grain/mod.rs、src/grain/program.rs、src/grain/compile/mod.rs、src/grain/format/mod.rs 等核心源码与 tests/grain 测试套件系统讲解 Grain 的模块结构、设计约束、字节码格式、残片机制、诊断分离与测试命令帮助读者理解并参与该模块的开发。一、Grain 的定位为什么需要字节码Rhai 默认通过遍历 AST 来求值脚本解析器按节点逐个分配内存。这意味着持有脚本的成本与程序体量成正比AST 每个节点都要占用堆内存解析峰值更高——解析器在编译期间的瞬时分配远高于最终稳定下来的 AST 大小在资源受限的嵌入式目标上这通常是限制脚本规模的第一因素比任何其他约束都更早触顶。Grain 的思路是把 AST 转译为一条扁平指令流。字节码可以在宿主机上编译好然后在没有解析器、甚至没有 AST 解释器的设备上直接加载执行。这正是 src/grain/mod.rs 中 Core Concept 一节阐述的动机tests/grain/allocation.rs 则用带计数功能的全局分配器对AST 成本 vs VM 成本做了量化测量——该测试特意独立成文件并独占一个二进制因为计数器是进程级的任何并发分配都会污染读数。1.1 执行模型复用宿主 EngineGrain 的执行不是另起炉灶值类型仍然是Dynamic函数注册与分发仍然由宿主Engine完成VM 只重新实现三件事控制流、局部变量访问、运算符快速路径。用 src/grain/mod.rs 中的话说Engine负责解析 AST 与运行时函数分发VM 本质上把 AST 压平并穿成线性指令流从而可以无递归、无多层函数调用地执行。下面是一个最小示例来自 src/grain/mod.rs 的文档测试use rhai::grain::{Compiler, Vm}; use rhai::{Engine, Scope}; let engine Engine::new(); let ast engine.compile(let total 0; for i in 0..10 { total i; } total)?; let program Compiler::new().compile(ast); // Scope 是调用方的局部变量脚本声明的变量留在其中 // 与 Engine::eval_with_scope 的行为完全一致。 let mut scope Scope::new(); let value Vm::new(engine).eval_with_scope(mut scope, program)?; assert_eq!(value.as_int().unwrap(), 45);1.2 安全性整个 Grain 模块不包含任何unsafe代码在 src/grain/mod.rs 顶部以#![forbid(unsafe_code)]强制约束。加载不可信字节码是 Grain 的典型场景这一约束是安全底线。二、仓库结构与模块职责src/grain/AGENTS.md 给出了完整的目录地图对照源码逐一展开如下目录/文件职责关键实现src/grainGrain 字节码编译器、格式与 VM 的总入口src/grain/mod.rs 导出Compiler、Program、SharedProgram、Vm、Fault、Sidecar、Stripped、Abi、AbiMismatch、Caps、ReadError、WriteError、Site等公共 APIsrc/grain/bytecodeGrain 操作码与字节码定义含验证器src/grain/bytecode/op.rs 定义Op指令集、Receiver、AssignOpsrc/grain/bytecode/verify.rs 实现Chunk运行前的校验仅internalsfeature 下导出src/grain/compile字节码编译器把 Rhai AST 转译为 Grain 字节码src/grain/compile/mod.rs 中的Compiler::compile与内部Lowering状态机src/grain/format字节码产物artifact与线格式的读写src/grain/format/mod.rs 定义MAGIC、VERSION、Sidecar、Stripped子模块abi.rs、read.rs、write.rssrc/grain/pos字节码指令地址到源码位置的映射src/grain/pos/mod.rs 的Site行/列与位置表解析src/grain/vm字节码执行 VMsrc/grain/vm/mod.rs 的Vm、Fault与回调包装src/grain/program.rs编译后的字节码程序src/grain/program.rs 的Programa含Function、Partstests/grainGrain 测试包括与 AST walker 的差分对比tests/grain/differential.rstests/grain/corpus生成与固定的脚本语料tests/grain/corpus/mod.rs 的Case列表tests/grain/fixtures字节码格式/版本检查的 fixturestests/grain/fixtures 中的golden.rgrn、golden.rhai、follow.rhai值得注意的是 src/grain/mod.rs 中模块的可见性随 feature 变化bytecode模块仅在internalsfeature 下公开pub mod bytecode否则为 crate 私有pub(crate) mod bytecode而Compiler仅在no_ast未启用时导出。三、设计约束与代码修改守则src/grain/AGENTS.md 的 Code modifications 一节是参与 Grain 开发的最高优先级约束逐条解读如下。3.1 线格式wire format稳定第一Avoid changing the artifacts wire format unless it significantly simplifies the change.产物的线格式是宿主与设备之间的契约除非能显著简化变更否则不要改动它。线格式的全部形状定义在 src/grain/format/mod.rs 的 Shape 一节RGRN magic u16 format version abi INT width, FLOAT width, restriction bitmask u128 debug id, naming the diagnostics compiled with this varint utf8 source name, empty for none section names section constants section operator tokens section op-assignments section chains section switch tables varint declared max stack section code, verbatim section position table, empty when stripped格式特征魔数MAGIC *bRGRNsrc/grain/format/mod.rs让非产物文件在读入的最初几个字节就被拒绝而不是被当作无意义的操作码解析版本号VERSION: u16 11仅在编码方式变化到旧读者会误读时才 bump除 code 段外全部采用 LEB128 变长编码有符号值做 zigzag因为它们在加载时只读一次code 段是特例它是 VM 直接执行的字节被原样拷贝并切片引用操作数为定宽因此分发时不需要解码。3.2 不要轻易新增 Opcode 或 op tagAvoid adding new Opcodes or new op tags unless it significantly simplifies the change.新增指令意味着验证器、编译器、VM 分发、disassembler 全线联动扩散面极大。指令集定义于 src/grain/bytecode/op.rs 的Op枚举注释中明确说明了其设计原则这是一台栈式机器操作数压入/弹出操作数栈局部变量存放在直接寻址的 slot 中指令不携带源码位置失败时的位置来自以指令地址为键的 positions 表——这正是诊断可以从产物中剥离而不触碰代码的原因任何放不进操作数的宽字段都作为u32索引指向Program的池pool重复出现的运算符或名字只存储一次。3.3 VERSION 的 bump 节奏No need to bumpconst VERSIONinsrc/grain/format/mod.rsfor every new feature that alters the wire format. OneVERSIONbump per public release is enough.针对加性变更新 op tag、新常量 tag旧读者本来就会拒绝因此不需要 bump。真正的 VERSION bump 只服务于旧读者会误读的编码方式变化。当前版本号在 src/grain/format/mod.rs 中为11。3.4 与 AST walker 行为等价 差分测试Keep Grain behavior equivalent to the AST walker for the same AST; add or update differential tests when changing behavior that affects Grain.这是 Grain 的正确性铁律同一个 AST走 AST 解释器和走 VM 必须得到相同结果。差分测试的实现见 tests/grain/differential.rs——每个语料脚本在同一Engine上分别用eval_ast_with_scope和 VM 求值要求结果、错误变体与位置、以及运行后留下的 Scope三者全部一致。其中 Scope 校验不是附带的Rhai 求值顶层语句不回卷let在顶层声明的变量在运行结束后仍对调用方可见VM 若自管帧就可能值对了但作用域错了。3.5 残片residual是最后手段Avoid fragmenting any non-lowerable AST nodes into residuals; seek instructions first.残片机制见第五节保证覆盖但残片意味着程序不可序列化、且运行时要借助 AST 解释器。开发者应优先寻找指令化路径只有在确实无法降低时才允许碎片化。3.6 操作码宽度最小化Opcode widths should be minimal, so avoid adding fields, especially long ones. When opcodes are added, prefer new tag variants instead of lengthening the opcode encoded width.两条配套原则字段越少越好、越短越好——操作码编码宽度直接决定产物体积与分发开销需要表达新语义时优先新增 tag 变体而不是把既有 opcode 的编码宽度加长。Op中的AssignOpx op y就是一个池化典型它携带token、名字、token、名字四个字段放不进操作数因此放入程序的 op-assignment 池脚本里十处只占一个池条目。四、字节码格式、ABI 与验证4.1 能力位图 Caps每个Program记录其指令所需的能力capabilities定义在 src/grain/format/abi.rs。Caps: u32位图按位记录脚本需要的运行时能力例如FLOATno_float下不可用、ARRAY、BLOB、MAP、DECIMALFUNCTION、FN_PTR、CURRYING、INDEXING、PROPERTY、METHOD、THIS、SHARINGEXPORT、IMPORT、MODULE、CUSTOM_SYNTAX。位顺序即线格式上的顺序只能追加、不可重排。Caps的双向含义是既描述脚本需要什么能力也描述宿主Engine支持或不支持什么能力。读取方可以用它做加载前的快速甄别。4.2 Program::read 的加载校验src/grain/format/mod.rs 对Program::read的语义有严格承诺The chunk is verified before this returns, so a program that loads cannot underflow the operand stack, jump outside itself, jump into the middle of an instruction, or index a pool entry that is not there.即加载即校验调用 src/grain/bytecode/verify.rs 的verify经由 src/grain/program.rs 的Program::verify。校验错误类型包括MissingCaps、Undecodable、TrailingBytes、ChunkOutOfRange、JumpOutOfRange等——每个变体都指向正确编译器不可能产生的情况因此校验失败要么是编译器 bug要么是损坏的产物绝不是脚本能表达的合法行为。此外 src/grain/format/mod.rs 的读端做了防御性设计count 字段在保留内存前先与剩余字节数比对防止几个字节请求一 TB 分配LEB128 解码封顶十个分组杜绝连续 continuation 字节造成的死循环。这也是 tests/grain/fuzz.rs 的测试对象对合法产物的每个字节做单字节翻转与结构性变异断言要么加载成功、要么报错永不 panic、永不行为失常对任意字节输入同样保证不会加载出会作恶的程序。4.3 Program 的内存特性src/grain/program.rs 中Programa的设计充分体现了嵌入式优先代码零分配加载read返回的Program直接借用输入字节缓冲区borrowVM 在原位分发执行不为每条指令构建 16 字节的枚举需要独立持有时调用into_owned()加载分配量有界只随脚本提及的不同常量、名字、运算符数量增长与脚本多长无关名字池是两大块分配整个程序的所有名字是一块借用 blob 加一段 spans脚本再长也不随名字数量增长src/grain/program.rs 的names: Stringsa字段注释操作数栈按需预留max_stack是各 chunk 所需操作数栈深度的最大值一次预留供所有帧共用因为调用把操作数压到调用方之上而不是另起栈。4.4write拒绝什么src/grain/format/mod.rs 明确列出拒绝写出的情形残留片段是真实的Expr树——恰恰是本模块要消除的分配——因此持有任何残片的程序拒绝序列化write返回WriteError编译器无法降低的脚本函数同理被拒因为 Rhai 会保留该函数自己的 AST 副本。两种失败都会点名是哪个构造挡了路Program::first_unsupported()src/grain/program.rs返回(static str, Position)例如import、export、eval、会改变作用域形状的 custom syntax让校验者可以直接拒绝一次上传并给出去改第几行的依据。这是把退回发源码从谜题变成决策的关键 API。五、残片Residual机制与全语言覆盖Coverage is total from the start, by construction rather than by effort.src/grain/mod.rs 明确Grain 的覆盖从一开始就是完全的而且是构造保证而非工作量堆出来的。凡是转译器尚不能降低的构造都会以 AST 片段的形式保留并在运行时通过Op::EvalAstsrc/grain/bytecode/op.rs 中的逃生舱指令交还给 Rhai 解释器。因此aProgramalways means the same thing as theASTit came from.关键度量 APIProgram::residual_count()src/grain/program.rs还有多少 AST 残片需要解释器求值Program::residual_nodes()残片内部共包含多少 AST 节点当残片数降到 0说明 AST完全降低VM 可以直接执行而无需任何 AST 解释器协助——此时甚至可以启用no_astfeature 把解析器/解释器从构建中整个移除。编译器内部src/grain/compile/mod.rs的兜底路径值得注意slot 模型算不清的脚本、或汇编失败名字/常量超过u16操作数索引上限的脚本会退回整段程序一个残片whole_program_residual由Op::EvalAst原样执行且不回卷作用域保证顶层声明仍然可达。函数级别的降级是逐函数的一个函数体 slot 模型搞不定只把该函数留给 Rhai 自己的副本走普通分发其余函数照常指令化。六、编译流程从 AST 到字节码Compiler::compile(ast) - Programstaticsrc/grain/compile/mod.rs的完整管线是Lowering以Lowering结构体含slots、code: VecOp、positions、residuals、consts、names、chains、switches、loops等状态逐语句转译 AST 顶层语句与各脚本函数体函数体追加到同一条指令列表使整个程序是一个统一地址空间汇编assemble(lowering.code)把VecOp编码成字节切片resolve_switch_targets单独解析跳转目标switch 目标以指令索引形式活在池里而非代码中位置表重建跳转目标与位置表在 lowering 阶段都按指令索引记录汇编后指令长度不一因此把 lowering 期间的positions重排到最终字节偏移上构建 Program组装Program::new(parts...)随后调用program.tighten_stack()——用验证器实测的栈高水位替换编译器无法深度遍历得出的上界估计VM 只预留实际用量而非每条指令一个槽位。代码中的关键决策注释还解释了break/continue的 backpatch 机制Loop结构体记录 continue 目标、break 深度、活跃迭代器数、try区域数等保证跳出循环时正确清理、switch的双表布局hashed cases 表 ranges 表、以及链式访问a.b[i].c的展开flatten_chain把 Rhai 的嵌套Dot/Index结构拆成步骤列表一条Op::Chain指令承载任意步数。七、诊断分离Sidecar 与错位回溯Grain 最重要的工程创新之一是把诊断与产物分离Program::write()产物包含诊断positions 表Program::write_stripped()src/grain/format/mod.rs返回Stripped { artifact: Vecu8, sidecar: Sidecar }——设备只拿 artifact宿主保留Sidecar设备上运行失败时VM 每帧产生一个Fault指令地址 链槽位宿主用Sidecar::resolve(trace)把它还原成VecOptionSite行/列内层帧在前如同崩溃转储的一边是地址栈、一边是符号表。7.1 debug_id 防错配Sidecar与产物通过debug_id: u128关联src/grain/format/mod.rs。其算法是FNV-1a刻意不用 Engine 的哈希器保证跨调用稳定输入是 positions 表与 chains 表的内容。设计动机在源码注释与测试a_sidecar_from_another_build_of_the_same_code_is_refused中讲得很清楚Two scripts differing only in whitespace compile to the same instructions... while their positions are exactly what changed.两份仅空白不同的脚本编译出的指令完全相同仅位置表不同。若把 A 的 sidecar 挂到 B 的产物上每个错误都会错报一两行且看起来像正确答案。因此 id 取自诊断内容而非代码挂错 sidecar 时Program::attach_positions直接返回TableError::WrongProgramsrc/grain/program.rs 的attach_positions。debug_id的定位正如注释所说PDB 的 GUID 或 ELF 的 build-id。7.2 位置表格式与两层解析src/grain/pos/mod.rs 给出了位置表格式varint entry count per entry: varint address delta from the previous entry varint line, 1-based (0 means the site is unknown) varint column, 1-based (0 means the start of a line)条目按地址升序排列查找是线性扫描——这正适合程序已经失败后才会被查询的场景。该模块刻意不依赖std且不认识Rhai 的Position类型因为设备端保留表以报告错误的前提就是不必链接产生它的编译器。值得注意的还有它的两层解析设计Site是压缩后minified源码中的位置若脚本在编译前被压缩过还需要 Source Map v3 解析器把Site再映射回原始源码。八、调试支持语句级粒度与标记成本8.1 与 AST 解释器的差异debugging构建会在每个语句处打标记VM 停在标记处——回溯back-trace、单步stepping、按位置断点、函数退出事件都可用src/grain/vm/mod.rs 中的rewind_after_stop还会把调试回调声明的变量从作用域上收走防止污染按编译期形状寻址的 slot。但 Grain 的粒度止于语句AST 解释器在每个表达式处都能停而Program不再拥有表达式节点所以单步落点是下一条语句而不是当前语句的中间按函数名/属性名断点永远不会命中——AST 解释器递给回调的是合成的Noop节点而非调用本身按位置断点能覆盖同一行。8.2 调试标记的成本src/grain/mod.rs 明确调试标记是程序中发布构建不该包含的唯一部分——没有debugging回调的Engine没有任何可停的东西。每个标记约占每条语句 6 字节在编译了它们的地方而没有标记的Program依然随处可跑只是无法被停下。注释还特别强调运行不可信字节码的 VM 不应携带任何调试标记。九、测试与质量保障src/grain/AGENTS.md 的 Checks and tests 一节给出了官方测试矩阵结合 Cargo.toml 的配置展开9.1 必要特性与命令internals是必需的许多 Grain 测试依赖内部 API如Program::functions()、bytecode模块Cargo.toml 中internals []即用于暴露这些结构可能影响 Grain 的改动运行cargo test --features internals,grain只跑 Grain 测试套件不跑其余测试cargo test --features internals,grain --test grain该测试二进制的装配在 Cargo.toml 中定义[[test]] name grainrequired-features [grain]tests/mod.rs 以内联mod grain方式把 tests/grain 下的各 harness 挂进同一个二进制。9.2 测试矩阵一览tests/grain 下每个 harness 都有明确分工测试文件验证内容tests/grain/differential.rs语料脚本在 walker 与 VM 下结果、错误、Scope 三方一致懒标准库与急标准库也须一致tests/grain/format.rs产物往返compile→write→read→run 与原程序等价read对任意字节要么加载要么报错、绝不 panictests/grain/fuzz.rs对产物做多轮变异与任意字节输入断言加载后行为正常tests/grain/allocation.rs用计数分配器测量 AST vs 字节码的内存占用独立二进制grain_allocation_efficiencytests/grain/scope.rs调用方已有变量slot 无法寻址、须按名读写与 Rhai 行为一致含writable断言tests/grain/call_fn.rs、tests/grain/callback.rs脚本函数按名调用、函数指针/闭包交给 native 的回调路径tests/grain/custom_syntax.rs、tests/grain/module.rs自定义语法与模块导入/导出tests/grain/debugger.rsdebugging构建下的调试接口tests/grain/limits.rs执行边界max_operations等tests/grain/lazy.rs懒加载标准库与默认行为等价tests/grain/projection.rs对照follow.rhai给 Rhai 自身 AST 节点定价9.3 语料库与 fixturescorpustests/grain/corpus/mod.rs一组固定脚本刻意偏向字节码 VM 最容易与树 walker 漂移的地方——作用域纪律、Rhai 用于return/break/throw的基于错误的回卷、以及无法用普通mut表达、必须显式写回的 lvalue 形式。语料还注册了带 getter/setter/indexer 的宿主类型Widget专门锻炼链式访问中getter 返回值而非引用、必须靠 setter 写回的保守路径fixturestests/grain/fixturesgolden.rgrn黄金字节码产物与golden.rhai对应脚本。当线格式变化需要重生成GOLDEN产物时应同步更新这两份 fixture——这正是 src/grain/AGENTS.md 末尾Regenerate theGOLDENartifact if necessary所指。9.4 配套二进制与示例Cargo.toml 还声明了 Grain 配套工具grain-compilebinrequired-features [grain]Rhai 脚本 → Grain 字节码转译器grain-dumpbinrequired-features [grain, internals]反汇编器用于阅读脚本降低成了什么指令Function::disassemble与code::disassemble均在internals下导出grain_benchexamplerequired-features [grain]VM 对 walker 的计时基准--release运行-- --check在低于记录下限时以非零退出。十、feature flags 与适用前提Grain 的启用与裁剪依赖 Cargo.toml 中的一组 featuregrain启用 Rhai Grain 字节码编译器与 VMinternals暴露内部数据结构Grain 测试与grain-dump依赖debugging启用调试接口隐含internals为每个语句编译约 6 字节的调试标记no_ast移除 AST、tokenizer、parser 与 optimizer隐含no_optimize配合grain实现纯 VM 字节码执行——这正是嵌入式设备端最小构建的形态没有解析器、没有解释器、没有 AST 在视野内。需要说明的限制Grain 当前对follow.rhai这类含属性读取与索引赋值的脚本仍会碎片化tests/grain/allocation.rs 注释明确 it still fragments on property reads and index assignment, and a program holding a fragment refuses to serialize因此能否写出产物取决于残片是否归零Program::read的产物由当前构建写出expect(written by this build)跨版本读取受VERSION与 ABIINT/FLOAT 宽度、限制位掩码约束。对于需要在设备上离线还原错误位置的场景可参考 src/grain/pos/mod.rs 的Site结构line从 1 计数、column从 1 计数且 0 表示行首与debug_id匹配流程确保 sidecar 与产物一一对应。结语Rhai Grain 是一个把嵌入式脚本安全分发贯彻到源码层面的工程线格式以RGRN魔数开头、以VERSION控制兼容编译管线由 lowering → 汇编 → 验证三步构成Op::EvalAst保证全语言覆盖residual_count让能否写出产物成为可判定的工程决策Sidecar/debug_id把错误回溯从设备搬回宿主。对参与开发的读者而言src/grain/AGENTS.md 的四条守则线格式稳定、不加 opcode、VERSION 按发布 bump、行为与 walker 等价并补差分测试配合cargo test --features internals,grain测试矩阵即是进入该模块的最低门槛与最高准绳。赞分享编程语言语言运行时嵌入式【免费下载链接】rhaiRhai - An embedded scripting language for Rust.项目地址https://gitcode.com/gh_mirrors/rh/rhai点击查看免费下载相关推荐Paseo Server 开发指南多编码 Agent 守护进程的架构、构建、测试与工程规范Paseo Server 开发指南多编码 Agent 守护进程的架构、构建、测试与工程规范 Paseo 是一个用于监控与控制本地 AI 编码 AgentClExpo Router 源码级开发指南文件路由架构、测试体系与工程实践expo/expoExpo Router 源码级开发指南文件路由架构、测试体系与工程实践expo/expo 导读Expo Router 是 Expo 官方开源的文件式路由移动开发前端跨平台原生移动Hermes 构建与运行实战指南从源码编译到字节码执行与测试Hermes 构建与运行实战指南从源码编译到字节码执行与测试 导读 本文是 Hermes JavaScript 引擎专为 React Native 优化的运语言运行时编译器移动开发上一篇Fprime错误码体系标准化异常处理与诊断下一篇Qwen3.6-27B-AWQ-INT4最佳实践3套采样参数配置让编码/推理/创作效率倍增创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考