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.
$ cargo install omnilua-cli
# Cargo.toml
omnilua = "0.2"
$ npm install omnilua
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.
$ 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.
$ 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):
$ 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:
$ 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.
[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.
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.
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.
// 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()?;
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.
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.
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.
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.
// `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
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, …).
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:
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.
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);
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:
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.
use omnilua::{Lua, LuaVersion}; let lua = Lua::new_versioned(LuaVersion::V51); // V51 … V55
$ 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).
$ omnilua /path/to/luarocks-3.11.1/src/bin/luarocks \
--tree ./rocks-tree install inspect
Native C rocks are not supported yet — only pure-Lua rocks. This is Lua source/runtime compatibility, not C API/ABI compatibility.