#lua #scripting #nostd #async

nightly no-std ezlua

Ergonomic, efficient and Zero-cost rust bindings to Lua5.4

9 releases

0.4.4 Nov 19, 2023
0.4.3 Nov 4, 2023
0.4.2 Oct 28, 2023
0.3.3 Oct 5, 2023
0.1.1 May 10, 2023

#22 in No standard library

Download history 3/week @ 2023-08-20 17/week @ 2023-08-27 25/week @ 2023-09-03 11/week @ 2023-09-10 4/week @ 2023-09-17 7/week @ 2023-09-24 33/week @ 2023-10-01 17/week @ 2023-10-08 130/week @ 2023-10-15 141/week @ 2023-10-22 192/week @ 2023-10-29 16/week @ 2023-11-05 6/week @ 2023-11-12 47/week @ 2023-11-19 65/week @ 2023-11-26 19/week @ 2023-12-03

138 downloads per month
Used in udbg

MIT license


crates.io docs.rs Build Status Coverage Status

ChangeLog | FAQ | Known issues

Ergonomic, efficient and Zero-cost rust bindings to Lua5.4


  • Serialization (serde) support
  • Async function bindings support
  • Ergonomic binding for functions and userdata methods
  • Ergonomic stack values operation, you don't need to pay attention to the stack details
  • Efficient: no auxiliary stack, support reference type conversion
  • Builtin bindings to most commonly used rust std functions and types
  • Mutilple thread support
  • nostd support


  • Nightly rust compiler needed (1.70+)
  • Only support lua5.4 currently


See builtin bindings tests


Feature flags

  • async: enable async/await support (any executor can be used, eg. [tokio] or [async-std])
  • serde: add serialization and deserialization support to ezlua types using [serde] framework
  • vendored: build static Lua library from sources during ezlua compilation using [lua-src] crates
  • thread enable the multiple thread support
  • std: enable the builtin bindings for rust std functions and types
  • json: enable the builtin bindings for [serde_json] crate
  • regex: enable the builtin bindings for [regex] crate
  • tokio: enable the builtin bindings for [tokio] crate


First, add ezlua to your dependencies in Cargo.toml

ezlua = { version = '0.4' }

Then, use ezlua in rust, the code framework like this

use ezlua::prelude::*;

