# 前言

很久以前也有拿着 XLua C# 这边的源码看过,网上也找过资料... 就是搞不大清楚。

可惜没人提醒,后来才想明白,直接硬看 C# 这边的源码是不行的,想明白 C# 与 XLua 的交互原理,至少得先了解 C/C++ 与 Lua 的交互原理

—— 毕竟 C# 与 XLua 交互,依然是基于中间的 C API,了解了那边的概念,再看 C# 与 XLua 交互原理,才好理解。

# 基本介绍

  • Lua 虚拟机由 C/C++ 实现,因此它可以直接与宿主进行通信
  • C# 则可以依靠 C API 通过 P/Invoke 方式调用 Lua 虚拟机函数
  • 即 C# 可以借助 C/C++ 来与 Lua 进行数据通信
  • XLua 相关 P/Invoke 调用接口位于 LuaDLL.cs 文件

# Lua 和 C/C++ 的数据交互

  • 基础:Lua 提供的一个虚拟栈
  • 两者所有类型的数据交换都通过这个栈完成
  • Lua 提供了两种索引方式操作虚拟栈
    • 正数索引:1 表示栈底
    • 反向索引:-1 表示栈顶
  • 例如:
    • 正向索引反向索引
      3-1
      2-2
      1-3

# Lua 调用 C/C++ 函数

  • 将 C++ 的函数包装成可供 Lua 调用的格式
    • 接收一个 Lua 状态机指针 ( IntPtr ) 的静态方法,该方法返回值为 int,表示方法返回值数量
  • 在 Lua 环境注册包装好的函数
  • Lua 调用
    • 首先通过 lua_gettop 获取 Lua 参数数量 (因为可能有重载)
    • 继续通过正数索引从 1 开始在 Lua 栈上获取具体参数值
    • 执行实际函数功能
    • 将返回值压栈
    • 包装函数的返回值为 int,表示返回值数量

# C/C++ 调用 Lua 函数

  • 使用 lua_getglobal(xlua_getglobal) 来获取函数,然后将其压入栈
  • 若函数有参则依次将函数的参数也压入栈
  • 调用 lua_pcall 让虚拟机执行函数
    • 参数分别为:
      • 虚拟机指针
      • 参数个数
      • 返回值个数
      • 错误处理函数,0 表示无,表示错误处理函数在栈中的索引
    • 如果运行出错, lua_pcall 会返回一个非零的结果
    • 若调用完毕没有出错,则可以通过 Lua 虚拟栈从中取出调用结果

# 基元类型传递

对于 bool、int 这样简单的值类型可以直接通过 C API 传递,见 LuaDLL.cs

  • xlua_pushinteger
  • lua_pushboolean
  • lua_pushnumber
  • xlua_pushuint

# 对象类型传递

# 基本流程

C# 与 Lua 交互依然还是依靠 C API 通过 P/Invoke 进行,为了正确的和 Lua 通讯,C# 与 Lua 通过相互保存的索引保持引用

对于 C# 对象,Lua 这边通过 Table 模拟,C# 对象在 Lua 对应的就是一个 userdata ,利用对象索引保持与 C# 对象的联系

  • 传递到 Lua 的只是 C# 对象的一个索引,并需要注册 C# 类型信息到 Lua 以便使用
  • 其中,对象的基本信息通过 XLua_Gen_Initer_Register__ 中初始化通过调用 ObjectTranslator.DelayWrapLoader 注册到 Lua 侧
  • userdata :特指 C# 对象在 Lua 这边对应的代理 userdata
    • userdata 设置的元表表示的实际是对象的类型信息,可以称为 “代理”
  • 在将 C# 对象传递到 Lua 以后,还需要告知 Lua 该对象的类型信息,比如对象类型有哪些成员方法,属性或是静态方法等。将这些都注册到 Lua 后,Lua 才能正确的调用
  • 对于 userdataindex ,主要由两个 C API 提供: LuaAPI.xlua_tocsobj_safe (实际上为 C API: lua_touserdata )、 LuaAPI.xlua_gettypeid (实际上为 C API lua_getmetatable )从 Lua 虚拟栈取值
    • 注:取出的是代理对象在 C# 侧的 ObjectPool 实例对象数组索引
