深入xLua實現原理之Lua如何呼叫C#

iwiniwin發表於2021-09-18

xLua是騰訊的一個開源專案,為Unity、 .Net、 Mono等C#環境增加Lua指令碼程式設計的能力。本文主要是探討xLua下Lua呼叫C#的實現原理。

Lua與C#資料通訊機制

無論是Lua呼叫C#,還是C#呼叫Lua,都需要一個通訊機制,來完成資料的傳遞。而Lua本身就是由C語言編寫的,所以它出生自帶一個和C/C++的通訊機制。

Lua和C/C++的資料互動通過棧進行,運算元據時,首先將資料拷貝到"棧"上,然後獲取資料,棧中的每個資料通過索引值進行定位,索引值為正時表示相對於棧底的偏移索引,索引值為負時表示相對於棧頂的偏移索引,索引值以1或-1為起始值,因此棧頂索引值永遠為-1, 棧底索引值永遠為1 。 “棧"相當於資料在Lua和C/C++之間的中轉地。每種資料都有相應的存取介面。

而C#可以通過P/Invoke方式呼叫Lua的dll,通過這個dll執行Lua的C API。換言之C#可以藉助C/C++來與Lua進行資料通訊。在xLua的LuaDLL.cs檔案中可以找到許多DllImport修飾的資料入棧與獲取的介面。

// LuaDLL.cs
[DllImport(LUADLL,CallingConvention=CallingConvention.Cdecl)]
public static extern void lua_pushnumber(IntPtr L, double number);

[DllImport(LUADLL,CallingConvention=CallingConvention.Cdecl)]
public static extern void lua_pushboolean(IntPtr L, bool value);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void xlua_pushinteger(IntPtr L, int value);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern double lua_tonumber(IntPtr L, int index);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int xlua_tointeger(IntPtr L, int index);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern uint xlua_touint(IntPtr L, int index);

[DllImport(LUADLL,CallingConvention=CallingConvention.Cdecl)]
public static extern bool lua_toboolean(IntPtr L, int index);

傳遞C#物件到Lua

對於bool,int這樣簡單的值型別可以直接通過C API傳遞。但對於C#物件就不同了,Lua這邊沒有能與之對應的型別,因此傳遞到Lua的只是C#物件的一個索引,具體實現請看下面的程式碼

// ObjectTranslator.cs
public void Push(RealStatePtr L, object o)
{
    // ...
    int index = -1;
    Type type = o.GetType();
#if !UNITY_WSA || UNITY_EDITOR
    bool is_enum = type.IsEnum;
    bool is_valuetype = type.IsValueType;
#else
    bool is_enum = type.GetTypeInfo().IsEnum;
    bool is_valuetype = type.GetTypeInfo().IsValueType;
#endif
    bool needcache = !is_valuetype || is_enum;  // 如果是引用或列舉,會進行快取
    if (needcache && (is_enum ? enumMap.TryGetValue(o, out index) : reverseMap.TryGetValue(o, out index)))  // 如果有快取
    {
        if (LuaAPI.xlua_tryget_cachedud(L, index, cacheRef) == 1)  
        {
            return;
        }
        //這裡實在太經典了,weaktable先刪除,然後GC會延遲呼叫,當index會迴圈利用的時候,不註釋這行將會導致重複釋放
        //collectObject(index);
    }

    bool is_first;
    int type_id = getTypeId(L, type, out is_first);

    //如果一個type的定義含本身靜態readonly例項時,getTypeId會push一個例項,這時候應該用這個例項
    if (is_first && needcache && (is_enum ? enumMap.TryGetValue(o, out index) : reverseMap.TryGetValue(o, out index))) 
    {
        if (LuaAPI.xlua_tryget_cachedud(L, index, cacheRef) == 1)   
        {
            return;
        }
    }
    // C#側進行快取
    index = addObject(o, is_valuetype, is_enum);
    // 將代表物件的索引push到lua
    LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
}

程式碼中的兩個if語句主要是對快取的判斷,如果要傳遞的物件已經被快取過了就直接使用快取的。如果這個物件是被第一次傳遞,則進行以下兩步操作

  1. 通過addObject將物件快取在objects物件池中,並得到一個索引(通過這個索引可以獲取到該物件)

    // ObjectTranslator.cs
    int addObject(object obj, bool is_valuetype, bool is_enum)
    {
        int index = objects.Add(obj);
        if (is_enum)
        {
            enumMap[obj] = index;
        }
        else if (!is_valuetype)
        {
            reverseMap[obj] = index;
        }
        
        return index;
    }
    
  2. 通過xlua_pushcsobj將代表物件的索引傳遞到Lua。

    引數key表示代表物件的索引,引數meta_ref表示代表物件型別的表的索引,它的值是通過getTypeId函式獲得的,後面會詳細講到。引數need_cache表示是否需要在Lua側進行快取,引數cache_ref表示Lua側快取表的索引

    // xlua.c
    LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
        int* pointer = (int*)lua_newuserdata(L, sizeof(int));
        *pointer = key;
        
        if (need_cache) cacheud(L, key, cache_ref);  // Lua側快取
    
        lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
    
        lua_setmetatable(L, -2);  // 為userdata設定元表
    }
    
    // 將 key = userdata 存入快取表
    static void cacheud(lua_State *L, int key, int cache_ref) {
        lua_rawgeti(L, LUA_REGISTRYINDEX, cache_ref);
        lua_pushvalue(L, -2);
        lua_rawseti(L, -2, key);
        lua_pop(L, 1);
    }
    

    xlua_pushcsobj的主要邏輯是,代表物件的索引被push到Lua後,Lua會為其建立一個userdata,並將這個userdata指向物件索引,如果需要快取則將userdata儲存到快取表中, 最後為userdata設定了元表。也就是說,C#物件在Lua這邊對應的就是一個userdata,利用物件索引保持與C#物件的聯絡。

註冊C#型別資訊到Lua

