找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2898

积分

0

好友

390

主题
发表于 3 天前 | 查看: 17| 回复: 0

我们的目标是,能够将 Rust 对象高效地映射到 Lua 中使用,并尽可能简化这一过程。

功能目标

struct HcTestMacro 这个结构体为例,我们希望实现以下功能:

  1. 类型构建:在 Lua 中调用 local val = HcTestMacro.new() 即可创建对象。
  2. 类型析构:在 Lua 中调用 HcTestMacro.del(val) 可销毁对象。此功能仅限于标记为 light userdata 的类型。
  3. 字段映射:假设结构体中有一个字段 hc,我们需要能快速地对这个字段进行读取和写入操作。
    • 取值val.hc 或者 val:get_hc() 均可获取字段值。
    • 赋值val.hc = "hclua" 或者 val:set_hc("hclua") 均可设置字段值。
  4. 类型方法:可以灵活地注册实例方法。由于 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
}));
  1. 静态方法:注册不需要实例化的类方法,相当于模块函数。
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 宏来自动生成代码:

  1. 宏声明:通过 #[derive(ObjectMacro, Default)] 启用我们的自动绑定宏。
  2. 配置属性
    • #[hclua_cfg(name = HcTest)]:声明这个类型在 Lua 中的名字为 HcTest。本质上是在 Lua 全局环境中注册了一个名为 HcTest 的 table,其中包含了 new, del 等函数。
    • #[hclua_cfg(light)]:表示该类型是 light userdata,其生命周期由 Rust 控制。默认为 userdata,生命周期由 Lua 控制,通过 __gc 元方法进行垃圾回收。
  3. 字段属性#[hclua_field] 放在结构体字段前,标记该字段需要被映射到 Lua。derive 宏在生成代码时会检查哪些字段带有此属性,并为它们生成对应的 getter 和 setter。

Derive宏实现

核心实现在 hclua-macro 库中,完整代码可供参考。其主要步骤为:

  1. 声明并解析 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);
  2. 解析配置
    let config = config::Config::parse_from_attributes(ident.to_string(), &attrs[..]).unwrap();
  3. 解析字段并生成相应的函数

    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)* 这样的语法,可以将所有生成的代码片段拼接起来。

  4. 生成最终的代码

    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 会按以下步骤执行:

  1. 查找 val 本身是否有 hc 这个值,存在则直接返回。
  2. 查找 val 对象对应的元表(lua_getmetatable)。
  3. 在元表中找到 __index 这个键对应的值。若不存在则返回 nil
  4. 调用 __index 函数。此时函数的第一个参数是 val,第二个参数是 "hc"(字段名)。
  5. 此时有两种情况:如果访问的是一个函数,则跳转到步骤6;如果访问的是一个字段变量,则跳转到步骤7。
  6. 直接从元表中取出 meta["hc"] 返回给 Lua。如果这个值是普通值就返回值,是函数则返回一个可调用的函数对象,流程结束。
  7. 因为字段变量的值是动态的,并不直接存储在元表中,所以需要额外调用一个 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" 时,流程如下:

  1. 查找 val 中是否有 hc 这个值,有则直接设置。
  2. 查找 val 对象对应的元表。
  3. 在元表中找到 __newindex 这个键对应的值。若不存在则报错。
  4. 调用 __newindex 函数。此时函数的第一个参数是 val,第二个参数是 "hc",第三个参数是字符串 "hclua"
  5. 判断第二个参数(字段名)是否是一个已注册的字段,如果不是则返回 Lua 错误。
  6. 在字段名后追加 __set 后缀(即 hc__set),然后查找元表中是否存在 meta["hc__set"]。若为空则失败,若为函数则跳转到步骤7。
  7. 调用这个 setter 函数,将 val(第一个参数)和 "hclua"(第三个参数)作为参数传入并执行。
lua_pushvalue(lua, 1);
lua_pushvalue(lua, 3);
lua_call(lua, 2, 1);

至此,字段的设置逻辑也完成了。

小结

Lua 脚本的执行速度相对较慢,为了追求高性能,我们常常会将许多关键函数放在 Rust 层或更底层实现。此时,一个快速、自动化的对象映射机制就变得非常重要,它可以极大地方便代码的复用与集成。通过本文介绍的 derive 宏方案,我们可以快速构建出符合需求的功能,简化 Rust 与 Lua 交互的复杂度。如果你对这类跨语言编程或元编程技巧感兴趣,欢迎到云栈社区探讨更多细节。




上一篇:星华辰U40R2真4K 120Hz带鱼屏使用体验分享:Mac外接与色彩校准心得
下一篇:Rust宏实战:使用Derive为Lua绑定自动生成Getter/Setter
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 19:46 , Processed in 1.125193 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表