#lua-bindings #lua #scripting #async #api-bindings

nightly no-std ezlua

针对Lua5.4的舒适、高效且零成本的Rust绑定

13个版本

0.5.3 2024年3月5日
0.5.0 2024年1月29日
0.4.4 2023年11月19日
0.3.2 2023年6月6日

#389无标准库

Download history 1/week @ 2024-05-19 5/week @ 2024-06-02 8/week @ 2024-06-30 78/week @ 2024-07-07

86 每月下载量
用于 udbg

MIT 许可证

360KB
9K SLoC

crates.io docs.rs Build Status Coverage Status

变更日志 | 常见问题解答 | 已知问题

针对Lua5.4的舒适、高效且零成本的Rust绑定

功能

  • 支持序列化(serde)
  • 支持异步函数绑定
  • 对函数和 userdata 方法进行舒适绑定
  • 对栈值进行舒适操作,无需关注栈细节
  • 高效:无需辅助栈,支持引用类型转换
  • 内建绑定到大多数常用的 Rust std 函数和类型
  • 支持多线程
  • 支持nostd

限制

  • 需要Nightly Rust编译器(1.76+)
  • 目前仅支持Lua5.4

示例

查看 内建绑定 测试

使用方法

功能标志

  • async:启用异步/await支持(可以使用任何执行器,例如[tokio]或[async-std])
  • serde:使用[serde]框架为ezlua类型添加序列化和反序列化支持
  • vendored:在ezlua编译期间从源代码构建静态Lua库使用[lua-src] crate
  • thread启用多线程支持
  • std:启用Rust std函数和类型的内建绑定
  • json:启用[serde_json] crate的内建绑定
  • regex:启用[regex] crate的内建绑定
  • tokio:启用[tokio] crate的内建绑定
  • chrono:启用[chrono] crate的内建绑定

基础

首先,在Cargo.toml中将ezlua添加到依赖项

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

然后,在Rust中使用ezlua,代码框架如下

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(())
}

绑定您的函数

当然,您可以通过ezlua绑定将Rust函数提供给Lua,非常简单,如下所示

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

并且您可以轻松地绑定现有函数

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

绑定您的类型

为您的类型实现 ToLua 特性,然后您可以将其传递给 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)?;

通过 serde 简单绑定

继续上面的示例,您可以通过 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
    })?;

绑定自定义对象(userdata)

ezlua 的 userdata 绑定机制非常强大,以下代码来自 std 绑定

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(())
    }
}

实现了 UserData 特性的类型,ezlua 也为它实现了 ToLua,并为它的引用实现了 FromLua

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

默认情况下,绑定为 userdata 的类型是不可变的,如果您需要 可变引用,您可以指定一个 UserData::Trans 类型,其中有一个内建的实现是 RefCell,所以可变绑定的实现如下所示

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

在实现 UserData 特性时,通常只需要实现 getter / setter / methods 方法,这允许您通过 userdata 值“读取属性”/“写入属性”/“调用方法”,但 ezlua 还为 UserData 提供了 更强大的功能,例如“uservalue 访问”和“userdata 缓存”。

为了启用 userdata 类型的“uservalue 访问”功能,只需指定 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)?;

为了启用 userdata 类型的“userdata 缓存”功能,您应该实现 UserData::key_to_cache 方法,该方法返回一个指针,作为 Lua 缓存表中的 lightuserdata 键。

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

注册您自己的模块

要注册一个 Lua 模块,您可以通过 LuaState::register_module 方法提供一个返回 Lua 表的 Rust 函数

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

然后在 Lua 中使用它们

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

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

多线程使用

要在 Lua 中使用多线程功能,您需要在 Cargo.toml 中指定 thread 功能,并使用 ezlua 的自定义 补丁 lua-src crate

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

[patch.crates-io]
lua-src = { git = "https://github.com/metaworm/lua-src-rs" }

然后,为 Lua 注册线程模块

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

然后,在 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)

此外,您还可以使用相同的 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();

模块模式

在模块模式下,ezlua 允许创建一个可编译的 Lua 模块,该模块可以使用 require 通过 Lua 代码加载。

首先,禁用默认的 vendored 功能,只保留 std 功能,并在 Cargo.toml 中将您的 crate 配置为 cdylib

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

[lib]
crate-type = ['cdylib']

然后,使用 ezlua::lua_module! 宏导出您的 luaopen_ 函数,其中第一个参数是 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);
});

内部设计

待办事项

依赖项

~0.5–11MB
~106K SLoC