luars 0.14.1

A library for lua 5.5 runtime implementation in Rust
Documentation

luars

License Crates.io

A Lua 5.5 interpreter written in pure Rust — embeddable, async-capable, with derive macros for UserData.

Features

  • Lua 5.5 — full language semantics: compiler, register-based VM, GC
  • Pure Rust — no C dependencies, no unsafe FFI
  • Ergonomic APIcall_global, register_function, load, dofile, TableBuilder, typed getters
  • UserData — derive macros to expose Rust structs/enums to Lua (fields, methods, operators)
  • Async — run async Rust functions from Lua via transparent coroutine bridging
  • Closures — register Rust closures with captured state as Lua globals
  • FromLua / IntoLua — automatic type conversion for seamless Rust ↔ Lua interop
  • Optional Serde — JSON serialization via serde / serde_json (feature-gated)
  • Standard Librariesbasic, string, table, math, io, os, coroutine, utf8, package, partial debug

Usage

[dependencies]

luars = "0.12"



# With JSON support:

luars = { version = "0.12", features = ["serde"] }

Basic Example

use luars::{LuaVM, Stdlib, LuaValue};
use luars::lua_vm::SafeOption;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut vm = LuaVM::new(SafeOption::default());
    vm.open_stdlib(Stdlib::All)?;

    // Execute Lua code
    let results = vm.execute("return 1 + 2")?;
    assert_eq!(results[0].as_integer(), Some(3));

    // Set / get globals
    vm.set_global("x", LuaValue::integer(42))?;
    let x: i64 = vm.get_global_as::<i64>("x")?.unwrap();

    // Call a Lua function
    vm.execute("function add(a,b) return a+b end")?;
    let sum = vm.call_global("add", vec![LuaValue::integer(1), LuaValue::integer(2)])?;
    assert_eq!(sum[0].as_integer(), Some(3));

    Ok(())
}

Register Rust Functions

vm.register_function("greet", |state| {
    let name = state.get_arg(1).and_then(|v| v.as_str().map(String::from))
        .unwrap_or_else(|| "World".into());
    let msg = state.create_string(&format!("Hello, {}!", name))?;
    state.push_value(msg)?;
    Ok(1)
})?;

vm.execute("print(greet('Rust'))")?;  // Hello, Rust!

Load, Dofile, Call

// Compile without executing
let f = vm.load("return 1 + 1")?;
let results = vm.call(f, vec![])?;

// Load named source
let f = vm.load_with_name("return 42", "my_chunk")?;

// Execute a file
vm.dofile("scripts/init.lua")?;

TableBuilder

use luars::TableBuilder;

let config = TableBuilder::new()
    .set("host", vm.create_string("localhost")?)
    .set("port", LuaValue::integer(8080))
    .push(LuaValue::integer(1))
    .build(&mut vm)?;
vm.set_global("config", config)?;

// Iterate
for (k, v) in vm.table_pairs(&config)? {
    println!("{:?} = {:?}", k, v);
}
let len = vm.table_length(&config)?;

Rust Closures

use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};

let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let func = vm.create_closure(move |state| {
    let n = counter_clone.fetch_add(1, Ordering::SeqCst);
    state.push_value(LuaValue::integer(n as i64))?;
    Ok(1)
})?;
vm.set_global("next_id", func)?;

UserData

use luars::{LuaUserData, lua_methods};

#[derive(LuaUserData)]
#[lua_impl(Display)]
struct Point { pub x: f64, pub y: f64 }

#[lua_methods]
impl Point {
    pub fn new(x: f64, y: f64) -> Self { Point { x, y } }
    pub fn distance(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() }
}

vm.register_type_of::<Point>("Point")?;
vm.execute(r#"
    local p = Point.new(3, 4)
    print(p.x, p:distance())   -- 3.0  5.0
"#)?;

Error Handling

use luars::LuaError;

match vm.execute("error('boom')") {
    Ok(_) => {}
    Err(e) => {
        // Lightweight message
        let msg = vm.get_error_message(e);
        eprintln!("Lua error: {}", msg);

        // Rich error (implements std::error::Error)
        let full = vm.into_full_error(e);
        eprintln!("{}", full);
    }
}

LuaError is a 1-byte enum (Runtime, Syntax, OutOfMemory, MessageHandler, IndexOutOfBounds). For rich errors with messages and std::error::Error impl, use vm.into_full_error()LuaFullError.

Selective Standard Libraries

use luars::Stdlib;

vm.open_stdlib(Stdlib::All)?;                          // everything
vm.open_stdlibs(&[Stdlib::Base, Stdlib::String])?;     // specific set

Async

use luars::AsyncReturnValue;

vm.register_async("fetch", |args| async move {
    let url = args[0].as_str().unwrap_or("").to_string();
    let body = reqwest::get(&url).await?.text().await?;
    Ok(vec![AsyncReturnValue::string(body)])
})?;

let results = vm.execute_async("return fetch('https://example.com')").await?;

Known Limitations

  • UTF-8 Only Strings — unlike C Lua, strings must be valid UTF-8. Use string.pack/string.unpack for binary data.
  • Custom Bytecode Formatstring.dump output is not compatible with C Lua bytecode.
  • No C API — pure Rust; cannot load C Lua modules.
  • Partial Debug Librarydebug.sethook is a stub; getinfo/getlocal/traceback work.
  • No string-to-number coercion in arithmetic"3" + 1 raises an error.

Documentation

Document Description
Guide VM, execution, values, functions, errors, API reference
UserData Guide Derive macros, fields, methods, constructors
Async Guide Async Rust functions, architecture, HTTP server example
Differences Behavioral differences from C Lua 5.5

License

MIT — see LICENSE.