為userdata(特指C#物件在Lua這邊對應的代理userdata,後面再出現的userdata也是同樣的含義,就不再贅述了)設定的元表,表示的實際是物件的型別資訊。在將C#物件傳遞到Lua以後,還需要告知Lua該物件的型別資訊,比如物件型別有哪些成員方法,屬性或是靜態方法等。將這些都註冊到Lua後,Lua才能正確的呼叫。這個元表是通過getTypeId函式生成的

// ObjectTranslator.cs
internal int getTypeId(RealStatePtr L, Type type, out bool is_first, LOGLEVEL log_level = LOGLEVEL.WARN)
{
    int type_id;
    is_first = false;
    if (!typeIdMap.TryGetValue(type, out type_id)) // no reference
    {
        // ...
        is_first = true;
        Type alias_type = null;
        aliasCfg.TryGetValue(type, out alias_type);
        LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);

        if (LuaAPI.lua_isnil(L, -1)) //no meta yet, try to use reflection meta
        {
            LuaAPI.lua_pop(L, 1);

            if (TryDelayWrapLoader(L, alias_type == null ? type : alias_type))
            {
                LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
            }
            else
            {
                throw new Exception("Fatal: can not load metatable of type:" + type);
            }
        }

        //迴圈依賴,自身依賴自己的class,比如有個自身型別的靜態readonly物件。
        if (typeIdMap.TryGetValue(type, out type_id))
        {
            LuaAPI.lua_pop(L, 1);
        }
        else
        {
            // ...
            LuaAPI.lua_pushvalue(L, -1);
            type_id = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);  // 將元表新增到登錄檔中
            LuaAPI.lua_pushnumber(L, type_id);
            LuaAPI.xlua_rawseti(L, -2, 1);   // 元表[1] = type_id
            LuaAPI.lua_pop(L, 1);

            if (type.IsValueType())
            {
                typeMap.Add(type_id, type);
            }

            typeIdMap.Add(type, type_id);
        }
    }
    return type_id;
}

函式主要邏輯是以類的名稱為key通過luaL_getmetatable獲取類對應的元表,如果獲取不到,則通過TryDelayWrapLoader函式生成。然後呼叫luaL_ref將獲取到的元表新增到Lua登錄檔中,並返回type_id。type_id表示的就是元表在Lua登錄檔中的索引,通過這個索引可以在Lua登錄檔中取回元表。前面提到的xlua_pushcsobj函式就是利用type_id即meta_ref,獲取到元表,然後為userdata設定的元表。

下面來看元表具體是怎樣生成的

// ObjectTranslator.cs
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
{
    // ...
    LuaAPI.luaL_newmetatable(L, type.FullName); //先建一個metatable,因為載入過程可能會需要用到
    LuaAPI.lua_pop(L, 1);

    Action<RealStatePtr> loader;
    int top = LuaAPI.lua_gettop(L);
    if (delayWrap.TryGetValue(type, out loader))  // 如果有預先註冊的型別元表生成器,則直接使用
    {
        delayWrap.Remove(type);
        loader(L);
    }
    else
    {
#if !GEN_CODE_MINIMIZE && !ENABLE_IL2CPP && (UNITY_EDITOR || XLUA_GENERAL) && !FORCE_REFLECTION && !NET_STANDARD_2_0
        if (!DelegateBridge.Gen_Flag && !type.IsEnum() && !typeof(Delegate).IsAssignableFrom(type) && Utils.IsPublic(type))
        {
            Type wrap = ce.EmitTypeWrap(type);
            MethodInfo method = wrap.GetMethod("__Register", BindingFlags.Static | BindingFlags.Public);
            method.Invoke(null, new object[] { L });
        }
        else
        {
            Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
        }
#else
        Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
#endif
        // ...
    }
    if (top != LuaAPI.lua_gettop(L))
    {
        throw new Exception("top change, before:" + top + ", after:" + LuaAPI.lua_gettop(L));
    }

    foreach (var nested_type in type.GetNestedTypes(BindingFlags.Public))
    {
        if (nested_type.IsGenericTypeDefinition())  // 過濾泛型型別定義
        {
            continue;
        }
        GetTypeId(L, nested_type);
    }
    
    return true;
}

TryDelayWrapLoader主要用來處理兩種情況

  1. 通過delayWrap判斷,是否有為該類生成程式碼,如果有,直接使用生成函式進行填充元表(loader方法)。在xLua的生成程式碼中有一個XLuaGenAutoRegister.cs檔案,在這個檔案中會為對應的類註冊初始化器,而這個初始化器負責將類對應的元表生成函式新增到delayWrap中。
    // XLuaGenAutoRegister.cs
    public class XLua_Gen_Initer_Register__
    {
        static void wrapInit0(LuaEnv luaenv, ObjectTranslator translator)
        {
            // ...
            translator.DelayWrapLoader(typeof(TestXLua), TestXLuaWrap.__Register);  // 將型別對應的元表填充函式__Register新增到delayWrap中
            // ...
        }
        
        static void Init(LuaEnv luaenv, ObjectTranslator translator)
        {
            wrapInit0(luaenv, translator);
            translator.AddInterfaceBridgeCreator(typeof(System.Collections.IEnumerator), SystemCollectionsIEnumeratorBridge.__Create);
        }
        
        static XLua_Gen_Initer_Register__()
        {
    	    XLua.LuaEnv.AddIniter(Init);  // 註冊初始化器
    	}
    }
    
  2. 如果沒有生成程式碼,通過反射填充元表(ReflectionWrap方法)

使用生成函式填充元表

以LuaCallCSharp修飾的TestXLua類為例來檢視生成函式是如何生成的

// TestXLua.cs
[LuaCallCSharp]
public class TestXLua
{
    public string Name;
    public void Test1(int a){
    }
    public static void Test2(int a, bool b, string c)
    {
    }
}

Generate Code之後生成的TestXLuaWrap.cs如下所示

