omniLua

Using omniLua

Three ways to run Lua with one pure-Rust runtime — a CLI, a Rust embedding crate, and a wasm package. Every example below is real and copy-pasteable.

Getting started

Pick the surface you need. They share one core, so behaviour matches across all three.

Command line
$ cargo install omnilua-cli
Embed in Rust
# Cargo.toml
omnilua = "0.2"
Browser / Node
$ npm install omnilua
Note

The crate is omnilua-cli but the binary it installs is omnilua. The embedding crate and the npm package are both named omnilua.

New here? Run a one-liner to confirm the install, then jump to your path below.

terminal sh
$ omnilua -e 'print("hello, omniLua")'
hello, omniLua

The CLI

The omnilua binary is a drop-in lua: run a file, evaluate an expression, pipe a program in, or open a REPL.

terminal sh
$ omnilua script.lua                # run a file
$ omnilua -e 'print(1 + 2)'        # evaluate an expression
$ echo 'print("hi")' | omnilua -    # read a program from stdin
$ omnilua                            # no args → interactive REPL
$ omnilua -i script.lua             # run a file, then drop into the REPL
$ omnilua -l inspect script.lua     # require a module before the script
$ omnilua -v                         # print the version banner

Choose a Lua version with the OMNILUA_VERSION environment variable (5.1 through 5.5; default 5.4):

terminal sh
$ OMNILUA_VERSION=5.1 omnilua script.lua

Run untrusted scripts under hard CPU and memory caps. Limits are uncatchable from inside Lua and --sandbox also strips host-access globals:

terminal sh
$ omnilua --sandbox --max-instructions=5000000 --max-memory=64M untrusted.lua

Embedding in Rust

The omnilua crate is an embedding API shaped after mlua — but pure Rust, with no liblua, no C toolchain, and no build script. The same code compiles to wasm32-unknown-unknown.

Cargo.toml toml
[dependencies]
omnilua = "0.2"

Functions in these examples return omnilua::Result<T>, so ? propagates Lua errors as an omnilua::Error (see Errors).

Run a chunk & read the result back

lua.load(src) compiles a chunk; .exec() runs it and discards results, while .eval::<T>() runs it and converts the return value into a Rust type.

main.rs rust
use omnilua::{Lua, Result};

