13 releases
0.5.3 | Mar 5, 2024 |
---|---|
0.5.0 | Jan 29, 2024 |
0.4.4 | Nov 19, 2023 |
0.3.2 | Jun 6, 2023 |
#79 in No standard library
475 downloads per month
Used in udbg
360KB
9K
SLoC
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
Usage
Feature flags
async
: enable async/await support (any executor can be used, eg. [tokio] or [async-std])serde
: add serialization and deserialization support toezlua
types using [serde] frameworkvendored
: build static Lua library from sources duringezlua
compilation using [lua-src] cratesthread
enable 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
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 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
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)
end)
end
for i, t in ipairs(threads) do
t:join()
print('#' .. i .. ' finished')
end
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")?;
LuaResult::Ok(())
})
.join()
.unwrap();
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
:
[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 functions
return Ok(module);
});
Internal design
TODO
Dependencies
~0.5–10MB
~110K SLoC