SkillAgentSearch skills...

Ezlua

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

Install / Use

/learn @metaworm/Ezlua
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

crates.io docs.rs Build Status Coverage Status

ChangeLog | FAQ | Known issues

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

Features

  • 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

Limits

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

Examples

See builtin bindings tests

Usage

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
  • chrono: enable the builtin bindings for [tokio] crate

Basic

First, add ezlua to your dependencies in Cargo.toml

[dependencies]
ezlua = { version = '0.5' }

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

    Ok(())
}

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)?;
        conf.to_lua(lua)
    }
}

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>> {
        SerdeValue(self).to_lua(lua)
    }
}

// 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()))?;
lua.global()
    .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())?;

        Ok(())
    }

    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)?;

        Ok(())
    }
}

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)?;

        Ok(())
    }

    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| {
            this.try_wait().ok().flatten().ok_or(())
        })?;
    }
}

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)?;
        Ok(())
    }
}

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)?;
        Ok(())
    }

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

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 f

Related Skills

View on GitHub
GitHub Stars38
CategoryDevelopment
Updated5d ago
Forks3

Languages

Rust

Security Score

75/100

Audited on Mar 26, 2026

No findings