LUA_API int xlua_tocsobj_safe(lua_State *L,int index) {
    int *udata = (int *)lua_touserdata (L,index);
    if (udata != NULL) {
        if (lua_getmetatable(L,index)) {
            lua_pushlightuserdata(L, &tag);
            lua_rawget(L,-2);
            if (!lua_isnil (L,-1)) {
                lua_pop (L, 2);
                return *udata;
            }
            lua_pop (L, 2);
        }
    }
    return -1;
}
LUA_API int xlua_tocsobj_fast (lua_State *L,int index) {
    int *udata = (int *)lua_touserdata (L,index);
    if(udata!=NULL) 
        return *udata;
    return -1;
}
LUA_API int xlua_gettypeid(lua_State *L, int idx) {
    int type_id = -1;
    if (lua_type(L, idx) == LUA_TUSERDATA) {
        if (lua_getmetatable (L, idx)) {
            lua_rawgeti(L, -1, 1);
            if (lua_type(L, -1) == LUA_TNUMBER) {
                type_id = (int)lua_tointeger(L, -1);
            }
            lua_pop(L, 2);
        }
    }
    return type_id;
}

类型的元表数据是通过 ObjectTranslator getTypeId 函数调用之前注册的 delayWrap 回调生成并注册到 Lua 侧的 (或通过反射生成)
主要的两个方法代码如下:

//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
    {
        if (type.IsArray)
        {
            if (common_array_meta == -1) throw new Exception("Fatal Exception! Array Metatable not inited!");
            return common_array_meta;
        }
        if (typeof(MulticastDelegate).IsAssignableFrom(type))
        {
            if (common_delegate_meta == -1) throw new Exception("Fatal Exception! Delegate Metatable not inited!");
            TryDelayWrapLoader(L, type);
            return common_delegate_meta;
        }
        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
        {
            if (type.IsEnum())
            {
                LuaAPI.xlua_pushasciistring(L, "__band");
                LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumAndMeta);
                LuaAPI.lua_rawset(L, -3);
                LuaAPI.xlua_pushasciistring(L, "__bor");
                LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumOrMeta);
                LuaAPI.lua_rawset(L, -3);
            }
            if (typeof(IEnumerable).IsAssignableFrom(type))
            {
                LuaAPI.xlua_pushasciistring(L, "__pairs");
                LuaAPI.lua_getref(L, enumerable_pairs_func);
                LuaAPI.lua_rawset(L, -3);
            }
            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);
            LuaAPI.lua_pop(L, 1);
            if (type.IsValueType())
            {
                typeMap.Add(type_id, type);
            }
            typeIdMap.Add(type, type_id);
        }
    }
    return type_id;
}
// 已加载类型列表
Dictionary<Type, bool> loaded_types = new Dictionary<Type, bool>();
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
{
    if (loaded_types.ContainsKey(type)) return true;
    loaded_types.Add(type, true);
    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 NOT_GEN_WARNING
        if (!typeof(Delegate).IsAssignableFrom(type))
        {
#if !XLUA_GENERAL
            UnityEngine.Debug.LogWarning(string.Format("{0} not gen, using reflection instead", type));
#else
            System.Console.WriteLine(string.Format("Warning: {0} not gen, using reflection instead", type));
#endif
        }
#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;
}
  1. 先判断是否生成过对应元数据,若存在这直接返回 typeIdMap 字典中 type 对应的 type_id
  • 注:数组是单独处理的, LuaEnv 构造函数最后调用的注册
  1. 若没有则先判断是否有生成代码,没有则反射 ( ReflectionWrap ) 填充元表
  • 反射时注册的 __call 元方法是个公共的 ObjectTranslator.methodWrapsCache.GetConstructorWrap 反射创建对象的操作回调
  • 反射创建的对象通过 PushAny 加入 ObjectPool (这里会判断实际类型去调用合适的添加操作,例如字符串直接调用 lua_pushstring ,基元类型调用 pushPrimitive 等)
  • 注:该方法会递归调用,若对象类型中有嵌套的公共类型,则递归注册

