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

iwiniwin發表於2021-09-24

本文主要是探討xLua下C#呼叫Lua的實現原理,有關Lua如何呼叫C#的介紹可以檢視深入xLua實現原理之Lua如何呼叫C#

C#與Lua資料通訊機制

無論是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);

除了普通的值型別之外,Lua中比較特殊但又很常用的大概就是table和funciton這兩種型別了,下面逐一來分析

傳遞Lua table到C#

以TestXLua類為例來看Lua table是如何被傳遞的,TestXLua有一個LuaTable型別的靜態變數,LuaTable是C#這邊定義的一個類,封裝了一些對Lua table的操作

// 注意,這裡新增的LuaCallCSharp特性只是為了使xLua為其生成程式碼,不新增並不影響功能
[LuaCallCSharp]
public class TestXLua
{
    public static LuaTable tab;
}

在點選Generate Code之後,部分生成程式碼如下所示。為tab變數生成了對應的set和get包裹方法

// TestXLuaWrap.cs
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _g_get_tab(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        translator.Push(L, TestXLua.tab);
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return 1;
}

[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _s_set_tab(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        TestXLua.tab = (XLua.LuaTable)translator.GetObject(L, 1, typeof(XLua.LuaTable));
    
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return 0;
}

為tab靜態變數賦值一個Lua table,table中包含一個 num = 1 鍵值對

-- Lua測試程式碼
local t = {
    num = 1
}
CS.TestXLua.tab = t

上述程式碼在賦值時,最終會呼叫到_s_set_tab包裹方法(具體原理可以檢視這裡),Lua這邊呼叫_s_set_tab前,會先將引數table t壓入到棧中,因此_s_set_tab內部需要通過translator.GetObject拿到這個table,並將其賦值給tab靜態變數

// ObjectTranslator.cs
public object GetObject(RealStatePtr L, int index, Type type)
{
    int udata = LuaAPI.xlua_tocsobj_safe(L, index);

    if (udata != -1)
    {
        // 對C#物件的處理
        object obj = objects.Get(udata);
        RawObject rawObject = obj as RawObject;
        return rawObject == null ? obj : rawObject.Target;
    }
    else
    {
        if (LuaAPI.lua_type(L, index) == LuaTypes.LUA_TUSERDATA)
        {
            GetCSObject get;
            int type_id = LuaAPI.xlua_gettypeid(L, index);
            if (type_id != -1 && type_id == decimal_type_id)
            {
                decimal d;
                Get(L, index, out d);
                return d;
            }
            Type type_of_struct;
            if (type_id != -1 && typeMap.TryGetValue(type_id, out type_of_struct) && type.IsAssignableFrom(type_of_struct) && custom_get_funcs.TryGetValue(type, out get))
            {
                return get(L, index);
            }
        }
        return (objectCasters.GetCaster(type)(L, index, null));
    }
}

GetObject方法負責從棧中獲取指定型別的物件,對於LuaTable型別是通過objectCasters.GetCaster獲取轉換器後,通過轉換器函式轉換得到

// ObjectTranslator.cs
public ObjectCast GetCaster(Type type)
{
    if (type.IsByRef) type = type.GetElementType();  // 如果是按引用傳遞的,則使用引用的物件的type

    Type underlyingType = Nullable.GetUnderlyingType(type);
    if (underlyingType != null)
    {
        return genNullableCaster(GetCaster(underlyingType)); 
    }
    ObjectCast oc;
    if (!castersMap.TryGetValue(type, out oc))
    {
        oc = genCaster(type);
        castersMap.Add(type, oc);
    }
    return oc;
}

xLua已經預設在castersMap中為一些型別定義好了轉換函式,其中就包括LuaTable型別

// ObjectCasters.cs
public ObjectCasters(ObjectTranslator translator)
{
    this.translator = translator;
    castersMap[typeof(char)] = charCaster;
    castersMap[typeof(sbyte)] = sbyteCaster;
    castersMap[typeof(byte)] = byteCaster;
    castersMap[typeof(short)] = shortCaster;
    castersMap[typeof(ushort)] = ushortCaster;
    castersMap[typeof(int)] = intCaster;
    castersMap[typeof(uint)] = uintCaster;
    castersMap[typeof(long)] = longCaster;
    castersMap[typeof(ulong)] = ulongCaster;
    castersMap[typeof(double)] = getDouble;
    castersMap[typeof(float)] = floatCaster;
    castersMap[typeof(decimal)] = decimalCaster;
    castersMap[typeof(bool)] = getBoolean;
    castersMap[typeof(string)] =  getString;
    castersMap[typeof(object)] = getObject;
    castersMap[typeof(byte[])] = getBytes;
    castersMap[typeof(IntPtr)] = getIntptr;
    //special type
    castersMap[typeof(LuaTable)] = getLuaTable;
    castersMap[typeof(LuaFunction)] = getLuaFunction;
}

LuaTable對應的轉換函式是getLuaTable

// ObjectCasters.cs
private object getLuaTable(RealStatePtr L, int idx, object target)
{
    if (LuaAPI.lua_type(L, idx) == LuaTypes.LUA_TUSERDATA)
    {
        object obj = translator.SafeGetCSObj(L, idx);
        return (obj != null && obj is LuaTable) ? obj : null;
    }
    if (!LuaAPI.lua_istable(L, idx))
    {
        return null;
    }
    // 處理普通table型別
    LuaAPI.lua_pushvalue(L, idx);
    return new LuaTable(LuaAPI.luaL_ref(L), translator.luaEnv);
}

getLuaTable的主要邏輯是將idx處的table通過luaL_ref新增到Lua登錄檔中並得到指向該table的索引,然後建立LuaTable物件儲存該索引。也就是說Lua table在C#這邊對應的是LuaTable物件,它們之間通過一個索引關聯起來,這個索引表示Lua table在Lua登錄檔中的引用,利用這個索引可以獲取到Lua table。拿到Lua table後,就可以繼續訪問Lua table的內容了。

// CS測試程式碼
int num = TestXLua.tab.Get<int>("num");

對Lua table的訪問操作都被封裝在LuaTable的Get方法中

// LuaTable.cs
public TValue Get<TValue>(string key)
{
    TValue ret;
    Get(key, out ret);
    return ret;
}

// no boxing version get
public void Get<TKey, TValue>(TKey key, out TValue value)
{
#if THREAD_SAFE || HOTFIX_ENABLE
    lock (luaEnv.luaEnvLock)
    {
#endif
        var L = luaEnv.L;
        var translator = luaEnv.translator;
        int oldTop = LuaAPI.lua_gettop(L);
        LuaAPI.lua_getref(L, luaReference);  // 通過luaReference獲取到對應的table
        translator.PushByType(L, key);

        if (0 != LuaAPI.xlua_pgettable(L, -2))  // 查詢 表[key]
        {
            string err = LuaAPI.lua_tostring(L, -1);
            LuaAPI.lua_settop(L, oldTop);
            throw new Exception("get field [" + key + "] error:" + err);
        }

        LuaTypes lua_type = LuaAPI.lua_type(L, -1);
        Type type_of_value = typeof(TValue);
        if (lua_type == LuaTypes.LUA_TNIL && type_of_value.IsValueType())
        {
            throw new InvalidCastException("can not assign nil to " + type_of_value.GetFriendlyName());
        }

        try
        {
            translator.Get(L, -1, out value);  // 獲取棧頂的元素,即 表[key]
        }
        catch (Exception e)
        {
            throw e;
        }
        finally
        {
            LuaAPI.lua_settop(L, oldTop);
        }
#if THREAD_SAFE || HOTFIX_ENABLE
    }
#endif
}

Get方法的主要邏輯是,先通過儲存的索引luaReference獲取到Lua table,然後通過xlua_pgettable將 表[key] 的值壓棧,最後通過translator.Get獲取到棧頂值對應的物件

// ObjectTranslator.cs
public void Get<T>(RealStatePtr L, int index, out T v)
{
    Func<RealStatePtr, int, T> get_func;
    if (tryGetGetFuncByType(typeof(T), out get_func))
    {
        v = get_func(L, index);  // 將給定索引處的值轉換為{T}型別
    }
    else
    {
        v = (T)GetObject(L, index, typeof(T));
    }
}

同樣的,xLua也在tryGetGetFuncByType中為一些基本型別預定義好了對應的物件獲取方法,採取泛型方式,這樣可以避免拆箱和裝箱。在本例中獲取的值 num = 1 是一個int型別,通過轉換器函式xlua_tointeger即可獲得。xlua_tointeger就是對Lua原生API lua_tointeger的一個簡單封裝

bool tryGetGetFuncByType<T>(Type type, out T func) where T : class
{
    if (get_func_with_type == null)
    {
        get_func_with_type = new Dictionary<Type, Delegate>()
        {
            {typeof(int), new Func<RealStatePtr, int, int>(LuaAPI.xlua_tointeger) },
            {typeof(double), new Func<RealStatePtr, int, double>(LuaAPI.lua_tonumber) },
            {typeof(string), new Func<RealStatePtr, int, string>(LuaAPI.lua_tostring) },
            {typeof(byte[]), new Func<RealStatePtr, int, byte[]>(LuaAPI.lua_tobytes) },
            {typeof(bool), new Func<RealStatePtr, int, bool>(LuaAPI.lua_toboolean) },
            {typeof(long), new Func<RealStatePtr, int, long>(LuaAPI.lua_toint64) },
            {typeof(ulong), new Func<RealStatePtr, int, ulong>(LuaAPI.lua_touint64) },
            {typeof(IntPtr), new Func<RealStatePtr, int, IntPtr>(LuaAPI.lua_touserdata) },
            {typeof(decimal), new Func<RealStatePtr, int, decimal>((L, idx) => {
                decimal ret;
                Get(L, idx, out ret);
                return ret;
            }) },
            {typeof(byte), new Func<RealStatePtr, int, byte>((L, idx) => (byte)LuaAPI.xlua_tointeger(L, idx) ) },
            {typeof(sbyte), new Func<RealStatePtr, int, sbyte>((L, idx) => (sbyte)LuaAPI.xlua_tointeger(L, idx) ) },
            {typeof(char), new Func<RealStatePtr, int, char>((L, idx) => (char)LuaAPI.xlua_tointeger(L, idx) ) },
            {typeof(short), new Func<RealStatePtr, int, short>((L, idx) => (short)LuaAPI.xlua_tointeger(L, idx) ) },
            {typeof(ushort), new Func<RealStatePtr, int, ushort>((L, idx) => (ushort)LuaAPI.xlua_tointeger(L, idx) ) },
            {typeof(uint), new Func<RealStatePtr, int, uint>(LuaAPI.xlua_touint) },
            {typeof(float), new Func<RealStatePtr, int, float>((L, idx) => (float)LuaAPI.lua_tonumber(L, idx) ) },
        };
    }

傳遞Lua function到C#

Lua的function傳遞到C#後,對應的是C#的委託,同樣以TestXLua類為例來分析具體過程

// 注意,這裡新增的LuaCallCSharp特性只是為了使xLua為其生成程式碼,不新增並不影響功能
[LuaCallCSharp]
public class TestXLua
{
    [CSharpCallLua]
    public delegate int Func(string s, bool b, float f);

    public static Func func;
}

點選Generate Code後,生成的部分TestXLuaWrap程式碼如下所示。為func變數生成了對應的set和get包裹方法

// TestXLuaWrap.cs
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _g_get_func(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        translator.Push(L, TestXLua.func);
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return 1;
}

[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _s_set_func(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        TestXLua.func = translator.GetDelegate<TestXLua.Func>(L, 1);
    
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return 0;
}

為func靜態變數賦值一個Lua function

-- Lua測試程式碼
CS.TestXLua.func = function(s, b, i)
    
end

上述程式碼在賦值時,會最終呼叫_s_set_func包裹方法(具體原理可以檢視這裡),Lua在呼叫_s_set_func前,會將引數function壓入到棧中,因此_s_set_func內部需要通過translator.GetDelegate拿到這個function,並將其賦值給func靜態變數

// ObjectTranslator.cs
public T GetDelegate<T>(RealStatePtr L, int index) where T :class
{
    
    if (LuaAPI.lua_isfunction(L, index))
    {
        return CreateDelegateBridge(L, typeof(T), index) as T;
    }
    else if (LuaAPI.lua_type(L, index) == LuaTypes.LUA_TUSERDATA)
    {
        return (T)SafeGetCSObj(L, index);
    }
    else
    {
        return null;
    }
}

對於Lua function型別會通過CreateDelegateBridge建立一個對應的委託並返回。CreateDelegateBridge內部會建立一個DelegateBridge物件來對應Lua function,原理和LuaTable類似,也是通過一個索引保持聯絡,利用這個索引可以獲取到Lua function

// ObjectTranslator.cs
Dictionary<int, WeakReference> delegate_bridges = new Dictionary<int, WeakReference>();  // 弱引用建立的DelegateBridge
public object CreateDelegateBridge(RealStatePtr L, Type delegateType, int idx)
{
    LuaAPI.lua_pushvalue(L, idx);
    LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
    // 對快取的處理
    if (!LuaAPI.lua_isnil(L, -1))
    {
        int referenced = LuaAPI.xlua_tointeger(L, -1);
        LuaAPI.lua_pop(L, 1);

        if (delegate_bridges[referenced].IsAlive)
        {
            if (delegateType == null)
            {
                return delegate_bridges[referenced].Target;
            }
            DelegateBridgeBase exist_bridge = delegate_bridges[referenced].Target as DelegateBridgeBase;
            Delegate exist_delegate;
            if (exist_bridge.TryGetDelegate(delegateType, out exist_delegate))
            {
                return exist_delegate;
            }
            else
            {
                exist_delegate = getDelegate(exist_bridge, delegateType);
                exist_bridge.AddDelegate(delegateType, exist_delegate);
                return exist_delegate;
            }
        }
    }
    else
    {
        LuaAPI.lua_pop(L, 1);
    }

    LuaAPI.lua_pushvalue(L, idx);
    int reference = LuaAPI.luaL_ref(L);  // 將idx處的元素新增到Lua登錄檔中
    LuaAPI.lua_pushvalue(L, idx);
    LuaAPI.lua_pushnumber(L, reference);
    LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX);  // 登錄檔[idx值] = reference
    DelegateBridgeBase bridge;
    try
    {
#if (UNITY_EDITOR || XLUA_GENERAL) && !NET_STANDARD_2_0
        if (!DelegateBridge.Gen_Flag)
        {
            bridge = Activator.CreateInstance(delegate_birdge_type, new object[] { reference, luaEnv }) as DelegateBridgeBase;  // 使用反射建立DelegateBridge物件
        }
        else
#endif
        {
            bridge = new DelegateBridge(reference, luaEnv);
        }
    }
    catch(Exception e)
    {
        LuaAPI.lua_pushvalue(L, idx);
        LuaAPI.lua_pushnil(L);
        LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX);
        LuaAPI.lua_pushnil(L);
        LuaAPI.xlua_rawseti(L, LuaIndexes.LUA_REGISTRYINDEX, reference);
        throw e;
    }
    if (delegateType == null)
    {
        delegate_bridges[reference] = new WeakReference(bridge);
        return bridge;
    }
    try {
        var ret = getDelegate(bridge, delegateType);  // 通過bridge獲取到指定型別的委託
        bridge.AddDelegate(delegateType, ret);
        delegate_bridges[reference] = new WeakReference(bridge);
        return ret;
    }
    catch(Exception e)
    {
        bridge.Dispose();
        throw e;
    }
}

在取得DelegateBridge物件後,還需要通過getDelegate方法,獲取delegateType型別的委託,即C#這邊指定要接收Lua function時宣告的委託型別。在本例中是typeof(TestXLua.Func)

Delegate getDelegate(DelegateBridgeBase bridge, Type delegateType)
{
    // ...
    Func<DelegateBridgeBase, Delegate> delegateCreator;
    if (!delegateCreatorCache.TryGetValue(delegateType, out delegateCreator))
    {
        // get by parameters
        MethodInfo delegateMethod = delegateType.GetMethod("Invoke");
        // 生成程式碼為配置了 CSharpCallLua的委託 生成以__Gen_Delegate_Imp開頭的方法 並新增到 DelegateBridge 類中
        var methods = bridge.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => !m.IsGenericMethodDefinition && (m.Name.StartsWith("__Gen_Delegate_Imp") || m.Name == "Action")).ToArray();
        // 查詢bridge中與delegateMethod匹配的方法,這個方法必須是以__Gen_Delegate_Imp或Action開頭
        for (int i = 0; i < methods.Length; i++)  
        {
            if (!methods[i].IsConstructor && Utils.IsParamsMatch(delegateMethod, methods[i]))
            {
                var foundMethod = methods[i];
                delegateCreator = (o) =>
#if !UNITY_WSA || UNITY_EDITOR
                    Delegate.CreateDelegate(delegateType, o, foundMethod);  // 建立表示foundMethod的delegateType型別的委託
#else
                    foundMethod.CreateDelegate(delegateType, o); 
#endif
                break;
            }
        }

        if (delegateCreator == null)
        {
            delegateCreator = getCreatorUsingGeneric(bridge, delegateType, delegateMethod);
        }
        delegateCreatorCache.Add(delegateType, delegateCreator);
    }

    ret = delegateCreator(bridge);  // 建立委託
    if (ret != null)
    {
        return ret;
    }

    throw new InvalidCastException("This type must add to CSharpCallLua: " + delegateType.GetFriendlyName());
}

