文档

把 luna 嵌入 Rust 宿主所需的一切——从三行的 hello 到稳定 API 契约。以 v2.16.0 为快照。rustdoc API 参考在 docs.rs

安装与 hello

luna 是一个含五个可发布 crate 的 Cargo 工作区。挑选合适的依赖体量:luna-jit 提供完整解释器加 Cranelift JIT 与 C ABI,luna-core 则是零依赖、对 wasm 友好的解释器。

Cargo.toml
# 完整解释器 + Cranelift JIT + C ABI
luna-jit = "2"

# 或者最小表面 —— 纯解释器,零第三方依赖
luna-core = "2"

为选定的方言构造一个 Vm 并求值。Vm::new 会打开所有默认安全的标准库并装上 JIT(在 luna-core 下是一个空操作后端)。

main.rs
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}");
}
选哪个 crate? luna-jit 重新导出了 luna-core 的一切,因此你可以直接从 JIT crate 起步,不必操心拆分。只有在你想要极小审计面、极快构建或 wasm32 目标时,才需要直接用 luna-core

命令行界面

安装 luna-jit 会在 PATH 上放一个 luna 二进制——一个 REPL 兼脚本运行器。

shell
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、触碰文件系统,或编译字节码块。

sandbox.rs
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)。表提供一次性与链式构造器两种形态。

globals.rs
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 元。

natives.rs
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 会生成样板代码。

userdata.rs
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)
唯一一条破坏即不安全的规则。若一个 userdata 持有任何 Gc<…> 字段,你必须重写 tracem.mark(...) 每一个句柄——在 trace 内你只能标记,绝不可调用 Vm、分配或加锁。缺失 trace 即是 use-after-free。derive 宏会替你处理好这件事。

从 Rust 驱动协程与调试钩子

无需 Lua 侧的 coroutine.create,用 create_coroutine / resume_coroutine 即可从 Rust 驱动 Lua 协程。用 set_rust_debug_hook 在 Call / Return / Line / Count / TailCall 事件上装一个 Rust 回调。

coroutine.rs
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 取得。

errors.rs
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 句柄。

facade.rs
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 数据(源码字符串、结果字符串)在它们之间流转。
pattern-1.rs
#[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(())
}
可选的 SendVmfeature = "send" 开关增加一个 SendVm newtype,它是 Send(但非 Sync),因此其克隆可跨线程移动,或在多线程运行时里跨 .await 持有。它目前仅解释器可用——trace JIT 不在 SendVm 上运行。实测开销大致落在噪声内。若要 Send + JIT,请用每线程一个 Vm 的范式。

安全与沙箱

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。
刻意的不对称。安全子集的标准库(basemathstringtablecoroutine)在构造器上打开。触碰 OS 的库(io/osdebugpackage)则需要一个构建后的句柄——vm.open_os_io() 之类——使这个信任决策始终显现在宿主代码里。预算在你武装之前是无界的;内存上限在触发后会自行解除,跨请求复用 Vm 前需重新武装。

部署

四种打包形态覆盖大多数宿主。luna 自身不做日志——可观测性通过调试钩子机制与 Vm::eval 周边的宿主计数器接入。

形态Crate说明
独立二进制,源码固化luna-aot构建期;宿主上无运行时 crate
Rust 服务,启用 JITluna-jit引入 Cranelift;跨线程用 feature = "send"
Rust 服务,纯解释器luna-core零第三方依赖
WASM(浏览器 / wasmtime)luna-corewasm32-wasip1;JIT 关闭,io/os 打桩

要最小的容器,就静态链接 musl 并 FROM scratch 构建;拷贝前先 strip,可去掉 __LINKEDIT 的绝大部分。

Dockerfile
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_counttrace_dispatched_counttrace_aborted_counttrace_deopt_count)帮助诊断一个负载是否真的获得了 JIT 加速。

提前编译

luna-aot 把一份 Lua 源码编译成自包含的原生二进制——解析、把字节码发射进数据段、在记录器下预热热 trace、链接静态运行时,并产出可执行文件。

shell
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.linux

Cranelift 的 all-arch 后端无需重建即可交叉编译到每个目标。一个 strip 后的 release 二进制落在 4.5 MiB 左右;运行时地板占主导,因此一个单行脚本与一个 1.5k 行脚本仅相差约 82 KiB。

兼容性

luna 在一个二进制里实现了 Lua 5.1 – 5.5 与 MacroLua。方言逐 Vm 选定;同一进程可同时承载多种。逐特性矩阵见首页的方言数据表

标准库

适合沙箱嵌入的白名单子集通过 Vm::open_*() 方法暴露:

方法覆盖
baseopen_base完整
mathopen_math完整
stringopen_string完整(含模式匹配)
tableopen_table完整
coroutineopen_coroutine完整
io / osopen_io / open_os完整(宿主控制)
utf8open_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-core0 第三方词法、语法、编译器、解释器、运行时、标准库、GC、模式引擎、JIT trait
luna-jit-derivesyn + quote#[derive(LuaUserdata)] proc-macro
luna-jitluna-core + Cranelift ×6Cranelift 后端、C ABI、luna CLI、嵌入门面
luna-runtime-helpersluna-jitAOT 二进制的静态运行时入口
luna-aotluna-core + luna-jit构建期 AOT 编译器

零依赖的 luna-core 契约由 CI 中的 cargo deny check 强制。源码被分为三类——石头(与业务无关的地基:模式引擎、堆、值布局)、钢筋(Lua 领域原语:编译器、分派器、JIT 后端)与水泥(宿主胶水:CLI、C ABI、标准库绑定)——各有各的改动纪律与 review 深度。

性能

luna 刻意不发布单一头条比率。一个胜出的 microbench 单元是营销产物;落后的那个才是值得追的信号。任何超过参考实现 1.5× 的差距,都会触发对该负载逐阶段、并排的分解——而非表层的打磨。

随发布交付的是可复现基线。内存在 dhat 下跨五个负载测量;任一稳态回归超过 5% 都是已跟踪的告警。

33KB
cold_start 峰值 · 435 次分配
71KB
repl_idle · 100 次求值
523KB
alloc_collect 稳态 · 1M + 10 GC
63KB
userdata_lifecycle · 200 + 终结器

版本策略与稳定性

公共 API 被划分为一个稳定表面(破坏性改动需 SemVer 主版本递增)与一个不稳定 / 内部表面(可在小版本中为性能工作而变动)。

  • 稳定:luna_jit 门面类型(LuaLuaFunctionLuaTableLuaRootLuaSandboxBuilder)、重新导出的核心(VmLuaVersionValueLuaError)、derive 宏,以及 lua.h 兼容的 C ABI。
  • 不稳定 / 内部:luna_core::{compiler, frontend, jit, pattern} 与 JIT 后端内部——可为优化自由变动。
  • 字节码:按方言的二进制格式是稳定的;PUC .luac 文件在整条线上都能加载。
完整的稳定 / 不稳定条目清单见嵌入手册的 API 契约小节与 CHANGELOG.mddocs.rs 上的 rustdoc 参考是逐条目的权威来源。