那么,Lua 如何知道调用呢?

  • 可以注意到 Lua 调用 C# 都是通过 CS.XXX 的方式进行的(因为我自己项目不是 XLua 项目,所以是看示例那些看的)
  • 而在 LuaEnv.cs 的构造方法中,会有 AddBuildin("CS", StaticLuaCallbacks.LoadCS) 的注册
  • 我怀疑这个是否就是将『CS』 注册为 Lua 侧的一个空表以当做命名空间?
  • Lua 使用 CS.XXX 的时候,就会到这边来查询

# 对象实例成员注册

生成代码中,通过 Utils.BeginObjectRegister 注册对象基本信息至 Lua 元表中

  • 如方法数量、getter_count、setter_count

    • 注:指实例方法及实例字段,静态变量和方法不在此列
    • 注:一个实例字段会被分别生成为 getter 与 setter 的静态 wrap 方法
  • 以及比较重要的 __gc 元方法等

    • 如果给对象设置了 __gc 元方法,那么当对象被 gc 回收时将会调用它的 __gc 元方法
    • C# 注册主要是为了当 Lua 回收 Lua 侧对应的 C# Table 对象后,同时可以允许回收 C# 这边的实际对象 (移除 C# 侧的缓存引用)
//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);
}
//StaticLuaCallbacks.cs 
//Lua 侧代理对象被回收后执行的回调
[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);
    }
}

然后注册实例字段、方法

  • 多个重载方法会被注册为一个静态 wrap 函数
    调用 Utils.EndObjectRegister 完成实例字段、方法注册

# 对象静态成员注册

调用 Utils.BeginClassRegister 注册创建该类型实例的回调及静态字段访问方法 static_getter_count、static_setter_count 数量

  • 若传递了创建实例类型的回调,则会注册到 Lua 的 __call 元方法中 (当 table 名字做为函数名字的形式被调用的时候,会调用 __call 函数)
//Utils.cs
//BeginClassRegister 注册静态数据时传递,注册创建对象实例回调
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);
}

注:代码生成始终会生成 __CreateInstance 即上述代码中 creator 这个回调,哪怕是静态类。
区别在于静态类要是调到了,是会直接报错的:

[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int __CreateInstance(RealStatePtr L)
{
    return LuaAPI.luaL_error(L, "TestS does not have a constructor!");
}

然后 Lua 侧创建时,例如官方 LuaCallCs.cs 示例中通过 local newGameObj2 = CS.UnityEngine.GameObject('helloworld') 创建了一个新的 GameObejct 对象,此时就是通过 Lua 侧被注册的 __call 元方法回调调用到 UnityEngineGameObjectWrap 中的 __CreateInstance 方法而创建的一个新对象

创建出新对象后,该对象会被代表该 Lua 状态机的 ObjectTranslator.ObjectPool 所缓存

  • ObjectPool 默认容量为 512 ,若超出则会双倍扩容

随后,通过 LuaAPI.xlua_pushcsobj 将返回的索引、C# 对象类型对应的 Lua 元表 type_id 等信息推送至 Lua 虚拟栈上,Lua 那边取值并用 userdata 缓存下来

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_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
    lua_setmetatable(L, -2);
}

最后就是:

  • 注册静态方法及字段
  • 调用 Utils.EndClassRegister 结束静态字段、方法注册