如何利用bridge獲取到指定型別delegateType的委託呢?主要邏輯是,先獲得delegateType委託的Invoke方法,然後通過反射遍歷bridge型別的所有方法,找到與Invoke引數匹配的目標方法。再使用bridge例項與目標方法建立一個delegateType型別的委託。換言之,這個delegateType型別的委託繫結的是bridge的與之引數匹配的成員方法,而且這個方法名稱要以"__Gen_Delegate_Imp"開頭

用於接收Lua function的委託必須新增CSharpCallLua特性也正是因為要為其生成以"__Gen_Delegate_Imp"開頭的方法,如果不新增則會丟擲異常

 c# exception:System.InvalidCastException: This type must add to CSharpCallLua: TestXLua+Func

新增CSharpCallLua特性後,點選Generate Code,會為該委託生成如下程式碼。雖然程式碼生成在DelegatesGensBridge.cs檔案中,但它通過partial宣告為DelegateBridge類的一部分。生成的函式名均以"__Gen_Delegate_Imp"開頭,且引數型別和個數與該委託一致

// DelegatesGensBridge.cs
public partial class DelegateBridge : DelegateBridgeBase
{
    // ...
    public int __Gen_Delegate_Imp1(string p0, bool p1, float p2)
    {
#if THREAD_SAFE || HOTFIX_ENABLE
        lock (luaEnv.luaEnvLock)
        {
#endif
            RealStatePtr L = luaEnv.rawL;
            int errFunc = LuaAPI.pcall_prepare(L, errorFuncRef, luaReference);
            
            LuaAPI.lua_pushstring(L, p0);  // 壓棧引數
            LuaAPI.lua_pushboolean(L, p1);  // 壓棧引數
            LuaAPI.lua_pushnumber(L, p2);  // 壓棧引數
            
            PCall(L, 3, 1, errFunc);  // Lua function呼叫
            
            
            int __gen_ret = LuaAPI.xlua_tointeger(L, errFunc + 1);
            LuaAPI.lua_settop(L, errFunc - 1);
            return  __gen_ret;
#if THREAD_SAFE || HOTFIX_ENABLE
        }
#endif
    }
}