fn main() -> LuaResult<()> {
    // create a lua VM
    let lua = Lua::with_open_libs();

    // load your lua script and execute it
    lua.do_string(r#"function add(a, b) return a + b end"#, None)?;

    // get function named add from lua global table
    let add = lua.global().get("add")?;

    // call add function and get its result
    let result = add.pcall::<_, u32>((111, 222))?;
    assert_eq!(result, 333);

    // ... for the following code


Bind your function

Of course, you can provide your rust function to lua via ezlua binding, and it's very simple, like this

lua.global().set("add", lua.new_closure(|a: u32, b: u32| a + b)?)?;
lua.do_string("assert(add(111, 222) == 333)", None)?;

And you can bind exists function easily

let string: LuaTable = lua.global().get("string")?.try_into()?;
string.set_closure("trim", str::trim)?;
string.set_closure("trim_start", str::trim_start)?;
string.set_closure("trim_end", str::trim_end)?;

let os: LuaTable = lua.global().get("os")?.try_into()?;
os.set_closure("mkdir", std::fs::create_dir::<&str>)?;
os.set_closure("mkdirs", std::fs::create_dir_all::<&str>)?;
os.set_closure("rmdir", std::fs::remove_dir::<&str>)?;
os.set_closure("chdir", std::env::set_current_dir::<&str>)?;
os.set_closure("getcwd", std::env::current_dir)?;
os.set_closure("getexe", std::env::current_exe)?;

Bind your type

Implement ToLua trait for your type, and then you can pass it to lua

#[derive(Debug, Default)]
struct Config {
    name: String,
    path: String,
    timeout: u64,
    // ...

impl ToLua for Config {
    fn to_lua<'a>(self, lua: &'a LuaState) -> LuaResult<ValRef<'a>> {
        let conf = lua.new_table()?;
        conf.set("name", self.name)?;
        conf.set("path", self.path)?;
        conf.set("timeout", self.timeout)?;

lua.global().set_closure("default_config", Config::default)?;

Simply bindings via serde

Continuing with the example above, you can simply the binding code via serde

use serde::{Deserialize, Serialize};
use ezlua::serde::SerdeValue;

#[derive(Debug, Default, Deserialize, Serialize)]
struct Config {
    name: String,
    path: String,
    timeout: u64,
    // ...

// You can use impl_tolua_as_serde macro to simply this after version v0.3.1
// ezlua::impl_tolua_as_serde!(Config);
impl ToLua for Config {
    fn to_lua<'a>(self, lua: &'a LuaState) -> LuaResult<ValRef<'a>> {

// You can use impl_fromlua_as_serde macro to simply this after version v0.3.1
// ezlua::impl_fromlua_as_serde!(Config);
impl FromLua<'_> for Config {
    fn from_lua(lua: &LuaState, val: ValRef) -> LuaResult<Self> {
        SerdeValue::<Self>::from_lua(lua, val).map(|s| s.0)

lua.global().set("DEFAULT_CONFIG", SerdeValue(Config::default()))?;
    .set_closure("set_config", |config: Config| {
        // ... set your config

Bind custom object (userdata)

ezlua's userdata binding mechanism is powerful, the following code comes from std bindings

use std::{fs::Metadata, path::*};

impl UserData for Metadata {
    fn getter(fields: UserdataRegistry<Self>) -> Result<()> {
        fields.set_closure("size", Self::len)?;
        fields.set_closure("modified", Self::modified)?;
        fields.set_closure("created", Self::created)?;
        fields.set_closure("accessed", Self::accessed)?;
        fields.set_closure("readonly", |this: &Self| this.permissions().readonly())?;


    fn methods(mt: UserdataRegistry<Self>) -> Result<()> {
        mt.set_closure("len", Self::len)?;
        mt.set_closure("is_dir", Self::is_dir)?;
        mt.set_closure("is_file", Self::is_file)?;
        mt.set_closure("is_symlink", Self::is_symlink)?;


Types which impls the UserData trait, ezlua also impls ToLua for it, and impls FromLua for its reference

lua.global().set("path_metadata", Path::metadata)?;

Defaultly, types binded as userdata is immutable, if you need mutable reference, you can specific a UserData::Trans type, and there is a builtin impl that is RefCell, so the mutable binding impls looks like this

use core::cell::RefCell;
use std::process::{Child, Command, ExitStatus, Stdio};

impl UserData for Child {
    type Trans = RefCell<Self>;

    fn getter(fields: UserdataRegistry<Self>) -> LuaResult<()> {
        fields.add("id", Self::id)?;


    fn methods(mt: UserdataRegistry<Self>) -> Result<()> {
        mt.add_mut("kill", Self::kill)?;
        mt.add_mut("wait", Self::wait)?;

        mt.add_mut("try_wait", |this: &mut Self| {

Under normal circumstances, you need only impl the getter/setter/methods methods when impl the UserData trait, which allows you "read property"/"write property"/"call method" through the userdata value, but also ezlua provides more powerful features for UserData, such as "uservalue access" and "userdata cache".

In order to enable the "uservalue access" feature for an userdata type, just needs to specify const INDEX_USERVALUE: bool = true

struct Test {
    a: i32,

impl UserData for Test {
    type Trans = RefCell<Self>;

    const INDEX_USERVALUE: bool = true;

    fn methods(mt: UserdataRegistry<Self>) -> LuaResult<()> {
        mt.set_closure("inc", |mut this: RefMut<Self>| this.a += 1)?;

let uv = lua.new_val(Test { a: 0 })?;
lua.global().set("uv", uv)?;
lua.do_string("uv.abc = 3; assert(uv.abc == 3)", None)?;
lua.do_string("assert(debug.getuservalue(uv).abc == 3)", None)?;

In order to enable the "userdata cache" feature for an userdata type, you should impl the UserData::key_to_cache method, which returns a pointer, as a lightuserdata key in the cache table in lua.

#[derive(derive_more::Deref, Clone)]
struct RcTest(Rc<Test>);

impl UserData for RcTest {
    fn key_to_cache(&self) -> *const () {
        self.as_ref() as *const _ as _

    fn getter(fields: UserdataRegistry<Self>) -> LuaResult<()> {
        fields.set_closure("a", |this: &Self| this.a)?;

    fn methods(_: UserdataRegistry<Self>) -> LuaResult<()> {

let test = RcTest(Test { a: 123 }.into());
lua.global().set("uv", test.clone())?;
// when converting an UserData type to lua value, ezlua will first use the userdata in the cache table if existing,
// otherwise, create a new userdata and insert it to the cache table, so the "uv" and "uv1" will refer to the same userdata object
lua.global().set("uv1", test.clone())?;
lua.do_string("print(uv, uv1)", None)?;
lua.do_string("assert(uv == uv1)", None)?;

Register your own module

To register a lua module, you can provide a rust function return a lua table via LuaState::register_module method

lua.register_module("json", ezlua::binding::json::open, false)?;
lua.register_module("path", |lua| {
    let t = lua.new_table()?;

    t.set_closure("dirname", Path::parent)?;
    t.set_closure("exists", Path::exists)?;
    t.set_closure("abspath", std::fs::canonicalize::<&str>)?;
    t.set_closure("isabs", Path::is_absolute)?;
    t.set_closure("isdir", Path::is_dir)?;
    t.set_closure("isfile", Path::is_file)?;
    t.set_closure("issymlink", Path::is_symlink)?;

    return Ok(t);
}, false)?;

And then use them in lua

local json = require 'json'
local path = require 'path'

local dir = path.abspath('.')
assert(json.load(json.dump(dir)) == dir)

Multiple thread usage

To use multiple thread feature in lua, you need to specify the thread feature in Cargo.toml, and patch the lua-src crate with ezlua's custom

ezlua = { version = '0.3', features = ['thread'] }

lua-src = { git = "https://github.com/metaworm/lua-src-rs" }

And then, register the thread module for lua

lua.register_module("thread", ezlua::binding::std::thread::init, true)?;

And then, use it in lua

local thread = require 'thread'
local threads = {}
local tt = { n = 0 }
local count = 64
for i = 1, count do
    threads[i] = thread.spawn(function()
        tt.n = tt.n + 1
        -- print(tt.n)

for i, t in ipairs(threads) do
    print('#' .. i .. ' finished')
assert(tt.n == count)

In addition, you can also start a new thread with the same lua VM

let co = Coroutine::empty(&lua);
std::thread::spawn(move || {
    let print = co.global().get("print")?;
    print.pcall_void("running lua in another thread")?;


Module mode

In a module mode ezlua allows to create a compiled Lua module that can be loaded from Lua code using require.

First, disable the default vendored feature, and keep std feature only, and config your crate as a cdylib in Cargo.toml :

ezlua = {version = '0.3', default-features = false, features = ['std']}

crate-type = ['cdylib']

Then, export your luaopen_ function by using ezlua::lua_module! macro, where the first argument is luaopen_<Your module name>

use ezlua::prelude::*;

ezlua::lua_module!(luaopen_ezluamod, |lua| {
    let module = lua.new_table()?;

    module.set("_VERSION", "0.1.0")?;
    // ... else module functions

    return Ok(module);

Internal design



~130K SLoC