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.76+)
- Only support lua5.4 currently
async: enable async/await support (any executor can be used, eg. [tokio] or [async-std])serde: add serialization and deserialization support toezluatypes using [serde] frameworkvendored: build static Lua library from sources duringezluacompilation using [lua-src] cratesthreadenable the multiple thread supportstd: enable the builtin bindings for rust std functions and typesjson: enable the builtin bindings for [serde_json] crateregex: enable the builtin bindings for [regex] cratetokio: enable the builtin bindings for [tokio] cratechrono: enable the builtin bindings for [tokio] crate
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::*;fnmain() -> LuaResult<()>{// create a lua VMlet 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 tablelet add = lua.global().get("add")?;// call add function and get its resultlet result = add.pcall::<_,u32>((111,222))?;assert_eq!(result,333);// ... for the following codeOk(())}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)?;Implement ToLua trait for your type, and then you can pass it to lua
#[derive(Debug,Default)]structConfig{name:String,path:String,timeout:u64,// ...}implToLuaforConfig{fnto_lua<'a>(self,lua:&'aLuaState) -> 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)?;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)]structConfig{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);implToLuaforConfig{fnto_lua<'a>(self,lua:&'aLuaState) -> 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);implFromLua<'_>forConfig{fnfrom_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})?;ezlua's userdata binding mechanism is powerful, the following code comes from std bindings
use std::{fs::Metadata, path::*};implUserDataforMetadata{fngetter(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(())}fnmethods(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};implUserDataforChild{typeTrans = RefCell<Self>;fngetter(fields:UserdataRegistry<Self>) -> LuaResult<()>{
fields.add("id",Self::id)?;Ok(())}fnmethods(mt:UserdataRegistry<Self>) -> Result<()>{
mt.add_mut("kill",Self::kill)?;
mt.add_mut("wait",Self::wait)?;
mt.add_mut("try_wait", |this:&mutSelf| {
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
structTest{a:i32,}implUserDataforTest{typeTrans = RefCell<Self>;constINDEX_USERVALUE:bool = true;fnmethods(mt:UserdataRegistry<Self>) -> LuaResult<()>{
mt.set_closure("inc", |mutthis: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)]structRcTest(Rc<Test>);implUserDataforRcTest{fnkey_to_cache(&self) -> *const(){self.as_ref()as*const_as_}fngetter(fields:UserdataRegistry<Self>) -> LuaResult<()>{
fields.set_closure("a", |this:&Self| this.a)?;Ok(())}fnmethods(_: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)?;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)?;returnOk(t);},false)?;And then use them in lua
localjson=require'json'localpath=require'path'localdir=path.abspath('.')
assert(json.load(json.dump(dir)) ==dir)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
[dependencies]
ezlua = { version = '0.3', features = ['thread'] }
[patch.crates-io]
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
localthread=require'thread'localthreads= {}
localtt= { n=0 }
localcount=64fori=1, countdothreads[i] =thread.spawn(function()
tt.n=tt.n+1-- print(tt.n)end)
endfori, tinipairs(threads) dot:join()
print('#' ..i..' finished')
endassert(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")?;LuaResult::Ok(())}).join().unwrap();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 :
[dependencies]
ezlua = {version = '0.3', default-features = false, features = ['std']}
[lib]
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 functionsreturnOk(module);});TODO