TestXLua.Func型別委託繫結的就是這個生成函式__Gen_Delegate_Imp1。之所以要使用生成函式,是因為需要生成函式來完成引數的壓棧與Lua function呼叫

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

文章開頭也已提到,C#可以藉助C/C++來與Lua進行資料通訊,所以C#在函式呼叫前,需要通過C API來壓棧函式呼叫所需的引數,而這個邏輯就被封裝在了以"__Gen_Delegate_Imp"開頭的生成方法中。生成方法將引數壓棧後,再通過PCall呼叫Lua function,PCall內部呼叫的就是Lua原生API lua_pcall

總結一下整個流程

-- Lua測試程式碼
CS.TestXLua.func = function(s, b, i)
    
end

當為TestXLua.func賦值Lua function時,會觸發func變數的set包裹方法_s_set_func,_s_set_func內部會獲取一個委託設定給func變數。這個委託繫結的是DelegateBridge物件的以"__Gen_Delegate_Imp"開頭的生成方法,DelegateBridge物件同時儲存著Lua function的索引

// CS測試程式碼
TestXLua.func("test", false, 3);

當呼叫TestXLua.func時,相當於呼叫以"__Gen_Delegate_Imp"開頭的生成方法,這個生成方法負責引數壓棧,並通過儲存的索引獲取到Lua function,然後使用lua_pcall完成Lua function的呼叫