fn main() -> Result<()> {
    let lua = Lua::new();

    // Run a chunk and discard its result.
    lua.load(r#"print("hello from Lua")"#).exec()?;

    // Run a chunk and read a typed value back.
    let answer: i64 = lua.load("return 6 * 7").eval()?;
    assert_eq!(answer, 42);
    Ok(())
}

Pass Rust values in

Set globals from Rust with lua.globals().set(name, value). Numbers, booleans, strings, tables, and functions all convert automatically.

rust rust
lua.globals().set("name", "omniLua")?;
lua.globals().set("answer", 42)?;
lua.load(r#"print(name .. " => " .. answer)"#).exec()?;

Register Rust functions

lua.create_function turns a Rust closure into a Lua-callable function. Its first argument is the &Lua; the second is the call arguments — a single type, or a tuple for several. Return any convertible value.

rust rust
// One argument in, one value out.
let greet = lua.create_function(|_, who: String| {
    Ok(format!("hello, {who}"))
})?;
lua.globals().set("greet", greet)?;

// Several arguments arrive as a tuple.
let add = lua.create_function(|_, (a, b): (i64, i64)| Ok(a + b))?;
lua.globals().set("add", add)?;

let msg: String = lua.load(r#"return greet("world")"#).eval()?;
let sum: i64 = lua.load("return add(2, 3)").eval()?;
Note

Use create_function_mut when the closure needs to capture mutable state. A re-entrant call errors with "already borrowed" rather than aliasing.

Tables

Build tables in Rust with lua.create_table(), or read one back out of Lua by evaluating to omnilua::Table.

rust rust
use omnilua::{Lua, Result, Table};

// Build a table in Rust and hand it to Lua.
let config = lua.create_table()?;
config.set("host", "localhost")?;
config.set("port", 8080)?;
lua.globals().set("config", config)?;

// Read a table back out of Lua.
let point: Table = lua.load("return { x = 3, y = 4 }").eval()?;
let x: i64 = point.get("x")?;

Errors

A Lua error surfaces in Rust as an omnilua::Error. It Displays the full message, derefs to the inner LuaError, and — importantly — preserves the raised Lua value, so re-raising it through pcall/xpcall returns the original object.

rust rust
use omnilua::{Lua, Result};

let result: Result<()> = lua.load("error('boom')").exec();
match result {
    Ok(()) => {}
    Err(err) => {
        // Display gives the full message + traceback.
        eprintln!("{err}");
        // Or inspect it: err.kind() / err.as_lua_error() / err.message_lossy()
        let _msg = err.message_lossy();
    }
}

Custom types (UserData)

Expose a Rust type to Lua by implementing UserData and registering methods, fields, and metamethods. Hand an owned instance to Lua with lua.create_userdata.

rust rust
use omnilua::{Lua, Result, UserData, UserDataMethods};

struct Counter { n: i64 }

impl UserData for Counter {
    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
        m.add_method("get", |_, this, ()| Ok(this.n));
        m.add_method_mut("add", |_, this, by: i64| {
            this.n += by;
            Ok(this.n)
        });
        m.add_field_method_get("n", |_, this| Ok(this.n));
    }
}

let counter = lua.create_userdata(Counter { n: 0 })?;
lua.globals().set("counter", counter)?;
lua.load(r#"
    counter:add(5)
    print(counter:get())   -- 5
    print(counter.n)       -- 5
"#).exec()?;

Operator overloading and other metamethods go in add_meta_methods via the MetaMethod enum (Add, Eq, Index, ToString, …).

Lend borrowed (non-'static) state

Most embedding APIs require 'static data. lua.scope lends a borrow — say a game engine's &mut World — to Lua for exactly one call. A handle that escapes the closure errors cleanly instead of dangling, and mutations are visible to the host afterwards.

rust rust
// `Counter` from above, but borrowed from the host for one call.
let mut counter = Counter { n: 0 };
lua.scope(|s| {
    let c = s.create_userdata_ref_mut(&lua, &mut counter)?;
    lua.globals().set("counter", &c)?;
    lua.load("counter:add(10)").exec()
})?;
assert_eq!(counter.n, 10); // the host sees the mutation
Reference

The full embedding API — every conversion, create_function_mut, the scope function variants, and the #[derive(LuaUserData)] macro — lives on docs.rs. A runnable Bevy example ships in the project repository.

Sandboxing untrusted scripts

Lua::sandboxed returns a Lua state plus a Sandbox handle. The budget is uncatchable from inside Lua; SandboxConfig::strict() also removes host-access globals (os.execute, io, load, require, debug, …).

rust rust
use omnilua::{Lua, SandboxConfig, TripReason};

// strict(): 10M instructions, 64 MiB, host globals removed.
let (lua, sandbox) = Lua::sandboxed(SandboxConfig::strict())?;

let result = lua.load("while true do end").exec();
assert!(result.is_err());
assert_eq!(sandbox.tripped(), Some(TripReason::Instructions));

sandbox.reset(); // refill the budget before the next run

Or configure the limits yourself — any field may be None to leave it uncapped:

rust rust
let config = SandboxConfig {
    instruction_limit: Some(5_000_000),
    memory_limit_bytes: Some(32 * 1024 * 1024),
    check_interval: 256,
    remove_globals: vec![b"os".to_vec(), b"io".to_vec()],
};
let (lua, sandbox) = Lua::sandboxed(config)?;

Browser & Node

The npm package omnilua ships the whole runtime as one .wasm module — no Emscripten, no bundled C interpreter. loadLuaRs is for the browser; loadLuaRsNode is for Node.

browser js
import { loadLuaRs, luaRsWasmUrl } from "omnilua";

const { lua } = await loadLuaRs(luaRsWasmUrl, {
  onStdout: (chunk) => console.log(chunk),
});

lua.exec('print("hello from wasm")');

// tryExec returns a result object instead of throwing.
const r = lua.tryExec("return 1 +");   // syntax error
console.log(r.ok, r.error);
node js
import { loadLuaRsNode } from "omnilua/node";

const { lua } = await loadLuaRsNode({
  onStdout: (chunk) => process.stdout.write(chunk),
});
lua.exec('print("hello from node")');

Sandboxing is exposed over the wasm ABI too:

js js
lua.setLimits({ maxInstructions: 5_000_000, maxMemory: 64 * 1024 * 1024, strict: true });
const r = lua.tryExec("while true do end");
console.log(r.ok);           // false
console.log(lua.lastTrip());  // "instructions"
lua.sandboxReset();           // refill for the next run

Versions

One core runs Lua 5.1, 5.2, 5.3, 5.4, and 5.5, selected per instance. The version is resolved once on a cold path, so the bytecode loop carries no per-version cost.

rust rust
use omnilua::{Lua, LuaVersion};
let lua = Lua::new_versioned(LuaVersion::V51); // V51 … V55
cli / browser sh · js
$ OMNILUA_VERSION=5.3 omnilua script.lua

// wasm: choose at load, or switch in place
const { lua } = await loadLuaRs(luaRsWasmUrl, { version: "5.3" });
lua.setVersion("5.1");

See the differences run live, one snippet across all five, in the playground.

LuaRocks

omniLua runs the stock LuaRocks 3.11.1 client — it is just a Lua program — and installs pure-Lua rocks (inspect, dkjson, argparse, middleclass, say, luassert).

terminal sh
$ omnilua /path/to/luarocks-3.11.1/src/bin/luarocks \
      --tree ./rocks-tree install inspect
Note

Native C rocks are not supported yet — only pure-Lua rocks. This is Lua source/runtime compatibility, not C API/ABI compatibility.

Going further