public class TestXLuaWrap 
{
    public static void __Register(RealStatePtr L)
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        System.Type type = typeof(TestXLua);
        Utils.BeginObjectRegister(type, L, translator, 0, 1, 1, 1);
        Utils.RegisterFunc(L, Utils.METHOD_IDX, "Test1", _m_Test1);
        Utils.RegisterFunc(L, Utils.GETTER_IDX, "Name", _g_get_Name);
        Utils.RegisterFunc(L, Utils.SETTER_IDX, "Name", _s_set_Name);
        Utils.EndObjectRegister(type, L, translator, null, null,
            null, null, null);
        Utils.BeginClassRegister(type, L, __CreateInstance, 2, 0, 0);
        Utils.RegisterFunc(L, Utils.CLS_IDX, "Test2", _m_Test2_xlua_st_);
        Utils.EndClassRegister(type, L, translator);
    }
    
    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int __CreateInstance(RealStatePtr L)
    {
        try {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            if(LuaAPI.lua_gettop(L) == 1)
            {
                TestXLua gen_ret = new TestXLua();
                translator.Push(L, gen_ret);
                return 1;
            }
        }
        catch(System.Exception gen_e) {
            return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
        }
        return LuaAPI.luaL_error(L, "invalid arguments to TestXLua constructor!");
        
    }

    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int _m_Test1(RealStatePtr L)
    {
        try {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
            {
                int _a = LuaAPI.xlua_tointeger(L, 2);
                gen_to_be_invoked.Test1( _a );
                return 0;
            }
        } catch(System.Exception gen_e) {
            return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
        }
    }
    
    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int _m_Test2_xlua_st_(RealStatePtr L)
    {
        try {
            {
                int _a = LuaAPI.xlua_tointeger(L, 1);
                bool _b = LuaAPI.lua_toboolean(L, 2);
                string _c = LuaAPI.lua_tostring(L, 3);
                TestXLua.Test2( _a, _b, _c );
                return 0;
            }
        } catch(System.Exception gen_e) {
            return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
        }
    }
    
    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int _g_get_Name(RealStatePtr L)
    {
        try {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        
            TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
            LuaAPI.lua_pushstring(L, gen_to_be_invoked.Name);
        } catch(System.Exception gen_e) {
            return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
        }
        return 1;
    }
    
    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int _s_set_Name(RealStatePtr L)
    {
        try {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        
            TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
            gen_to_be_invoked.Name = LuaAPI.lua_tostring(L, 2);
        
        } catch(System.Exception gen_e) {
            return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
        }
        return 0;
    }
}