GC

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

為了保證Lua不會自動回收物件,所有傳遞給C#的物件都會被Lua登錄檔引用。比如前面建立LuaTable或DelegateBridge時 都有呼叫LuaAPI.luaL_ref將物件新增到登錄檔中

C#這邊為對應的Lua物件定義了LuaBase基類,LuaTable或DelegateBridge均派生於LuaBase,這個類實現了IDisposable介面,並且在解構函式中會呼叫Dispose

// LuaBase.cs
public virtual void Dispose(bool disposeManagedResources)
{
    if (!disposed)
    {
        if (luaReference != 0)
        {
#if THREAD_SAFE || HOTFIX_ENABLE
            lock (luaEnv.luaEnvLock)
            {
#endif
                bool is_delegate = this is DelegateBridgeBase;
                if (disposeManagedResources)
                {
                    luaEnv.translator.ReleaseLuaBase(luaEnv.L, luaReference, is_delegate);  // 釋放Lua物件
                }
                else //will dispse by LuaEnv.GC
                {
                    luaEnv.equeueGCAction(new LuaEnv.GCAction { Reference = luaReference, IsDelegate = is_delegate });  // 加入GC佇列
                }
#if THREAD_SAFE || HOTFIX_ENABLE
            }
#endif
        }
        disposed = true;
    }
}

