我们的目标是,能够将 Rust 对象高效地映射到 Lua 中使用,并尽可能简化这一过程。
功能目标
以 struct HcTestMacro 这个结构体为例,我们希望实现以下功能:
- 类型构建:在 Lua 中调用
local val = HcTestMacro.new() 即可创建对象。
- 类型析构:在 Lua 中调用
HcTestMacro.del(val) 可销毁对象。此功能仅限于标记为 light userdata 的类型。
- 字段映射:假设结构体中有一个字段
hc,我们需要能快速地对这个字段进行读取和写入操作。
- 取值:
val.hc 或者 val:get_hc() 均可获取字段值。
- 赋值:
val.hc = "hclua" 或者 val:set_hc("hclua") 均可设置字段值。
- 类型方法:可以灵活地注册实例方法。由于 Lua 虚拟机可能不是全局唯一的,所以不能完全通过宏来直接注册,需要手动或半自动地进行。注册方式支持直接函数或闭包:
// 直接注册已有的成员函数
HcTestMacro::object_def(&mut lua, "ok", hclua::function1(HcTestMacro::ok));
// 闭包注册单参数方法
HcTestMacro::object_def(&mut lua, "call1", hclua::function1(|obj: &HcTestMacro| -> u32 {
obj.field
}));
// 闭包注册双参数方法
HcTestMacro::object_def(&mut lua, "call2", hclua::function2(|obj: &mut HcTestMacro, val: u32| -> u32 {
obj.field + val
}));
- 静态方法:注册不需要实例化的类方法,相当于模块函数。
HcTestMacro::object_static_def(&mut lua, "sta_run", hclua::function0(|| -> String {
"test".to_string()
}));
完整示例代码
use hclua_macro::ObjectMacro;
#[derive(ObjectMacro, Default)]
#[hclua_cfg(name = HcTest)]
#[hclua_cfg(light)]
struct HcTestMacro {
#[hclua_field]
field: u32,
#[hclua_field]
hc: String,
}
impl HcTestMacro {
fn ok(&self) {
println!("ok!!!!");
}
}
fn main() {
let mut lua = hclua::Lua::new();
let mut test = HcTestMacro::default();
HcTestMacro::register(&mut lua);
// 直接注册函数注册
HcTestMacro::object_def(&mut lua, "ok", hclua::function1(HcTestMacro::ok));
// 闭包注册单参数
HcTestMacro::object_def(&mut lua, "call1", hclua::function1(|obj: &HcTestMacro| -> u32 {
obj.field
}));
// 闭包注册双参数
HcTestMacro::object_def(&mut lua, "call2", hclua::function2(|obj: &mut HcTestMacro, val: u32| -> u32 {
obj.field + val
}));
HcTestMacro::object_static_def(&mut lua, "sta_run", hclua::function0(|| -> String {
"test".to_string()
}));
lua.openlibs();
let val = "
print(aaa);
print(\"cccxxxxxxxxxxxxxxx\");
print(type(HcTest));
local v = HcTest.new();
print(\"call ok\", v:ok())
print(\"call1\", v:call1())
print(\"call2\", v:call2(2))
print(\"kkkk\", v.hc)
v.hc = \"dddsss\";
print(\"kkkk ok get_hc\", v:get_hc())
v.hc = \"aa\";
print(\"new kkkkk\", v.hc)
v:set_hc(\"dddddd\");
print(\"new kkkkk1\", v.hc)
print(\"attemp\", v.hc1)
print(\"vvvvv\", v:call1())
print(\"static run\", HcTest.sta_run())
HcTest.del(v);
";
let _: Option<()> = lua.exec_string(val);
}
源码地址
实现上述功能的库名为 hclua ,它是一个用于在 Rust 中进行 Lua 绑定的工具。
功能实现剖析
整个绑定过程主要依赖于 derive 宏来自动生成代码:
- 宏声明:通过
#[derive(ObjectMacro, Default)] 启用我们的自动绑定宏。
- 配置属性:
#[hclua_cfg(name = HcTest)]:声明这个类型在 Lua 中的名字为 HcTest。本质上是在 Lua 全局环境中注册了一个名为 HcTest 的 table,其中包含了 new, del 等函数。
#[hclua_cfg(light)]:表示该类型是 light userdata,其生命周期由 Rust 控制。默认为 userdata,生命周期由 Lua 控制,通过 __gc 元方法进行垃圾回收。
- 字段属性:
#[hclua_field] 放在结构体字段前,标记该字段需要被映射到 Lua。derive 宏在生成代码时会检查哪些字段带有此属性,并为它们生成对应的 getter 和 setter。
Derive宏实现
核心实现在 hclua-macro 库中,完整代码可供参考。其主要步骤为:
- 声明并解析 ItemStruct
#[proc_macro_derive(ObjectMacro, attributes(hclua_field, hclua_cfg))]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
let ItemStruct {
ident,
fields,
attrs,
..
} = parse_macro_input!(input);
- 解析配置
let config = config::Config::parse_from_attributes(ident.to_string(), &attrs[..]).unwrap();
-
解析字段并生成相应的函数
let functions: Vec<_> = fields
.iter()
.map(|field| {
let field_ident = field.ident.clone().unwrap();
if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) {
let get_name = format_ident!("get_{}", field_ident);
let set_name = format_ident!("set_{}", field_ident);
let ty = field.ty.clone();
quote! {
fn #get_name(&mut self) -> ty {
&self.#field_ident
}
fn #set_name(&mut self, val: #ty) {
self.#field_ident = val;
}
}
} else {
quote! {}
}
})
.collect();
let registers: Vec<_> = fields.iter().map(|field| {
let field_ident = field.ident.clone().unwrap();
if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) {
let ty = field.ty.clone();
let get_name = format_ident!("get_{}", field_ident);
let set_name = format_ident!("set_{}", field_ident);
quote!{
hclua::LuaObject::add_object_method_get(lua, &stringify!(#field_ident), hclua::function1(|obj: &mut #ident| -> ty {
&obj.#field_ident
}));
// ...
}
} else {
quote!{}
}
}).collect();
通过生成 TokenStream 数组,并在最终展开时使用 #(#functions)* 这样的语法,可以将所有生成的代码片段拼接起来。
-
生成最终的代码
let name = config.name;
let is_light = config.light;
let gen = quote! {
impl #ident {
fn register_field(lua: &mut hclua::Lua) {
#(#registers)*
}
fn register(lua: &mut hclua::Lua) {
let mut obj = if #is_light {
hclua::LuaObject::<#ident>::new_light(lua.state(), name)
} else {
hclua::LuaObject::<#ident>::new(lua.state(), name)
};
obj.create();
Self::register_field(lua);
}
fn object_def<P>(lua: &mut hclua::Lua, name: &str, param: P)
where
P: hclua::LuaPush,
{
hclua::LuaObject::<#ident>::object_def(lua, name, param);
}
#(#functions)*
}
// ...
};
gen.into()
这样,我们就通过过程宏实现了一个快速、自动化的 Rust 到 Lua 的对象绑定方案。
Field映射的实现原理
在 Rust 与 Lua 的对象绑定中,type(val) 在 Lua 中是一个 userdata 对象。对这个对象的所有属性访问都会触发其元表(metatable)的操作。
Field的获取
当我们访问 val.hc 时,Lua 会按以下步骤执行:
- 查找
val 本身是否有 hc 这个值,存在则直接返回。
- 查找
val 对象对应的元表(lua_getmetatable)。
- 在元表中找到
__index 这个键对应的值。若不存在则返回 nil。
- 调用
__index 函数。此时函数的第一个参数是 val,第二个参数是 "hc"(字段名)。
- 此时有两种情况:如果访问的是一个函数,则跳转到步骤6;如果访问的是一个字段变量,则跳转到步骤7。
- 直接从元表中取出
meta["hc"] 返回给 Lua。如果这个值是普通值就返回值,是函数则返回一个可调用的函数对象,流程结束。
- 因为字段变量的值是动态的,并不直接存储在元表中,所以需要额外调用一个 getter 函数。我们手动调用这个 getter:
lua_call(lua, 1, 1);(一个参数,一个返回值),即可实现字段值的返回。
注:为了高效地区分对函数和字段的访问,这里使用了全局静态变量来存储某个类型(TypeId)下的所有字段名。
lazy_static! {
static ref FIELD_CHECK: RwLock<HashSet<(TypeId, &'static str)>> = RwLock::new(HashSet::new());
}
__index 元方法的核心实现如下:
extern "C" fn index_metatable(lua: *mut sys::lua_State) -> libc::c_int {
unsafe {
if lua_gettop(lua) < 2 {
let value = CString::new(format!("index field must use 2 top")).unwrap();
return luaL_error(lua, value.as_ptr());
}
}
if let Some(key) = String::lua_read_with_pop(lua, 2, 0) {
let typeid = Self::get_metatable_real_key();
unsafe {
sys::lua_getglobal(lua, typeid.as_ptr());
let is_field = LuaObject::is_field(&*key);
let key = CString::new(key).unwrap();
let t = lua_getfield(lua, -1, key.as_ptr());
if !is_field {
if t == sys::LUA_TFUNCTION {
return 1;
} else {
return 1;
}
}
lua_pushvalue(lua, 1);
lua_call(lua, 1, 1);
1
}
} else {
0
}
}
至此,字段的获取逻辑就完成了。
Field的设置
当我们在 Lua 中执行 val.hc = "hclua" 时,流程如下:
- 查找
val 中是否有 hc 这个值,有则直接设置。
- 查找
val 对象对应的元表。
- 在元表中找到
__newindex 这个键对应的值。若不存在则报错。
- 调用
__newindex 函数。此时函数的第一个参数是 val,第二个参数是 "hc",第三个参数是字符串 "hclua"。
- 判断第二个参数(字段名)是否是一个已注册的字段,如果不是则返回 Lua 错误。
- 在字段名后追加
__set 后缀(即 hc__set),然后查找元表中是否存在 meta["hc__set"]。若为空则失败,若为函数则跳转到步骤7。
- 调用这个 setter 函数,将
val(第一个参数)和 "hclua"(第三个参数)作为参数传入并执行。
lua_pushvalue(lua, 1);
lua_pushvalue(lua, 3);
lua_call(lua, 2, 1);
至此,字段的设置逻辑也完成了。
小结
Lua 脚本的执行速度相对较慢,为了追求高性能,我们常常会将许多关键函数放在 Rust 层或更底层实现。此时,一个快速、自动化的对象映射机制就变得非常重要,它可以极大地方便代码的复用与集成。通过本文介绍的 derive 宏方案,我们可以快速构建出符合需求的功能,简化 Rust 与 Lua 交互的复杂度。如果你对这类跨语言编程或元编程技巧感兴趣,欢迎到云栈社区探讨更多细节。