文档
把 luna 嵌入 Rust 宿主所需的一切——从三行的 hello 到稳定 API 契约。以 v2.16.0 为快照。rustdoc API 参考在 docs.rs。
安装与 hello
luna 是一个含五个可发布 crate 的 Cargo 工作区。挑选合适的依赖体量:luna-jit 提供完整解释器加 Cranelift JIT 与 C ABI,luna-core 则是零依赖、对 wasm 友好的解释器。
# 完整解释器 + Cranelift JIT + C ABI
luna-jit = "2"
# 或者最小表面 —— 纯解释器,零第三方依赖
luna-core = "2"为选定的方言构造一个 Vm 并求值。Vm::new 会打开所有默认安全的标准库并装上 JIT(在 luna-core 下是一个空操作后端)。
use luna_jit::vm::Vm;
use luna_jit::version::LuaVersion;
fn main() {
let mut vm = Vm::new(LuaVersion::Lua55); // 5.5 + full stdlib + JIT on
let result = vm.eval("return 'hello, ' .. 'world'").unwrap();
let s: String = result[0].try_as_str().unwrap().to_string();
println!("{s}");
}命令行界面
安装 luna-jit 会在 PATH 上放一个 luna 二进制——一个 REPL 兼脚本运行器。
luna script.lua # 运行文件
luna -e "print(1 + 2)" # 运行内联代码
luna --lua=5.4 script.lua # 选择方言
luna # 交互式 REPL(Ctrl-D 退出)| 选项 | 行为 |
|---|---|
| --lua=5.X | 选择方言(5.1 / 5.2 / 5.3 / 5.4 / 5.5;默认 5.5) |
| --sandbox | 仅打开 base/math/string/table/coroutine;拒绝字节码加载 |
| --budget=N | 运行前设置指令预算 |
| --no-jit | 纯解释器运行(装上空 JIT 后端) |
| --profile | 脚本结束后打印 trace-JIT 计数器 |
| -e "<code>" | 运行内联代码而非文件 |
| - | 从 stdin 读取源码 |
REPL 先把每一行当表达式求值(前缀 return),遇语法错误再当语句重试——所以表达式与赋值都能工作。
嵌入
嵌入 API 是首要表面——比 C ABI 更丰富,且拥有完全安全的所有权。以下展示每种能力的形态;完整手册覆盖 14 个小节,含 proc-macro userdata 与异步表面。
面向不受信任脚本的沙箱
沙箱构造器为库设立白名单并武装预算。被沙箱化的脚本无法 require、触碰文件系统,或编译字节码块。
use luna_jit::Lua;
use luna_jit::version::LuaVersion;
let mut lua = Lua::sandbox(LuaVersion::Lua54)
.open_base()
.open_math()
.open_string()
.open_table()
.with_instr_budget(1_000_000) // ~10 ms 墙钟预算
.with_memory_cap(8 * 1024 * 1024) // 8 MiB 上限
.build();
let r: i64 = lua.eval("return 1 + 2").unwrap();全局变量与表
set_global 接受任意 IntoValue 类型(Option<T> 映射为 nil)。表提供一次性与链式构造器两种形态。
vm.set_global("answer", 42_i64)?;
vm.set_global("name", "luna")?;
vm.set_global("missing", Option::<i64>::None)?; // 设为 nil
// 一次性、固定形状:
let t = vm.table_of([("answer", 42_i64), ("year", 2026_i64)]);
// 或用链式构造器应对可变形状:
let t = vm.new_table()
.with("name", "luna")
.with(1_i64, "first array entry")
.build();类型化原生函数
native_typed 把 Rust 闭包或函数指针桥接到 Lua,自动完成参数解析与返回值编码——纯函数、多返回值或可失败(Result<T, LuaError>)皆可,支持 0 到 6 元。
let add = vm.native_typed(|a: i64, b: i64| -> i64 { a + b });
vm.set_global("add", add)?;
let split = vm.native_typed(|x: i64| -> (i64, i64) { (x / 10, x % 10) });
vm.set_global("split", split)?;
let safe_div = vm.native_typed(|a: i64, b: i64| -> Result<i64, LuaError> {
if b == 0 { Err(LuaError::new(Value::Nil)) } else { Ok(a / b) }
});
vm.set_global("safe_div", safe_div)?;Userdata —— 暴露宿主类型
把任意 T: 'static 的 Rust 值藏在 Lua userdata 背后。一个空的 impl LuaUserdata for T {} 就足以桥接;再加上方法与元方法,它便如原生一般。#[derive(LuaUserdata)] proc-macro 会生成样板代码。
use luna_core::vm::{LuaUserdata, MetaMethod, UserdataMethods};
struct Counter { value: i64 }
impl LuaUserdata for Counter {
fn type_name() -> &'static str { "Counter" }
fn add_methods<M: UserdataMethods<Self>>(m: &mut M) {
m.add_method("get", |_vm, this, ()| Ok::<_, _>(this.value));
m.add_method_mut("incr", |_vm, this, (by,): (i64,)| {
this.value += by; Ok::<_, _>(())
});
m.add_meta_method(MetaMethod::ToString, |_vm, this, ()| {
Ok::<_, _>(format!("Counter({})", this.value))
});
}
}
vm.set_userdata("c", Counter { value: 100 })?;
vm.eval("c:incr(50); print(tostring(c))")?; // → Counter(150)从 Rust 驱动协程与调试钩子
无需 Lua 侧的 coroutine.create,用 create_coroutine / resume_coroutine 即可从 Rust 驱动 Lua 协程。用 set_rust_debug_hook 在 Call / Return / Line / Count / TailCall 事件上装一个 Rust 回调。
let body = vm.eval(r#"
return function()
coroutine.yield(1)
coroutine.yield(2)
return 3
end
"#)?[0];
let co = vm.create_coroutine(body);
let r1 = vm.resume_coroutine(co, vec![])?; // r1[0] == Int(1)
let r2 = vm.resume_coroutine(co, vec![])?; // r2[0] == Int(2)
let r3 = vm.resume_coroutine(co, vec![])?; // r3[0] == Int(3), 终止错误
Lua 错误以 Result<T, LuaError> 浮现。LuaError 实现了 Display / Error;更丰富的上下文可从 Vm 取得。
match vm.eval("error('something failed')") {
Ok(v) => println!("ok: {:?}", v),
Err(e) => {
println!("error: {}", e); // Display
let kind = vm.error_kind(); // LuaErrorKind
let source = vm.error_source(); // Option<(&str, u32)>
let tb = vm.take_error_traceback(); // Option<String>
}
}Lua 门面
若你偏爱 mlua 风格的门面,Lua 用同一套机制封装,提供由 host-root 票据支撑的 Copy + Clone 句柄。
use luna_jit::Lua;
let mut lua = Lua::new(); // JIT on, Lua 5.5
lua.open_base(); lua.open_math();
let add = lua.create_function(|a: i64, b: i64| -> i64 { a + b });
lua.set_global("add", add)?;
let r: i64 = lua.eval("return add(40, 2)")?; // 42
let t = lua.create_table();
t.set(&mut lua, "name", "luna")?;
let name: String = t.get(&mut lua, "name")?;线程
默认的 Vm(以及 Lua 门面)是 !Send + !Sync——一个 Vm 及其 GC 堆上的每个句柄,都活在创建它的线程上。该约束由一个 compile_fail doctest 在编译期强制。
这是一个刻意的性能取舍:GC 使用 Gc<T> = NonNull<T>,建于侵入式标记-清除堆之上,没有 stop-the-world 协议,也没有加锁的 trace 缓存读取。让它变成 Send 会在真实负载上带来 5–15% 的开销。而 Lua 自身的数据模型本就是单线程的。
三种范式
- 单线程 Tokio —— 用 flavor = "current_thread" 把执行器跑在 Vm 所在的线程上,并使用 vm.eval_async(...)。
- 多线程 Tokio 上的 LocalSet —— LocalSet::run_until 把 Vm 的 future 钉在调用线程上。
- 每 OS 线程一个 Vm + 通道 —— 要真正的并行,就每线程一个 Vm;只有 Send 数据(源码字符串、结果字符串)在它们之间流转。
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut vm = Vm::new_minimal_with_jit(LuaVersion::Lua55);
vm.open_base();
let result = vm.eval_async("return 1 + 2").await?;
Ok(())
}安全与沙箱
luna 是一个嵌入式 VM:安全边界属于宿主,而非脚本。脚本能看到的每一项能力,都是 Rust 代码主动放行的。
在范围内
- 脚本驱动的 OS 访问外泄(文件系统、环境变量、网络、子进程)——超出宿主暴露范围的一律阻断。
- 资源耗尽——每次调用的指令预算与近似内存上限,会强制抛出一个可捕获的 Lua 错误。
- 字节码加载器逃逸——luna 自有的 dump 格式与 PUC .luac 加载在沙箱中默认关闭。
- 嵌入方表面零 unsafe——每个 cargo doc 可见的 API 都是安全 Rust。
不在范围内
- VM 正确性 bug(当作普通 bug 上报,而非沙箱逃逸)、侧信道抵抗,以及供应链完整性。
- 合法但病态脚本导致的拒绝服务——预算约束的是每次调用的 CPU 与内存,而非流向宿主接收端的输出量。
- 一旦打开的 debug.*,以及 os.execute / io.popen——这些等同于交出 Vm 或一个 shell。
部署
四种打包形态覆盖大多数宿主。luna 自身不做日志——可观测性通过调试钩子机制与 Vm::eval 周边的宿主计数器接入。
| 形态 | Crate | 说明 |
|---|---|---|
| 独立二进制,源码固化 | luna-aot | 构建期;宿主上无运行时 crate |
| Rust 服务,启用 JIT | luna-jit | 引入 Cranelift;跨线程用 feature = "send" |
| Rust 服务,纯解释器 | luna-core | 零第三方依赖 |
| WASM(浏览器 / wasmtime) | luna-core | wasm32-wasip1;JIT 关闭,io/os 打桩 |
要最小的容器,就静态链接 musl 并 FROM scratch 构建;拷贝前先 strip,可去掉 __LINKEDIT 的绝大部分。
FROM scratch
COPY ./target/x86_64-unknown-linux-musl/release/service /service
ENTRYPOINT ["/service"]JIT 与 AOT
在 luna-jit 下,trace JIT 默认开启。少量粘性旋钮让你为可预测的延迟与 A/B 测试而调优或关闭它。
| 旋钮 | 默认 | 作用 |
|---|---|---|
| set_jit_enabled(false) | true | 为可预测延迟 / 调试复现而关闭 |
| set_trace_jit_enabled(false) | true | 把 trace JIT 与解释器做 A/B 对比 |
| set_hot_threshold(n) | 常量 | 为立即变热的负载调低 |
| set_max_trace_len(n) | 常量 | 为长展开循环调高 |
热路径计数器(trace_compiled_count、trace_dispatched_count、trace_aborted_count、trace_deopt_count)帮助诊断一个负载是否真的获得了 JIT 加速。
提前编译
luna-aot 把一份 Lua 源码编译成自包含的原生二进制——解析、把字节码发射进数据段、在记录器下预热热 trace、链接静态运行时,并产出可执行文件。
cargo install luna-aot
luna-aot compile hello.lua --out hello
./hello # 独立原生二进制
# 从 macOS 宿主交叉编译到 Linux
luna-aot compile foo.lua --target x86_64-unknown-linux-gnu --out foo.linuxCranelift 的 all-arch 后端无需重建即可交叉编译到每个目标。一个 strip 后的 release 二进制落在 4.5 MiB 左右;运行时地板占主导,因此一个单行脚本与一个 1.5k 行脚本仅相差约 82 KiB。
兼容性
luna 在一个二进制里实现了 Lua 5.1 – 5.5 与 MacroLua。方言逐 Vm 选定;同一进程可同时承载多种。逐特性矩阵见首页的方言数据表。
标准库
适合沙箱嵌入的白名单子集通过 Vm::open_*() 方法暴露:
| 库 | 方法 | 覆盖 |
|---|---|---|
| base | open_base | 完整 |
| math | open_math | 完整 |
| string | open_string | 完整(含模式匹配) |
| table | open_table | 完整 |
| coroutine | open_coroutine | 完整 |
| io / os | open_io / open_os | 完整(宿主控制) |
| utf8 | open_utf8 | 完整(5.3+) |
| debug | — | 部分,默认不暴露 |
C API
luna 提供一个 cdylib / staticlib,暴露 lua.h 兼容的 C ABI 子集。现有链接 liblua 的 PUC 使用方,可在其覆盖范围内把 luna 当作 drop-in:状态生命周期、值 push/read、栈操作、表 API、lua_call / lua_pcall,以及脚本加载。一个 13 项的一致性测试套件为其兜底。userdata、C 侧协程与延续暂未覆盖——这些请用更丰富的 Rust API。
字节码
luna 生成与 PUC 编译器二进制格式一致的按方言字节码,因此 PUC 编译的 .luac 可直接加载,luna 转储的字节码也能在 PUC 中加载。字节码加载在沙箱中默认关闭——精心构造的字节码可绕过编译器强制的类型检查。
架构
五个可发布 crate。解释器核心不带任何第三方依赖;JIT 通过 trait 接入,因此替换或移除后端是 luna-jit 的事,永不触碰 luna-core 的 API。
| Crate | 依赖 | 表面 |
|---|---|---|
| luna-core | 0 第三方 | 词法、语法、编译器、解释器、运行时、标准库、GC、模式引擎、JIT trait |
| luna-jit-derive | syn + quote | #[derive(LuaUserdata)] proc-macro |
| luna-jit | luna-core + Cranelift ×6 | Cranelift 后端、C ABI、luna CLI、嵌入门面 |
| luna-runtime-helpers | luna-jit | AOT 二进制的静态运行时入口 |
| luna-aot | luna-core + luna-jit | 构建期 AOT 编译器 |
零依赖的 luna-core 契约由 CI 中的 cargo deny check 强制。源码被分为三类——石头(与业务无关的地基:模式引擎、堆、值布局)、钢筋(Lua 领域原语:编译器、分派器、JIT 后端)与水泥(宿主胶水:CLI、C ABI、标准库绑定)——各有各的改动纪律与 review 深度。
性能
luna 刻意不发布单一头条比率。一个胜出的 microbench 单元是营销产物;落后的那个才是值得追的信号。任何超过参考实现 1.5× 的差距,都会触发对该负载逐阶段、并排的分解——而非表层的打磨。
随发布交付的是可复现基线。内存在 dhat 下跨五个负载测量;任一稳态回归超过 5% 都是已跟踪的告警。
版本策略与稳定性
公共 API 被划分为一个稳定表面(破坏性改动需 SemVer 主版本递增)与一个不稳定 / 内部表面(可在小版本中为性能工作而变动)。
- 稳定:luna_jit 门面类型(Lua、LuaFunction、LuaTable、LuaRoot、LuaSandboxBuilder)、重新导出的核心(Vm、LuaVersion、Value、LuaError)、derive 宏,以及 lua.h 兼容的 C ABI。
- 不稳定 / 内部:luna_core::{compiler, frontend, jit, pattern} 与 JIT 后端内部——可为优化自由变动。
- 字节码:按方言的二进制格式是稳定的;PUC .luac 文件在整条线上都能加载。