ドキュメント
luna を Rust ホストに埋め込むために必要なすべて——3 行の hello から安定 API 契約まで。v2.16.0 時点のスナップショットです。rustdoc の API リファレンスは docs.rs にあります。
インストールと hello
luna は 5 つの公開可能な crate からなる Cargo ワークスペースです。合う依存量を選んでください。完全なインタプリタに Cranelift JIT と C ABI を加えた luna-jit、あるいは依存ゼロで wasm フレンドリーな luna-core。
# 完全なインタプリタ + Cranelift JIT + C ABI
luna-jit = "2"
# または最小の表面 —— 純インタプリタ、第三者依存ゼロ
luna-core = "2"選んだダイアレクトの Vm を構築して評価します。Vm::new は既定で安全な標準ライブラリをすべて開き、JIT を組み込みます(luna-core では no-op バックエンド)。
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 | インタプリタのみで実行(no-op JIT バックエンドを組み込む) |
| --profile | スクリプト終了後に trace-JIT カウンタを出力 |
| -e "<code>" | ファイルの代わりにインラインコードを実行 |
| - | stdin からソースを読む |
REPL は各行をまず式として評価し(return を前置)、構文エラーなら文として再試行します——式も代入も動きます。
埋め込み
埋め込み API が主要な表面です——C ABI より豊かで、完全に安全な所有権を備えます。以下は各能力の形です。完全なクックブックは proc-macro userdata と非同期表面を含む 14 節をカバーします。
信頼できないスクリプト向けサンドボックス
サンドボックスビルダーがライブラリをホワイトリスト化し、予算を武装します。サンドボックス化されたスクリプトは 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 に対応)。テーブルはワンショットとチェーンビルダーの 2 形態です。
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 プロトコルもロック付きのトレースキャッシュ読み取りもありません。Send にすると実ワークロードで 5〜15% のコストが乗ります。そもそも Lua 自身のデータモデルがシングルスレッドです。
3 つの定石パターン
- シングルスレッド Tokio —— flavor = "current_thread" で Vm のいるスレッド上に executor を走らせ、vm.eval_async(...) を使う。
- マルチスレッド Tokio 上の LocalSet —— LocalSet::run_until が Vm の future を呼び出しスレッドに固定する。
- OS スレッドごとに 1 つの Vm + チャネル —— 真の並列には、スレッドごとに Vm を 1 つ。スレッド間を渡るのは 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 の正しさに関するバグ(サンドボックスエスケープではなく通常のバグとして報告)、サイドチャネル耐性、サプライチェーンの完全性。
- 合法だが病的なスクリプトによる DoS——予算が縛るのは呼び出しごとの CPU とメモリであり、ホストのシンクへの出力量ではありません。
- 一度開いた debug.*、および os.execute / io.popen——これらは Vm やシェルを手渡すのと同義です。
デプロイ
4 つのパッケージ形態がほとんどのホストをカバーします。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 ではトレース JIT が既定で有効です。いくつかの粘着的なノブで、予測可能なレイテンシや A/B テストのために調整・無効化できます。
| ノブ | 既定 | 効果 |
|---|---|---|
| set_jit_enabled(false) | true | 予測可能なレイテンシ / デバッグ再現のため無効化 |
| set_trace_jit_enabled(false) | true | トレース 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 ソースを自己完結型のネイティブバイナリにコンパイルします——解析、バイトコードをデータセクションに発行、レコーダの下でホットトレースを温め、静的ランタイムをリンクし、実行ファイルを生成します。
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 行のスクリプトと 1.5k 行のスクリプトの差はわずか約 82 KiB です。
互換性
luna は 1 つのバイナリで Lua 5.1 – 5.5 と MacroLua を実装します。ダイアレクトは Vm ごとに選択され、1 つのプロセスが複数を同時にホストできます。機能別のマトリクスはトップページのダイアレクト仕様表を参照してください。
標準ライブラリ
サンドボックス埋め込みに適したホワイトリスト部分集合は、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 は lua.h 互換の C ABI 部分集合を公開する cdylib / staticlib を提供します。liblua にリンクしている既存の PUC 利用者は、そのカバー範囲で luna を drop-in として使えます:状態のライフサイクル、値の push/read、スタック操作、テーブル API、lua_call / lua_pcall、そしてスクリプトのロード。13 個の適合性テストがこれを固定します。userdata、C 側コルーチン、継続はまだ未対応——これらはより豊かな Rust API を使ってください。
バイトコード
luna は PUC コンパイラのバイナリ形式に一致するダイアレクト別バイトコードを発行するため、PUC でコンパイルした .luac はそのままロードでき、luna がダンプしたバイトコードも PUC でロードできます。バイトコードのロードはサンドボックスでは既定オフです——細工されたバイトコードは、コンパイラが強制する型チェックを迂回しうるためです。
アーキテクチャ
5 つの公開可能な crate。インタプリタコアは第三者依存を一切持ちません。JIT はトレイト経由で接続されるため、バックエンドの差し替えや削除は luna-jit の関心事であり、luna-core の API には決して触れません。
| Crate | 依存 | 表面 |
|---|---|---|
| luna-core | 第三者 0 | 字句・構文解析、コンパイラ、インタプリタ、ランタイム、標準ライブラリ、GC、パターンエンジン、JIT トレイト |
| 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 で強制されます。ソースは 3 層に分類されます——Stone(石)(業務非依存の基盤:パターンエンジン、ヒープ、値レイアウト)、Steel(鋼)(Lua ドメインのプリミティブ:コンパイラ、ディスパッチャ、JIT バックエンド)、Cement(コンクリート)(ホストのグルー:CLI、C ABI、標準ライブラリ束縛)——それぞれ変更規律とレビュー深度が異なります。
パフォーマンス
luna はあえて単一の見出し比率を出しません。勝つマイクロベンチのセルはマーケティングの産物であり、負けるセルこそ追う価値のあるシグナルです。参照実装に対して 1.5 倍を超える差があれば、表層の磨き込みではなく、その負荷を段階ごとに並べて分解します。
リリースに同梱されるのは再現可能なベースラインです。メモリは dhat の下で 5 つの負荷にわたり測定され、いずれかの定常状態で 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 ファイルは全ラインでロードできます。