在Lua中實現Rust物件的繫結

问蒙服务框架發表於2024-10-21

實現目標:能將Rust物件快速的對映到lua中使用,儘可能的簡化使用。

功能目標

struct HcTestMacro為例:

  1. 型別構建,在lua呼叫local val = HcTestMacro.new()可構建
  2. 型別析構,在lua呼叫HcTestMacro.del(val)可析建,僅限light use**rdata
  3. 欄位的對映,假設有欄位hc,我們需要能快速的進行欄位的取值賦值
  • 取值val.hc或者val:get_hc()均可進行取值
  • 賦值val.hc = "hclua"或者val:set_hc("hclua")均可進行取值
  1. 型別方法,註冊類方法,比如額外的方法call1,那我們就可以透過註冊到lua虛擬機器,由於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宏進行函式註冊:#[derive(ObjectMacro, Default)]
透過attrib宣告命名:#[hclua_cfg(name = HcTest)],配置該類在lua
中的名字為HcTest,本質上在lua裡註冊全域性的table,透過在該table下注冊
HcTest { new = function(), del = function() }
透過attrib註冊生命:#[hclua_cfg(light)],表示該型別是light userdata即生命週期由Rust控制,預設為userdata即生命週期由Lua控制,透過__gc進行回收。
透過attrib宣告欄位:#[hclua_field]放到欄位前面,即可以註冊欄位使用,在derive生成的時候判斷是否有該欄位,進行欄位的對映。

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);
  1. 解析Config,即判斷類名及是否light
let config = config::Config::parse_from_attributes(ident.to_string(), &attrs[..]).unwrap();
  1. 解析欄位並生成相應的函式
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)*即可以得到我們的TokenStream拼接的效果。

  1. 生成最終的程式碼
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()

這樣子我們透過宏就實現了我們快速的實現方案。

Field對映的實現

Lua物件對映中,type(val)為一個object變數,在這基礎上進行訪問的都將會觸發元表的操作metatable

Field的獲取

我們訪問任何物件如val.hc

  1. 查詢val中是否有hc的值,若存在直接返回
  2. 查詢object中對應的元表lua_getmetatable若為meta
  3. 找到__index的key值,若不存在則返回空值
  4. 呼叫__index函式,此時呼叫該數第一個引數為val,第二個引數為hc
  5. 此時有兩種可能,一種是訪問函式跳轉6,一種是訪問變數跳轉7,
  6. 將直接取出meta["hc"]返回給lua,如果是值即為值,為函式則返回給lua的後續呼叫,通常的形式表達為val:hc()val.hc(val)實現呼叫,結束流程
  7. 因為變數是一個動態值,我們並未存在metatable中,所以需要額外的呼叫取出正確值,我們將取出的函式手動繼續在呼叫lua_call(lua, 1, 1);即可以實現欄位的返回

注:在變數中該值是否為欄位處理過程會有相對的差別,又需要高效的進行驗證,這裡用的是全域性的靜態變數來儲存是否為該型別的欄位值。

lazy_static! {
    static ref FIELD_CHECK: RwLock<HashSet<(TypeId, &'static str)>> = RwLock::new(HashSet::new());
}

完整原始碼:

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的設定

此時我們需要設定物件val.hc = "hclua"

  1. 查詢val中是否有hc的值,若有直接設定該值
  2. 查詢object中對應的元表lua_getmetatable若為meta
  3. 找到__newindex的key值,若不存在則返回空值
  4. 呼叫__newindex函式,此時呼叫該數第一個引數為val,第二個引數為hc,第三個引數為字串"hclua"
  5. 若此時判斷第二個引數不是欄位,則直接返回lua錯誤內容
  6. 此時我們會在第二個引數的key值後面新增__set即為hc__set,我們查詢meta["hc__set"] 若為空則返回失敗,若為函式則轉到7
  7. 我們將呼叫該函式,並將第一個引數val,第三個引數hclua,並進行函式呼叫
lua_pushvalue(lua, 1);
lua_pushvalue(lua, 3);
lua_call(lua, 2, 1);

此時欄位的設定已經完成了。

小結

Lua的處理速度較慢,為了高效能,通常有許多函式會放到Rust層或者底層進行處理,此時有一個快速的對映就可以方便程式碼的快速使用複用,而透過derive宏,我們可以快速的構建出想要的功能。

相關文章