# 其它

  • xlua_pushlstring
    • 需要注意的是,LuaAPI 中封装了重载的接口,直接传递 string 类型的话
    • 会通过转化为 UTF8 编码的 bytes 数组传递
    • 有大小为 256 的数组缓存,小于该字节的走缓存,否则直接 GetBytes 转成字节数组传递
  • decimal 也是单独通过 LuaAPI.xlua_pushstruct 处理的

# 数据交互 - Lua 调 C#

  1. 首先通过 getTypeId 注册 C# 对象信息至 Lua 侧,并通过一个索引 (userdata) 保持联系
  2. Lua 这边调用 C 函数时的参数会被自动的压栈 (若为实例对象,则会将对象索引压栈至第一位)
  3. 然后,通过上述注册的元表信息,例如自动生成的 __Register 延迟注册的代码:
public static void __Register(RealStatePtr L)
{
    ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
    System.Type type = typeof(Test);
    Utils.BeginObjectRegister(type, L, translator, 0, 2, 4, 4);
    
    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, 3, 2, 2);
    Utils.RegisterFunc(L, Utils.CLS_IDX, "Test2", _m_Test2_xlua_st_);
    Utils.RegisterFunc(L, Utils.CLS_IDX, "Test4", _m_Test4_xlua_st_);     
    
    Utils.EndClassRegister(type, L, translator);
}
  • C# 这边的字段、方法都会被生成为 wrap 过的静态方法 (字段为两个 get、set 静态方法)
    • Wrap 方法主要将 Lua 的访问或赋值操作转换成函数调用形式
    • 生成的 wrap 方法是一个接收有一个参数,即接受 Lua 状态机指针 ( System.IntPtr ) 的静态方法
// 实例字段被编译而成的 getter,可视作普通实例方法的调用
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _g_get_Name(RealStatePtr L)
{
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
    
        Test gen_to_be_invoked = (Test)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;
}
  1. 生成的静态 Wrap 方法从 Lua 虚拟栈通过正数索引取参数值,然后调用实际方法,填入方法参数
  2. 实例方法编译出来的 lua 调用的方法,会先取缓存列表中实例对象,然后调用对应方法
  3. 重载函数必须通过同名函数被调用时传递的参数数量 (或类型) 来判断到底应该调用哪个函数
  • LuaAPI.lua_gettop(L) 获取参数数量(静态与实例均如此,当然若没有重载会省略这一步)
  • 后续
    • 实例对象从 index 1 获取对象类型索引 (userdata),从 index 2 开始获取实际方法参数值
    • 静态调用直接从 index 1 开始获取参数值
// 这是原本就是静态的方法
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_Test4_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);       
            Test.Test4( _a, _b, _c ); 
            return 0;
        }       
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    } 
}

函数返回值为 int,代表返回值数量

  1. 当 Lua 调用时,会调用 C# 这边的 Wrap 静态方法,并通过索引获取到对应对象,再调用指定方法
  • 函数通过 Lua 中的栈来接受 Lua 传递的参数,参数以正序入栈(第一个参数数量首先入栈)
    • 因此,当函数开始的时候, lua_gettop(L) 可以返回函数收到的参数个数
    • 并根据正数索引从 索引 1 开始取值

# 数据交互 - C# 调 XLua

  • 可以通过数据映射进行
  • 映射对象继承自 LuaBase ,如果是接口,并标记了 [CSharpCallLua] 特性,则会由 XLua 自动生成继承了 LuaBase 的桥接代码,该代码与 LuaTable 原理一致
  • 主要通过 luaenv.Global 全局 _G 表获取数据并映射至 C# 这边类型
    • 根据 Tutorial.CSCallLua 示例,对于引用类型映射 (即两边修改同步) 主要有 标记特性接口 和 LuaTable、委托 (加入过生成列表,见 LuaFunction.cs )
    • 否则普通的类型或直接取值,均通过值传递 (获取后就无关联)
  • 映射原理
    • 例如 Lua 侧 Table 被映射为 C# LuaTable 类型
    • LuaTable 继承自 LuaBase
    • LuaBase 中构造函数接受两个参数:
      • reference :Lua 中对象索引
      • luaenv :指定的 Lua 运行环境
    • C# 这边通过调用 LuaAPI.luaL_ref(L) 将指定对象放入一张 LUA_REGISTRYINDEX 的全局表
