ドキュメント

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

Cargo.toml
# 完全なインタプリタ + Cranelift JIT + C ABI
luna-jit = "2"

# または最小の表面 —— 純インタプリタ、第三者依存ゼロ
luna-core = "2"

選んだダイアレクトの Vm を構築して評価します。Vm::new は既定で安全な標準ライブラリをすべて開き、JIT を組み込みます(luna-core では no-op バックエンド)。

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-jitluna-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)
--sandboxbase/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 もファイルシステムへのアクセスも、バイトコードチャンクのコンパイルもできません。

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 に対応)。テーブルはワンショットとチェーンビルダーの 2 形態です。

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)
破れば即 unsound な唯一の規則。userdata が Gc<…> フィールドを 1 つでも持つなら、trace をオーバーライドし、すべてのハンドルを m.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> として現れます。LuaErrorDisplay / 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 プロトコルもロック付きのトレースキャッシュ読み取りもありません。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 なデータ(ソース文字列、結果文字列)だけ。
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 をまたいで保持できます。現状はインタプリタ専用で、トレース JIT は SendVm 上では動きません。実測オーバーヘッドはおおむねノイズの範囲です。Send + JIT が必要なら、スレッドごとに Vm のパターンを使ってください。

セキュリティとサンドボックス

luna は埋め込み VM です。セキュリティ境界を握るのはスクリプトではなくホストです。スクリプトが見るすべての能力は、Rust コードが明示的に許可したものです。

対象範囲

  • スクリプト由来の OS アクセスによる漏洩(ファイルシステム、環境変数、ネットワーク、子プロセス)——ホストが公開した範囲を超えるものは遮断。
  • リソース枯渇——呼び出しごとの命令予算と概算メモリ上限が、捕捉可能な Lua エラーを強制します。
  • バイトコードローダーのエスケープ——luna 自身の dump 形式と PUC .luac ロードはサンドボックスで既定オフ。
  • 埋め込み表面のゼロ unsafe——cargo doc で見えるすべての API は安全な Rust。

対象外

  • VM の正しさに関するバグ(サンドボックスエスケープではなく通常のバグとして報告)、サイドチャネル耐性、サプライチェーンの完全性。
  • 合法だが病的なスクリプトによる DoS——予算が縛るのは呼び出しごとの CPU とメモリであり、ホストのシンクへの出力量ではありません。
  • 一度開いた debug.*、および os.execute / io.popen——これらは Vm やシェルを手渡すのと同義です。
意図した非対称性。安全な部分集合の標準ライブラリ(basemathstringtablecoroutine)はビルダー上で開きます。OS に触れるライブラリ(io/osdebugpackage)は、ビルド後のハンドル——vm.open_os_io() など——を要求し、その信頼判断が常にホストコードに現れるようにします。予算は武装するまで無制限で、メモリ上限は発火後に自ら解除されるため、リクエスト間で Vm を再利用する前に再武装が必要です。

デプロイ

4 つのパッケージ形態がほとんどのホストをカバーします。luna 自身はロギングを行いません——可観測性はデバッグフック機構と Vm::eval 周りのホストカウンタで接続します。

形態Crate備考
単独バイナリ、ソース固定luna-aotビルド時;ホストにランタイム crate なし
Rust サービス、JIT 有効luna-jitCranelift を引き込む;クロススレッドは 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 ではトレース 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_counttrace_dispatched_counttrace_aborted_counttrace_deopt_count)は、その負荷が実際に JIT の高速化を得ているかの診断に役立ちます。

事前コンパイル

luna-aot は Lua ソースを自己完結型のネイティブバイナリにコンパイルします——解析、バイトコードをデータセクションに発行、レコーダの下でホットトレースを温め、静的ランタイムをリンクし、実行ファイルを生成します。

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 行のスクリプトと 1.5k 行のスクリプトの差はわずか約 82 KiB です。

互換性

luna は 1 つのバイナリで Lua 5.1 – 5.5 と MacroLua を実装します。ダイアレクトは Vm ごとに選択され、1 つのプロセスが複数を同時にホストできます。機能別のマトリクスはトップページのダイアレクト仕様表を参照してください。

標準ライブラリ

サンドボックス埋め込みに適したホワイトリスト部分集合は、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 は 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-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 で強制されます。ソースは 3 層に分類されます——Stone(石)(業務非依存の基盤:パターンエンジン、ヒープ、値レイアウト)、Steel(鋼)(Lua ドメインのプリミティブ:コンパイラ、ディスパッチャ、JIT バックエンド)、Cement(コンクリート)(ホストのグルー:CLI、C ABI、標準ライブラリ束縛)——それぞれ変更規律とレビュー深度が異なります。

パフォーマンス

luna はあえて単一の見出し比率を出しません。勝つマイクロベンチのセルはマーケティングの産物であり、負けるセルこそ追う価値のあるシグナルです。参照実装に対して 1.5 倍を超える差があれば、表層の磨き込みではなく、その負荷を段階ごとに並べて分解します。

リリースに同梱されるのは再現可能なベースラインです。メモリは dhat の下で 5 つの負荷にわたり測定され、いずれかの定常状態で 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.md にあります。docs.rs の rustdoc リファレンスが項目ごとの正典です。