生成函式__Register主要是這樣一個框架

  1. Utils.BeginObjectRegister,在對類的非靜態值(例如成員變數,成員方法等)進行註冊前做一些準備工作。主要是為元表新增__gc和__tostring元方法,以及準備好method表、getter表、setter表,後面呼叫RegisterFunc時,可以選擇插入到對應的表中

    // Utils.cs
    public static void BeginObjectRegister(Type type, RealStatePtr L, ObjectTranslator translator, int meta_count, int method_count, int getter_count,
        int setter_count, int type_id = -1)
    {
        if (type == null)
        {
            if (type_id == -1) throw new Exception("Fatal: must provide a type of type_id");
            LuaAPI.xlua_rawgeti(L, LuaIndexes.LUA_REGISTRYINDEX, type_id);
        }
        else
        {
            LuaAPI.luaL_getmetatable(L, type.FullName);
            // 如果type.FullName對應的元表是空,則建立一個新的元表,並設定到登錄檔中
            if (LuaAPI.lua_isnil(L, -1))
            {
                LuaAPI.lua_pop(L, 1);
                LuaAPI.luaL_newmetatable(L, type.FullName);
            }
        }
        LuaAPI.lua_pushlightuserdata(L, LuaAPI.xlua_tag());
        LuaAPI.lua_pushnumber(L, 1);
        LuaAPI.lua_rawset(L, -3);  // 為元表設定標誌
    
        if ((type == null || !translator.HasCustomOp(type)) && type != typeof(decimal))
        {
            LuaAPI.xlua_pushasciistring(L, "__gc");
            LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta);
            LuaAPI.lua_rawset(L, -3);  // 為元表設定__gc方法
        }
    
        LuaAPI.xlua_pushasciistring(L, "__tostring");
        LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.ToStringMeta);
        LuaAPI.lua_rawset(L, -3);  // 為元表設定__tostring方法
    
        if (method_count == 0)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            LuaAPI.lua_createtable(L, 0, method_count);  // 建立method表
        }
    
        if (getter_count == 0)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            LuaAPI.lua_createtable(L, 0, getter_count);  // 建立getter表
        }
    
        if (setter_count == 0)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            LuaAPI.lua_createtable(L, 0, setter_count);  // 建立setter表
        }
    }
    
  2. 多個Utils.RegisterFunc,將類的每個非靜態值對應的包裹方法註冊到不同的Lua表中。包裹方法是Generate Code時動態生成的,對於類的屬性會生成兩個包裹方法,分別是get和set包裹方法。

    例如成員方法Test1對應的包裹方法是_m_Test1,並被註冊到了method表中。Name變數的_g_get_Name包裹方法被註冊到getter表,而_s_set_Name包裹方法被註冊到setter表。這個包裹方法只是對原來方法的一層包裹,呼叫這個包裹方法本質上就是呼叫原來的方法。至於為什麼需要生成包裹方法,後面會再講到

    // Utils.cs RegisterFunc根據不同的巨集定義會有不同的版本,但大同小異
    public static void RegisterFunc(RealStatePtr L, int idx, string name, LuaCSFunction func)
    {
        idx = abs_idx(LuaAPI.lua_gettop(L), idx);
        LuaAPI.xlua_pushasciistring(L, name);
        LuaAPI.lua_pushstdcallcfunction(L, func);
        LuaAPI.lua_rawset(L, idx);  // 將idx指向的表中新增鍵值對 name = func
    }
    
  3. Utils.EndObjectRegister,結束對類的非靜態值的註冊。主要邏輯是為元表生成__index元方法和__newindex元方法,這也是Lua呼叫C#的核心所在

    // Utils.cs
    public static void EndObjectRegister(Type type, RealStatePtr L, ObjectTranslator translator, LuaCSFunction csIndexer,
        LuaCSFunction csNewIndexer, Type base_type, LuaCSFunction arrayIndexer, LuaCSFunction arrayNewIndexer)
    {
        int top = LuaAPI.lua_gettop(L);
        int meta_idx = abs_idx(top, OBJ_META_IDX);
        int method_idx = abs_idx(top, METHOD_IDX);
        int getter_idx = abs_idx(top, GETTER_IDX);
        int setter_idx = abs_idx(top, SETTER_IDX);
    
        //begin index gen
        LuaAPI.xlua_pushasciistring(L, "__index");
        LuaAPI.lua_pushvalue(L, method_idx);  // 1. 壓入methods表
        LuaAPI.lua_pushvalue(L, getter_idx);  // 2. 壓入getters表
    
        if (csIndexer == null)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            // ...
            LuaAPI.lua_pushstdcallcfunction(L, csIndexer);  // 3. 壓入csindexer
            // ...
        }
    
        translator.Push(L, type == null ? base_type : type.BaseType());  // 4. 壓入base
    
        LuaAPI.xlua_pushasciistring(L, LuaIndexsFieldName);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);  // 5. 壓入indexfuncs
        if (arrayIndexer == null)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            // ...
            LuaAPI.lua_pushstdcallcfunction(L, arrayIndexer);  // 6. 壓入arrayindexer
            // ...
        }
    
        LuaAPI.gen_obj_indexer(L);  // 生成__index元方法
    
        if (type != null)
        {
            LuaAPI.xlua_pushasciistring(L, LuaIndexsFieldName);
            LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);//store in lua indexs function tables
            translator.Push(L, type);
            LuaAPI.lua_pushvalue(L, -3);
            LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaIndexs][type] = __index函式
            LuaAPI.lua_pop(L, 1);
        }
    
        LuaAPI.lua_rawset(L, meta_idx);
        //end index gen
    
        //begin newindex gen
        LuaAPI.xlua_pushasciistring(L, "__newindex");
        LuaAPI.lua_pushvalue(L, setter_idx);
    
        if (csNewIndexer == null)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            // ...
            LuaAPI.lua_pushstdcallcfunction(L, csNewIndexer);
            // ...
        }
    
        translator.Push(L, type == null ? base_type : type.BaseType());
    
        LuaAPI.xlua_pushasciistring(L, LuaNewIndexsFieldName);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    
        if (arrayNewIndexer == null)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            // ...
            LuaAPI.lua_pushstdcallcfunction(L, arrayNewIndexer);
            // ...
        }
    
        LuaAPI.gen_obj_newindexer(L);  // 生成__newindex元方法
    
        if (type != null)
        {
            LuaAPI.xlua_pushasciistring(L, LuaNewIndexsFieldName);
            LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);//store in lua newindexs function tables
            translator.Push(L, type);
            LuaAPI.lua_pushvalue(L, -3);
            LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaNewIndexs][type] = __newindex函式
            LuaAPI.lua_pop(L, 1);
        }
    
        LuaAPI.lua_rawset(L, meta_idx);
        //end new index gen
        LuaAPI.lua_pop(L, 4);
    }
    

    __index元方法是通過呼叫gen_obj_indexer獲得的,在呼叫該方法前會依次壓入6個引數(程式碼註釋中有標註),gen_obj_indexer內部又會再壓入一個nil值,用於為baseindex提前佔位。共7個引數會作為upvalue關聯到閉包obj_indexer。obj_indexer函式就是__index元方法,它的邏輯是當訪問userdata[key]時,先依次查詢之前通過RegisterFunc填充的methods,getters等表中是否存有對應key的包裹方法,如果有則直接使用,如果沒有則遞迴在父類中查詢。__newindex元方法是通過呼叫gen_obj_newindexer獲得的,與__index的獲得原理類似,這裡就不再列出了。

    // xlua.c
    LUA_API int gen_obj_indexer(lua_State *L) {
        lua_pushnil(L);
        lua_pushcclosure(L, obj_indexer, 7);
        return 0;
    }
    
    //upvalue --- [1]: methods, [2]:getters, [3]:csindexer, [4]:base, [5]:indexfuncs, [6]:arrayindexer, [7]:baseindex
    //param   --- [1]: obj, [2]: key
    LUA_API int obj_indexer(lua_State *L) {	
        if (!lua_isnil(L, lua_upvalueindex(1))) {  // 如果methods中有key,則使用methods[key]
            lua_pushvalue(L, 2);
            lua_gettable(L, lua_upvalueindex(1));
            if (!lua_isnil(L, -1)) {//has method
                return 1;
            }
            lua_pop(L, 1);
        }
        
        if (!lua_isnil(L, lua_upvalueindex(2))) {  // 如果getters中key,則呼叫getters[key]
            lua_pushvalue(L, 2);
            lua_gettable(L, lua_upvalueindex(2));
            if (!lua_isnil(L, -1)) {//has getter
                lua_pushvalue(L, 1);
                lua_call(L, 1, 1);
                return 1;
            }
            lua_pop(L, 1);
        }
        
        
        if (!lua_isnil(L, lua_upvalueindex(6)) && lua_type(L, 2) == LUA_TNUMBER) {  // 如果arrayindexer中有key且key是數字,則呼叫arrayindexer[key]
            lua_pushvalue(L, lua_upvalueindex(6));
            lua_pushvalue(L, 1);
            lua_pushvalue(L, 2);
            lua_call(L, 2, 1);
            return 1;
        }
        
        if (!lua_isnil(L, lua_upvalueindex(3))) {  // 如果csindexer中有key,則呼叫csindexer[key]
            lua_pushvalue(L, lua_upvalueindex(3));
            lua_pushvalue(L, 1);
            lua_pushvalue(L, 2);
            lua_call(L, 2, 2);
            if (lua_toboolean(L, -2)) {
                return 1;
            }
            lua_pop(L, 2);
        }
        
        if (!lua_isnil(L, lua_upvalueindex(4))) {  // 遞迴向上在base中查詢
            lua_pushvalue(L, lua_upvalueindex(4));
            while(!lua_isnil(L, -1)) {
                lua_pushvalue(L, -1);
                lua_gettable(L, lua_upvalueindex(5));
                if (!lua_isnil(L, -1)) // found
                {
                    lua_replace(L, lua_upvalueindex(7)); //baseindex = indexfuncs[base]
                    lua_pop(L, 1);
                    break;
                }
                lua_pop(L, 1);
                lua_getfield(L, -1, "BaseType");
                lua_remove(L, -2);
            }
            lua_pushnil(L);
            lua_replace(L, lua_upvalueindex(4));//base = nil
        }
        
        if (!lua_isnil(L, lua_upvalueindex(7))) {  
            lua_settop(L, 2);
            lua_pushvalue(L, lua_upvalueindex(7));  
            lua_insert(L, 1);
            lua_call(L, 2, 1);  // 呼叫父類的__index,indexfuncs[base](obj, key)
            return 1;
        } else {
            return 0;
        }
    }
    
  4. Utils.BeginClassRegister,在對類的靜態值(例如靜態變數,靜態方法等)進行註冊前做一些準備工作。主要是為類生成對應的cls_table表,以及提前建立好static_getter表與static_setter表,後續用來存放靜態欄位對應的get和set包裹方法。注意這裡還會為cls_table設定元表meta_table

    // Utils.cs
    public static void BeginClassRegister(Type type, RealStatePtr L, LuaCSFunction creator, int class_field_count,
        int static_getter_count, int static_setter_count)
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        LuaAPI.lua_createtable(L, 0, class_field_count);
    
        LuaAPI.xlua_pushasciistring(L, "UnderlyingSystemType");
        translator.PushAny(L, type);
        LuaAPI.lua_rawset(L, -3);
    
        int cls_table = LuaAPI.lua_gettop(L);
    
        SetCSTable(L, type, cls_table);
    
        LuaAPI.lua_createtable(L, 0, 3);
        int meta_table = LuaAPI.lua_gettop(L);
        if (creator != null)
        {
            LuaAPI.xlua_pushasciistring(L, "__call");
    #if GEN_CODE_MINIMIZE
            translator.PushCSharpWrapper(L, creator);
    #else
            LuaAPI.lua_pushstdcallcfunction(L, creator);
    #endif
            LuaAPI.lua_rawset(L, -3);
        }
    
        if (static_getter_count == 0)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            LuaAPI.lua_createtable(L, 0, static_getter_count);   // 建立好static_getter表
        }
    
        if (static_setter_count == 0)
        {
            LuaAPI.lua_pushnil(L);
        }
        else
        {
            LuaAPI.lua_createtable(L, 0, static_setter_count);  // 建立好static_setter表
        }
        LuaAPI.lua_pushvalue(L, meta_table);
        LuaAPI.lua_setmetatable(L, cls_table);  // 設定元表
    }
    

    cls_table表是根據類的名稱空間名逐層新增到登錄檔中的,主要是通過SetCSTable實現。

    // Utils.cs
    public static void SetCSTable(RealStatePtr L, Type type, int cls_table)
    {
        int oldTop = LuaAPI.lua_gettop(L);
        cls_table = abs_idx(oldTop, cls_table);
        LuaAPI.xlua_pushasciistring(L, LuaEnv.CSHARP_NAMESPACE);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    
        List<string> path = getPathOfType(type);
    
        // 對於A.B.C來說
    
        // for迴圈處理A.B
        // 1. 登錄檔[xlua_csharp_namespace][A] = {} 且出棧 登錄檔[xlua_csharp_namespace]
        // 2. 登錄檔[xlua_csharp_namespace][A][B] = {} 且出棧 登錄檔[xlua_csharp_namespace][A]
    
        for (int i = 0; i < path.Count - 1; ++i)
        {
            LuaAPI.xlua_pushasciistring(L, path[i]);
            if (0 != LuaAPI.xlua_pgettable(L, -2))
            {
                var err = LuaAPI.lua_tostring(L, -1);
                LuaAPI.lua_settop(L, oldTop);
                throw new Exception("SetCSTable for [" + type + "] error: " + err);
            }
            if (LuaAPI.lua_isnil(L, -1))  // 如果 登錄檔[xlua_csharp_namespace] 中沒有key path[i] , 則新增一個 path[i] = {} 鍵值對
            {
                LuaAPI.lua_pop(L, 1);
                LuaAPI.lua_createtable(L, 0, 0);
                LuaAPI.xlua_pushasciistring(L, path[i]);
                LuaAPI.lua_pushvalue(L, -2);
                LuaAPI.lua_rawset(L, -4);
            }
            else if (!LuaAPI.lua_istable(L, -1))
            {
                LuaAPI.lua_settop(L, oldTop);
                throw new Exception("SetCSTable for [" + type + "] error: ancestors is not a table!");
            }
            LuaAPI.lua_remove(L, -2);
        }
    
        // 處理C
        // 登錄檔[xlua_csharp_namespace][A][B][C] = cls_table 且出棧 [xlua_csharp_namespace][A][B][C]
        LuaAPI.xlua_pushasciistring(L, path[path.Count - 1]);
        LuaAPI.lua_pushvalue(L, cls_table);
        LuaAPI.lua_rawset(L, -3);  
        LuaAPI.lua_pop(L, 1);
    
        // 在 登錄檔[xlua_csharp_namespace] 中新增鍵值對 [type對應的lua代理userdata] = cls_table
        LuaAPI.xlua_pushasciistring(L, LuaEnv.CSHARP_NAMESPACE);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
        ObjectTranslatorPool.Instance.Find(L).PushAny(L, type);
        LuaAPI.lua_pushvalue(L, cls_table);
        LuaAPI.lua_rawset(L, -3);
        LuaAPI.lua_pop(L, 1);
    }
    

    以A.B.C類為例,將在Lua登錄檔中新增以下表結構,而Lua登錄檔[xlua_csharp_namespace]實際上對應的就是CS全域性表,所以要在xLua中訪問C#類時才可以直接使用CS.A.B.C這樣的形式

    Lua登錄檔 = {
        xlua_csharp_namespace = {  -- 就是CS全域性表
            A = {
                B = {
                    C = cls_table
                }
            },
        },
    }
    
  5. 多個Utils.RegisterFunc,與BeginObjectRegister到EndObjectRegister之間的RegisterFunc作用相同,將類的每個靜態值對應的包裹方法註冊到對應的Lua表中。靜態變數對應的get和set包裹方法會被分別註冊到static_getter表和static_setter表(只讀的靜態變數除外)

  6. Utils.EndClassRegister,結束對類的靜態值的註冊。與EndObjectRegister類似,但它是為cls_table的元表meta_tabl設定__index元方法和__newindex元方法

    // Utils.cs
    public static void EndClassRegister(Type type, RealStatePtr L, ObjectTranslator translator)
    {
        int top = LuaAPI.lua_gettop(L);
        int cls_idx = abs_idx(top, CLS_IDX);
        int cls_getter_idx = abs_idx(top, CLS_GETTER_IDX);
        int cls_setter_idx = abs_idx(top, CLS_SETTER_IDX);
        int cls_meta_idx = abs_idx(top, CLS_META_IDX);
    
        //begin cls index
        LuaAPI.xlua_pushasciistring(L, "__index");
        LuaAPI.lua_pushvalue(L, cls_getter_idx);
        LuaAPI.lua_pushvalue(L, cls_idx);
        translator.Push(L, type.BaseType());
        LuaAPI.xlua_pushasciistring(L, LuaClassIndexsFieldName);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);  
        LuaAPI.gen_cls_indexer(L);
    
        LuaAPI.xlua_pushasciistring(L, LuaClassIndexsFieldName);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);//store in lua indexs function tables  
        translator.Push(L, type);
        LuaAPI.lua_pushvalue(L, -3);
        LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaClassIndexs][type] = __index函式
        LuaAPI.lua_pop(L, 1);
    
        LuaAPI.lua_rawset(L, cls_meta_idx);
        //end cls index
    
        //begin cls newindex
        LuaAPI.xlua_pushasciistring(L, "__newindex");
        LuaAPI.lua_pushvalue(L, cls_setter_idx);
        translator.Push(L, type.BaseType());
        LuaAPI.xlua_pushasciistring(L, LuaClassNewIndexsFieldName);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
        LuaAPI.gen_cls_newindexer(L);
    
        LuaAPI.xlua_pushasciistring(L, LuaClassNewIndexsFieldName);
        LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);//store in lua newindexs function tables
        translator.Push(L, type);
        LuaAPI.lua_pushvalue(L, -3);
        LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaClassNewIndexs][type] = __newindex函式
        LuaAPI.lua_pop(L, 1);
    
        LuaAPI.lua_rawset(L, cls_meta_idx);
        //end cls newindex
    
        LuaAPI.lua_pop(L, 4);
    }
    