//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;
    }
    LuaAPI.lua_pushvalue(L, idx);
    return new LuaTable(LuaAPI.luaL_ref(L), translator.luaEnv);
}

因为 LuaBase 保存了对象在 LUA_REGISTRYINDEX 表的索引,因此可以再其中通过索引获取 Lua 侧对象,然后再通过虚拟栈进行交互

  • 也就是说 reference 指的不是栈上索引,而是这个全局表 ( LUA_REGISTRYINDEX ) 中的索引
  • 根据相关信息解释,对于该全局表,C 代码可以自由使用,但 Lua 代码不能访问

当获取值时,通过 LuaAPI.lua_getref(L, luaReference) 传入存储的 reference 获取

  • 见源码 public partial class LuaTable 中的 Get 方法
  • 也就是说对于映射的引用类型,并非是直接通过虚拟栈 (Lua 为每次函数调用都新分配了一个栈,因此在离开作用域之后,栈索引就失效了)
  • 而是通过保存对象在 LUA_REGISTRYINDEX 中的索引实现映射,在实际调用相关方法或字段时,通过存储的索引获取对应 Lua 表,再通过虚拟栈进行交互
  • 注:映射后函数调用与取值都是类似流程,区别在于函数调用会推入参数,调用 LuaAPI.lua_pcall 。见官方 Tutorial.CSCallLua.ItfD 生成的 TutorialCSCallLuaItfDBridge.add 方法 (委托啥的一样,反正都先要在 Table 取到)

当对象在 C# 这边被回收时,通过 LuaBase 析构函数的 Dispose 方法,调用 luaenvObjectTranslator.ReleaseLuaBase 将对象从 LUA_REGISTRYINDEX 表中删除 (随后该对象就能受 Lua 侧的垃圾回收了)

# 垃圾回收相关

C# 和 Lua 都有各自的垃圾回收机制,为了避免冲突,当使用了对方代理对象时,代理对象会被缓存,并在 真实对象 被回收后,移除缓存,使 代理对象 也能被回收

# Lua 传递至 C# 的对象

Lua 传递至 C# 的对象,会通过 LuaAPI.luaL_ref 保持引用 (取值也是通过这个) 而不被回收

  • C# 这边对象被回收后,将其从 LUA_REGISTRYINDEX 表中移除使其可以被 Lua 垃圾管理器回收
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);
    }
}

# C# 传递至 Lua 的对象

至于 C# 传递至 Lua 的对象,我们知道 C# 这边对象在 Lua 侧会被注册为元表

  • 在我们生成的元表数据,即 C# 对象的 Wrap 代码 (或反射生成) 的时候,就会将相关对象被 Lua 回收的回调注册到 Lua 中
LuaAPI.xlua_pushasciistring(L, "__gc");
LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta);
LuaAPI.lua_rawset(L, -3);

其中的 translator.metaFunctions.GcMeta(StaticLuaCallbacks) 就是当对象在 Lua 那边回收后,会将回收对象压栈,然后回调到 C# 这边注册的静态函数

随后, C# 这边通过回调传过来的 Lua 状态机指针,通过正向索引从 Lua 虚拟栈中获取到对应对象索引,从缓存列表移除,后续该对象就会受 C# 垃圾回收器回收

[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);
    }
}

# 问题:关于调用初始化

目前有点搞不清楚的问题就是: CS.UnityEngine.GameObject() 这种代码,实际上是什么时候被初始化的?