當disposeManagedResources為true時,直接呼叫ReleaseLuaBase釋放Lua物件

// ObjectTranslator.cs
public void ReleaseLuaBase(RealStatePtr L, int reference, bool is_delegate)
{
    if(is_delegate)
    {
        LuaAPI.xlua_rawgeti(L, LuaIndexes.LUA_REGISTRYINDEX, reference);
        if (LuaAPI.lua_isnil(L, -1))
        {
            LuaAPI.lua_pop(L, 1);
        }
        else
        {
            LuaAPI.lua_pushvalue(L, -1);
            LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
            if (LuaAPI.lua_type(L, -1) == LuaTypes.LUA_TNUMBER && LuaAPI.xlua_tointeger(L, -1) == reference) //
            {
                //UnityEngine.Debug.LogWarning("release delegate ref = " + luaReference);
                LuaAPI.lua_pop(L, 1);// pop LUA_REGISTRYINDEX[func]
                LuaAPI.lua_pushnil(L);
                LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX); // LUA_REGISTRYINDEX[func] = nil
            }
            else //another Delegate ref the function before the GC tick
            {
                LuaAPI.lua_pop(L, 2); // pop LUA_REGISTRYINDEX[func] & func
            }
        }

        LuaAPI.lua_unref(L, reference);
        delegate_bridges.Remove(reference);
    }
    else
    {
        LuaAPI.lua_unref(L, reference);
    }
}

ReleaseLuaBase的主要任務是將Lua物件從Lua登錄檔中移除,這樣Lua GC時發現該物件不再被引用,就可以進行回收了

// LuaEnv.cs
public void Tick()
{
#if THREAD_SAFE || HOTFIX_ENABLE
    lock (luaEnvLock)
    {
#endif
        var _L = L;
        lock (refQueue) 
        {
            while (refQueue.Count > 0)  // 遍歷GC佇列
            {
                GCAction gca = refQueue.Dequeue();
                translator.ReleaseLuaBase(_L, gca.Reference, gca.IsDelegate);
            }
        }
#if !XLUA_GENERAL
        last_check_point = translator.objects.Check(last_check_point, max_check_per_tick, object_valid_checker, translator.reverseMap);
#endif
#if THREAD_SAFE || HOTFIX_ENABLE
    }
#endif
}

當disposeManagedResources為false時,會將其加入GC佇列。當主動釋放Lua環境時,會遍歷GC佇列,再逐一呼叫ReleaseLuaBase進行釋放

參考

相關文章