上述6個部分的程式碼量比較大,邏輯也比較複雜,到這裡有必要做一個總結。

生成程式碼會為類的非靜態值都生成對應的包裹方法,並將包裹方法以 key = func 的形式註冊到不同的表中。userdata元表的__index和__newindex負責從這不同的表中找到對應key的包裹方法,最終通過呼叫包裹方法實現對C#物件的控制

-- lua測試程式碼
local obj = CS.TestXLua()
obj.Name = "test"  -- 賦值操作將觸發obj元表的__newindex,__newindex在setter表中找到Name對應的set包裹方法_s_set_Name,然後通過呼叫_s_set_Name方法設定了TestXLua物件的Name屬性為"test"

生成程式碼還會為每個類以名稱空間為層次結構生成cls_table表。與類的非靜態值相同,生成程式碼也會為類的靜態值都生成對應的包裹方法並註冊到不同的表中(注意這裡有些區別,類的靜態方法會被直接註冊到cls_table表中)。而cls_table元表的__index和__newindex負責從這不同的表中找到對應key的包裹方法,最終通過呼叫包裹方法實現對C#類的控制

-- lua測試程式碼
CS.TestXLua.Test2()  -- CS.TestXLua獲取到TestXLua類對應的cls_table,由於Test2是靜態方法,在cls_table中可以直接拿到其對應的包裹方法_m_Test2_xlua_st_,然後通過呼叫_m_Test2_xlua_st_而間接呼叫了TestXLua類的Test2方法