在 C# 这边源码中可以明显看到,自动生成的 wrap 代码是在 XLua_Gen_Initer_Register__ 通过 ObjectTranslator.DelayWrapLoader 注册的 —— 也就是说并不会立即加载

  • 在调用 ObjectTranslator.GetTypeId 才会判断是否注册过元表数据,判断是否反射或调用生成的 wrap 代码进行注册

例如上面提到过的官方示例 local newGameObj2 = CS.UnityEngine.GameObject('helloworld') 创建了一个新的 GameObejct 对象,在调用的时候这个元表应该还没被初始化设置到 Lua 侧

所以应该还有一个东西,让它可以在没有找到的时候,调用 ObjectTranslator.GetTypeId 注册的基本元表数据

  • 目前怀疑是: LuaEnv 构造函数中的对 __index 设置的 StaticLuaCallbacks.MetaFuncIndex 回调,但是看着又.... 不大确定,因为这里看着是固定加载索引为 2 的 Type,虽然调用了 GetTypeId ,不过难道不是只会初始化这一个吗?调用的指定类型呢?光看 C# 这边代码还是相当有点疑惑。
//StaticLuaCallbacks.cs 文件
[MonoPInvokeCallback(typeof(LuaCSFunction))]
public static int MetaFuncIndex(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        Type type = translator.FastGetCSObj(L, 2) as Type;
        if (type == null)
        {
            return LuaAPI.luaL_error(L, "#2 param need a System.Type!");
        }
        //UnityEngine.Debug.Log("============================load type by __index:" + type);
        //translator.TryDelayWrapLoader(L, type);
        translator.GetTypeId(L, type);
        LuaAPI.lua_pushvalue(L, 2);
        LuaAPI.lua_rawget(L, 1);
        return 1;
    }
    catch (System.Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in MetaFuncIndex:" + e);
    }
}
//ObjectTranslator.cs 文件
internal object FastGetCSObj(RealStatePtr L,int index)
{
    return getCsObj(L, index, LuaAPI.xlua_tocsobj_fast(L,index));
}
private object getCsObj(RealStatePtr L, int index, int udata)
{
    object obj;
    if (udata == -1)
    {
        if (LuaAPI.lua_type(L, index) != LuaTypes.LUA_TUSERDATA) return null;
        Type type = GetTypeOf(L, index);
        if (type == typeof(decimal))
        {
            decimal v;
            Get(L, index, out v);
            return v;
        }
        GetCSObject get;
        if (type != null && custom_get_funcs.TryGetValue(type, out get))
        {
            return get(L, index);
        }
        else
        {
            return null;
        }
    }
    else if (objects.TryGetValue(udata, out obj))
    {
#if !UNITY_5 && !XLUA_GENERAL && !UNITY_2017 && !UNITY_2017_1_OR_NEWER && !UNITY_2018
        if (obj != null && obj is UnityEngine.Object && ((obj as UnityEngine.Object) == null))
        {
            //throw new UnityEngine.MissingReferenceException("The object of type '"+ obj.GetType().Name +"' has been destroyed but you are still trying to access it.");
            return null;
        }
#endif
        return obj;
    }
    return null;
}

也许还有另一个可能?那就是这里其实是指的虚拟栈正数索引?但是感觉又不像.... 上边使用 index 传入 GetTypeOf 获取类型的方法如下:

//ObjectTranslator.cs 文件
public Type GetTypeOf(RealStatePtr L, int idx)
{
    Type type = null;
    int type_id = LuaAPI.xlua_gettypeid(L, idx);
    if (type_id != -1)
    {
        typeMap.TryGetValue(type_id, out type);
    }
    return type;
}

这里通过 LuaAPI.xlua_gettypeid 获取 type_id,然而 type_id 需求我们先注册了 (也就是) 才会有.... 陷入循环了?

还是说通过 ObjectTranslator.OpenLib 处理的?

后面还有诸如 AddBuildin("CS", StaticLuaCallbacks.LoadCS) 的代码,看着是将『CS』这个注册为一个 Lua 表当做命名空间?所以 Lua 那边调用,都是通过 CS. 调用的

疑惑.... 我们项目本身并不是 XLua 的,所以其实也不是很熟,研究了几天倒是一堆揣测。


# 对猜想的测试

突然想到 XLua C# 这边不是可以直接调试的么,何不直接调调看?硬看代码,不如实际来测试下看看。

# 猜测一:StaticLuaCallbacks.MetaFuncIndex 初始化

实际调试了一下, 初始化 Lua 虚拟机就向 __index 注册的 StaticLuaCallbacks.MetaFuncIndex 确实被调用到了

然后通过在 LuaCallCs 示例的 Lua 脚本前加上 print ,并查看 print 与 StaticLuaCallbacks.MetaFuncIndex 调用顺序:

  • 注:print 是在 LuaEnv 初始化时,通过 LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print) 注册的功能函数。
  • 结果 GameObject 创建完了,后续 print 都来了都没执行

该猜测 Pass

# 猜测二:ObjectTranslator.OpenLib 中注册的某个处理

直接在 ObjectTranslator.getTypeId 方法里边打断点,所有对象使用先必然先通过这里注册基本元数据,直接查看什么时候来的、怎么来的。
然后来到了... 之前猜测的 ObjectTranslator.OpenLib 中注册的 import_type ,即 StaticLuaCallbacks.ImportType 函数中:

(这好像就符合第二个猜测了)

然后进入 TryDelayWrapLoader ,因为之前我生成过代码,所以 delayWrap ,即之前提到过的生成代码注册的列表存在对应类型:

所以,在 Lua 侧调用不存在对象时,会调到 C# 侧的 import_type 代码,对类型进行实际注册,使其可以被调用。

  • 注:LuaEnv init_xlua 会有初始化 __index 元方法的 lua 代码,其中会判断调用 import_type

然后,若 Lua 代码通过 __call 方式调用,则 C# 调用对象注册的创建对应实例方法,创建对应实例,并两者映射起来。

  • 后续,不管是调用方法,还是获取变量,均通过正常交互流程进行了!

# 总结

最后,再来梳理一下流程:

Lua 调 C#

  • 首先,在创建一个 LuaEnv 环境时,会保存该环境返回的指针,并注册一些初始的公共静态函数
    • 例如生成的 wrap 代码的延迟注册回调 __RegisterXLuaGenAutoRegister.cs 添加至 D elayWrapLoader`
      • 注:结构体、枚举等自定义值类型会在 WrapPusher.cs 中单独注册类型(前提是加了 [XLua.LuaCallCSharp][GCOptimize] 这类 XLua 的特性、或者加到 GenConfig 也可以)
    • Lua 调用时,若对应类型还未进行实际数据注册,则会调到 ObjectTranslator.OpenLib 中注册的 import_type ,在该方法中调用注册的 __Register 回调去实际注册对象
      • 实例对象的创建方法 __CreateInstance 也是在此 ( __Register回调 ) 通过注册到 Lua__call 元方法进行
    • 而后,就可以实际工作了,查询 typeIdMap 是否存在对应类型,不存在则调用 TryDelayWrapLoader 进行类型实际初始化
      • 注:数组是单独处理的, LuaEnv 构造函数最后调用的注册
      • 注:若没有生成代码,则反射调用,由 Utils.ReflectionWrap 方法注册,即公共的反射调用回调替代生成代码
    • 调用时还会区分静态和非静态的实例调用 (虽然都是注册的静态 wrap 方法,但实际操作还是有区别的):
    • Lua 调过来的时候,会传递 LuaEnv 的指针,
    • 实例调用:
      • 通过字典查询得到实际 LuaEnv 对应的 ObjectTranslator
      • 通过正数索引 1Lua虚拟栈 获取对象索引,然后使用索引从 ObjectTranslator 获取 C# 侧实例对象
        • 若有调用方法有重载,则通过 lua_gettop 获取参数数量
      • 通过正数索引 2 开始获取实际参数
    • 静态调用:
      • Lua 调过来的时候,直接以正数索引从 1 开始取参数值
        • 若有调用方法有重载,则通过 lua_gettop 获取参数数量
    • 调用实际方法
    • 将方法调用结果压栈
    • 返回调用方法后,方法的返回值数量
    • Lua 侧拿到调用结果
  • 所以,静态字段或方法与实例的调用流程是一样的
  • 两者的主要区别是:是否需要通过额外对象索引参数去查找实际对象

C# 调用 Lua 则通过映射实现

  • luaenv.Global (初始化映射的 Lua _G 表)
  • 然后后续则通过调用 luaenv.Global.Get_G 表获取数据,并映射至 C# 侧对应对象结构
    • 继承 LuaBase (添加特性会自动生动对应 wrap 代码) 通过引用映射,两边保持对应索引
      • 调用对象时,通过去 LUA_REGISTRYINDEX 获取对应对象,并通过虚拟栈传递信息进行实际调用
    • 没有继承 LuaBase 的 会通过值传递,获取一次值后两边就无关系了

C# 侧缓存的 Lua 对象被缓存至 LUA_REGISTRYINDEX
Lua 侧创建的 C# 对象被缓存至 ObjectTranslator.ObjectPool

避免相互之前 GC 导致对象回收,当一边的代理对象被回收后,通知对面从缓存表移除缓存,然后执行真实对象的回收。

最后:

  • 对于静态方法,只需要根据虚拟机的 RealStatePtr 指针直接调用 C API 去 Lua虚拟栈 取值,然后调用实际方法即可。
  • 然而对于实例对象, 除了根据 RealStatePtr 去字典查询一次虚拟机 ObjectTranslator 外,还得在 ObjectTranslator.objects 中通过对象索引查找实际对象(当然因为 ObjectPool 是数组结构,其实还是挺快的),然后通过正向索引从 Lua虚拟栈 获取参数并调用
  • 因此实例对象的调用,会比静态方法、字典慢些 —— 另外要是只有一个虚拟机环境的需求,是否可以直接把通过字典查 Lua虚拟机 这一步给省掉?毕竟这一步主要是为支持多虚拟机环境,如果没用多虚拟机环境感觉好像可以?

可能写得稍微有点重复啰嗦,毕竟是一边看一边猜测,又一边修改的,不过也算加深映象了。虽然我自己项目还是纯 C# 在搞,不过毕竟公司在推 XLua,研究这个主要是避免别人问起来,都说不出什么深点的原理。

# 参考文档

  • 【最详细易懂】C++ 和 Lua 交互总结 鹅厂程序小哥的博客 - CSDN 博客
  • lua_pcall 详解_lua lua_pcall_俊哥兜里有糖的博客 - CSDN 博客
  • 为什么调用 lua_pcall
  • 在 C 语言中调用 lua 实现的回调函数_luaapi.lua_getref 是干什么的_superarhow 的博客 - CSDN 博客
  • lua gc 对象复活
  • Lua 与 C 交互之 LUA_REGISTRYINDEX(3) - RubbyZhang - 博客园
  • 深入 xLua 实现原理之 Lua 如何调用 C# - iwiniwin - 博客园
  • 深入 xLua 实现原理之 C# 如何调用 Lua - iwiniwin - 博客园
  • 5.1 函数和类型 - luaL_newstate - 《Lua 5.3 参考手册》 - 书栈网・BookStack
  • Lua 与 C 语言的互相调用 - 掘金
  • lua 源码编译及与 C/C++ 交互调用细节剖析
  • lua_touserdata - byfei - 博客园