使用反射填充元表

當沒有生成程式碼時,會使用反射進行註冊,與生成程式碼進行註冊的邏輯基本相同。通過反射獲取到類的各個靜態值和非靜態值,然後分別註冊到不同的表中,以及填充__index和__newindex元方法

// Utils.cs
public static void ReflectionWrap(RealStatePtr L, Type type, bool privateAccessible)
{
    LuaAPI.lua_checkstack(L, 20);

    int top_enter = LuaAPI.lua_gettop(L);
    ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
    //create obj meta table
    LuaAPI.luaL_getmetatable(L, type.FullName);
    if (LuaAPI.lua_isnil(L, -1))
    {
        LuaAPI.lua_pop(L, 1);
        LuaAPI.luaL_newmetatable(L, type.FullName);
    }
    // 為元表新增xlua_tag標誌
    LuaAPI.lua_pushlightuserdata(L, LuaAPI.xlua_tag());
    LuaAPI.lua_pushnumber(L, 1);
    LuaAPI.lua_rawset(L, -3);  // 元表[xlua_tag] = 1
    int obj_meta = LuaAPI.lua_gettop(L);  

    LuaAPI.lua_newtable(L);
    int cls_meta = LuaAPI.lua_gettop(L);

    LuaAPI.lua_newtable(L);
    int obj_field = LuaAPI.lua_gettop(L);
    LuaAPI.lua_newtable(L);
    int obj_getter = LuaAPI.lua_gettop(L);
    LuaAPI.lua_newtable(L);
    int obj_setter = LuaAPI.lua_gettop(L);
    LuaAPI.lua_newtable(L);
    int cls_field = LuaAPI.lua_gettop(L);
    //set cls_field to namespace
    SetCSTable(L, type, cls_field);
    //finish set cls_field to namespace
    LuaAPI.lua_newtable(L);
    int cls_getter = LuaAPI.lua_gettop(L);
    LuaAPI.lua_newtable(L);
    int cls_setter = LuaAPI.lua_gettop(L);

    LuaCSFunction item_getter;
    LuaCSFunction item_setter;
    makeReflectionWrap(L, type, cls_field, cls_getter, cls_setter, obj_field, obj_getter, obj_setter, obj_meta,
        out item_getter, out item_setter, privateAccessible ? (BindingFlags.Public | BindingFlags.NonPublic) : BindingFlags.Public);

    // init obj metatable
    LuaAPI.xlua_pushasciistring(L, "__gc");
    LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta);
    LuaAPI.lua_rawset(L, obj_meta);

    LuaAPI.xlua_pushasciistring(L, "__tostring");
    LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.ToStringMeta);
    LuaAPI.lua_rawset(L, obj_meta);

    LuaAPI.xlua_pushasciistring(L, "__index");
    LuaAPI.lua_pushvalue(L, obj_field);  // 1.upvalue methods = obj_field
    LuaAPI.lua_pushvalue(L, obj_getter);  // 2.upvalue getters = obj_getter
    translator.PushFixCSFunction(L, item_getter);  // 3.upvalue csindexer = item_getter
    translator.PushAny(L, type.BaseType());  // 壓入BaseType,4.upvalue base
    LuaAPI.xlua_pushasciistring(L, LuaIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);  // 5.upvalue indexfuncs = 登錄檔[LuaIndexs]
    LuaAPI.lua_pushnil(L);  // 6.upvalue arrayindexer = nil
    LuaAPI.gen_obj_indexer(L);  // 生成__index函式
    //store in lua indexs function tables
    LuaAPI.xlua_pushasciistring(L, LuaIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);  
    translator.Push(L, type);  // 壓入type
    LuaAPI.lua_pushvalue(L, -3);
    LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaIndexs][type] = __index函式
    LuaAPI.lua_pop(L, 1);
    LuaAPI.lua_rawset(L, obj_meta); // set __index  即 obj_meta["__index"] = 生成的__index函式

    LuaAPI.xlua_pushasciistring(L, "__newindex");
    LuaAPI.lua_pushvalue(L, obj_setter);
    translator.PushFixCSFunction(L, item_setter);
    translator.Push(L, type.BaseType());
    LuaAPI.xlua_pushasciistring(L, LuaNewIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    LuaAPI.lua_pushnil(L);
    LuaAPI.gen_obj_newindexer(L);
    //store in lua newindexs function tables
    LuaAPI.xlua_pushasciistring(L, LuaNewIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    translator.Push(L, type);
    LuaAPI.lua_pushvalue(L, -3);
    LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaNewIndexs][type] = __newindex函式
    LuaAPI.lua_pop(L, 1);
    LuaAPI.lua_rawset(L, obj_meta); // set __newindex
                                    //finish init obj metatable

    LuaAPI.xlua_pushasciistring(L, "UnderlyingSystemType");
    translator.PushAny(L, type);
    LuaAPI.lua_rawset(L, cls_field);  // cls_field["UnderlyingSystemType"] = type  , 記錄類的基礎型別

    if (type != null && type.IsEnum())
    {
        LuaAPI.xlua_pushasciistring(L, "__CastFrom");
        translator.PushFixCSFunction(L, genEnumCastFrom(type));
        LuaAPI.lua_rawset(L, cls_field);
    }

    //init class meta
    LuaAPI.xlua_pushasciistring(L, "__index");
    LuaAPI.lua_pushvalue(L, cls_getter);
    LuaAPI.lua_pushvalue(L, cls_field);
    translator.Push(L, type.BaseType());
    LuaAPI.xlua_pushasciistring(L, LuaClassIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    LuaAPI.gen_cls_indexer(L);
    //store in lua indexs function tables
    LuaAPI.xlua_pushasciistring(L, LuaClassIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    translator.Push(L, type);
    LuaAPI.lua_pushvalue(L, -3);
    LuaAPI.lua_rawset(L, -3);  // 登錄檔[LuaClassIndexs][type] = __index函式
    LuaAPI.lua_pop(L, 1);
    LuaAPI.lua_rawset(L, cls_meta); // set __index 

    LuaAPI.xlua_pushasciistring(L, "__newindex");
    LuaAPI.lua_pushvalue(L, cls_setter);
    translator.Push(L, type.BaseType());
    LuaAPI.xlua_pushasciistring(L, LuaClassNewIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    LuaAPI.gen_cls_newindexer(L);
    //store in lua newindexs function tables
    LuaAPI.xlua_pushasciistring(L, LuaClassNewIndexsFieldName);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    translator.Push(L, type);
    LuaAPI.lua_pushvalue(L, -3);
    LuaAPI.lua_rawset(L, -3);  // // 登錄檔[LuaClassNewIndexs][type] = __newindex函式
    LuaAPI.lua_pop(L, 1);
    LuaAPI.lua_rawset(L, cls_meta); // set __newindex
    // ...
}

呼叫C#方法時引數的傳遞

先來解決前面遺留的一個問題,對於類的靜態值或是非靜態值為什麼都需要生成對應的包裹方法?其實包裹方法就是用來處理引數傳遞問題的。

為了正確的和Lua通訊,C函式已經定義好了協議。這個協議定義了引數以及返回值傳遞方法:C函式通過Lua中的棧來接受引數,引數以正序入棧(第一個引數首先入棧)。因此,當函式開始的時候,lua_gettop(L)可以返回函式收到的引數個數。第一個引數(如果有的話)在索引1的地方,而最後一個引數在索引lua_gettop(L)處。當需要向Lua返回值的時候,C函式只需要把它們以正序壓到堆疊上(第一個返回值最先壓入),然後返回這些返回值的個數。在這些返回值之下的,堆疊上的東西都會被Lua丟掉。和Lua函式一樣,從Lua中呼叫C函式可以有很多返回值。

也就是說,Lua這邊呼叫C函式時的引數會被自動的壓棧,這套機制Lua內部已經實現好了。文章開頭也提到,C#可以藉助C/C++來與Lua進行資料通訊,所以C#需要通過C API獲取到Lua傳遞過來的引數,而這個邏輯就被封裝在了包裹方法中。以TestXLua的Test1方法為例,它需要一個int引數。所以它的包裹方法需要通過C API獲取到一個int引數,然後再使用這個引數去呼叫真正的方法

[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_Test1(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
        {
            int _a = LuaAPI.xlua_tointeger(L, 2);  // 獲取到int引數
            gen_to_be_invoked.Test1( _a );  // 呼叫真正的Test1方法
            return 0;
        }
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
}

這也解釋了為什麼需要為類的屬性生成對應的get和set方法,因為只有將Lua的訪問或賦值操作轉換成函式呼叫形式時,引數才能利用函式呼叫機制被自動的壓棧,從而傳遞給C#

-- lua測試程式碼
obj.Name = "test"  -- 賦值操作
setter["Name"]("test")  -- 函式呼叫形式

這裡再提一下函式過載的問題,因為C#是支援過載的,所以會存在多個同名函式,但引數不同的情況。對於這種情況,只能通過同名函式被呼叫時傳遞的引數情況來判斷到底應該呼叫哪個函式

[LuaCallCSharp]
public class TestXLua
{
    // 函式過載Test1
    public void Test1(int a){
    }
    // 函式過載Test1
    public void Test1(bool b){
    }
}

// 為Test1生成的包裹方法
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_Test1(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
        int gen_param_count = LuaAPI.lua_gettop(L);
        if(gen_param_count == 2&& LuaTypes.LUA_TNUMBER == LuaAPI.lua_type(L, 2))  // 根據引數數量與型別判斷呼叫哪個方法
        {
            int _a = LuaAPI.xlua_tointeger(L, 2);
            gen_to_be_invoked.Test1( _a );
            return 0;
        }
        if(gen_param_count == 2&& LuaTypes.LUA_TBOOLEAN == LuaAPI.lua_type(L, 2))  // 根據引數數量與型別判斷呼叫哪個方法
        { 
            bool _b = LuaAPI.lua_toboolean(L, 2);
            gen_to_be_invoked.Test1( _b );
            return 0;
        }
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return LuaAPI.luaL_error(L, "invalid arguments to TestXLua.Test1!");
}

GC

C#和Lua都是有自動垃圾回收機制的,並且相互是無感知的。如果傳遞到Lua的C#物件被C#自動回收掉了,而Lua這邊仍毫不知情繼續使用,則必然會導致無法預知的錯誤。所以基本原則是傳遞到Lua的C#物件,C#不能自動回收,只能Lua在確定不再使用後通知C#進行回收

為了保證C#不會自動回收物件,所有傳遞給Lua的物件都會被objects保持引用。真實傳遞給Lua的物件索引就是物件在objects中的索引

Lua這邊為物件索引建立的userdata會被儲存在快取表中,而快取表的引用模式被設定為弱引用

// ObjectTranslator.cs
LuaAPI.lua_newtable(L);  // 建立快取表
LuaAPI.lua_newtable(L);  // 建立元表
LuaAPI.xlua_pushasciistring(L, "__mode");
LuaAPI.xlua_pushasciistring(L, "v");
LuaAPI.lua_rawset(L, -3);  // 元表[__mode] = v,表示這張表的所有值皆為弱引用
LuaAPI.lua_setmetatable(L, -2);  // 為快取表設定元表
cacheRef = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);

當Lua這邊不再引用這個userdata時,userdata會被從快取表中移除,Lua GC時會回收這個userdata,回收之前又會呼叫userdata元表的__gc方法,以此來通知C#,"我Lua這邊不再使用這個物件了,你該回收可以回收了"。在BeginObjectRegister方法內部,會為userdata的元表新增__gc方法

// Utils.cs BeginObjectRegister方法
if ((type == null || !translator.HasCustomOp(type)) && type != typeof(decimal))
{
    LuaAPI.xlua_pushasciistring(L, "__gc");
    LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta);
    LuaAPI.lua_rawset(L, -3);  // 為元表設定__gc方法
}

translator.metaFunctions.GcMeta實際上就是StaticLuaCallbacks的LuaGC方法

// StaticLuaCallbacks.cs
[MonoPInvokeCallback(typeof(LuaCSFunction))]
public static int LuaGC(RealStatePtr L)
{
    try
    {
        int udata = LuaAPI.xlua_tocsobj_safe(L, 1);
        if (udata != -1)
        {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            if ( translator != null )
            {
                translator.collectObject(udata);
            }
        }
        return 0;
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in LuaGC:" + e);
    }
}

LuaGC方法又會呼叫collectObject方法。在collectObject方法內部會將物件從objects移除,從而使物件不再被固定引用,能夠被C# GC正常回收

// ObjectTranslator.cs
internal void collectObject(int obj_index_to_collect)
{
    object o;
    
    if (objects.TryGetValue(obj_index_to_collect, out o))
    {
        objects.Remove(obj_index_to_collect);
        
        if (o != null)
        {
            int obj_index;
            //lua gc是先把weak table移除後再呼叫__gc,這期間同一個物件可能再次push到lua,關聯到新的index
            bool is_enum = o.GetType().IsEnum();
            if ((is_enum ? enumMap.TryGetValue(o, out obj_index) : reverseMap.TryGetValue(o, out obj_index))
                && obj_index == obj_index_to_collect)
            {
                if (is_enum)
                {
                    enumMap.Remove(o);
                }
                else
                {
                    reverseMap.Remove(o);
                }
            }
        }
    }
}

參考

相關文章