风雨路人 发表于 2023-5-21 14:23

C#与XLua交互道理

前言

很久以前也有拿着 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 才能正确的调用
[*]对于 userdata 转 index,主要由两个 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;
}

[*]先判断是否生成过对应元数据,若存在这直接返回 typeIdMap 字典中 type 对应的 type_id
[*]注:数组是单独措置的,LuaEnv 构造函数最后调用的注册
[*]若没有则先判断是否有生成代码,没有则反射(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 侧代办代理对象被回收后执行的回调

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 这个回调,哪怕是静态类。 区别在于静态类要是调到了,是会直接报错的:

static int __CreateInstance(RealStatePtr L)
{
    return LuaAPI.luaL_error(L, ”TestS does not have a constructor!”);
}
然后 Lua 侧创建时,例如官方 LuaCallCs.cs 示例中通过 local newGameObj2 = CS.UnityEngine.GameObject(&#39;helloworld&#39;) 创建了一个新的 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#


[*]首先通过 getTypeId 注册 C# 对象信息至 Lua 侧,并通过一个索引(userdata)保持联系
[*]Lua 这边调用 C 函数时的参数会被自动的压栈(若为实例对象,则会将对象索引压栈至第一位)
[*]然后,通过上述注册的元表信息,例如自动生成的 __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,可视作普通实例方式的调用

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

[*]生成的静态 Wrap 方式从 Lua 虚拟栈通过正数索引取参数值,然后调用实际方式,填入方式参数
[*]实例方式编译出来的 lua 调用的方式,会先取缓存列表中实例对象,然后调用对应方式
[*]重载函数必需通过同名函数被调用时传递的参数数量(或类型)来判断到底应该调用哪个函数
[*]LuaAPI.lua_gettop(L) 获取参数数量(静态与实例均如此,当然若没有重载会省略这一步)


[*]后续


[*]实例对象从 index 1 获取对象类型索引(userdata),从 index 2 开始获取实际方式参数值
[*]静态调用直接从 index 1 开始获取参数值


//这是原本就是静态的方式

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,代表返回值数量

[*]当 Lua 调用时,会调用 C# 这边的 Wrap 静态方式,并通过索引获取到对应对象,再调用指定方式
[*]函数通过 Lua 中的栈来接受 Lua 传递的参数,参数以正序入栈(第一个参数数量首先入栈)

[*]因此,当函数开始的时候,lua_gettop(L) 可以返回函数收到的参数个数
[*]并按照正数索引从索引 1 开始取值

数据交互-C# 调 XLua


[*]可以通过数据映射进行
[*]映射对象担任自 LuaBase,如果是接口,并标识表记标帜了 特性,则会由 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表,再通过虚拟栈进行交互
当对象在 C# 这边被回收时,通过 LuaBase 析构函数的 Dispose 方式,调用 luaenv 的 ObjectTranslator.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
                LuaAPI.lua_pushnil(L);
                LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX); // LUA_REGISTRYINDEX = nil
            }
            else //another Delegate ref the function before the GC tick
            {
                LuaAPI.lua_pop(L, 2); // pop LUA_REGISTRYINDEX & 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# 垃圾回收器回收

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(&#39;helloworld&#39;) 创建了一个新的 GameObejct 对象,在调用的时候这个元表应该还没被初始化设置到 Lua 侧
所以应该还有一个东西,让它可以在没有找到的时候,调用 ObjectTranslator.GetTypeId 注册的基本元表数据

[*]目前怀疑是:LuaEnv 构造函数中的对 __index 设置的 StaticLuaCallbacks.MetaFuncIndex 回调,但是看着又....不大确定,因为这里看着是固定加载索引为 2 的 Type,虽然调用了 GetTypeId,不外难道不是只会初始化这一个吗?调用的指定类型呢?光看 C#这边代码还是相当有点疑惑。
//StaticLuaCallbacks.cs 文件

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 &#39;”+ obj.GetType().Name +”&#39; 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 的,所以其实也不是很熟,研究了几天倒是一堆测度。
<hr/>对猜想的测试

俄然想到 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 代码,对类型进行实际注册,使其可以被调用。
然后,若 Lua 代码通过 __call 方式调用,则 C# 调用对象注册的创建对应实例方式,创建对应实例,并两者映射起来。

[*]后续,不管是调用方式,还是获取变量,均通过正常交互流程进行了!
总结

最后,再来梳理一下流程:
Lua 调 C#

[*]首先,在创建一个 LuaEnv 环境时,会保留该环境返回的指针,并注册一些初始的公共静态函数
[*]例如生成的 wrap 代码的延迟注册回调 __Register 从 XLuaGenAutoRegister.cs 添加至 DelayWrapLoader`

[*]注:布局体、枚举等自定义值类型会在 WrapPusher.cs 中单独注册类型(前提是加了 、 这类 XLua 的特性、或者加到 GenConfig 也可以)

[*]当 Lua 调用时,若对应类型还未进行实际数据注册,则会调到 ObjectTranslator.OpenLib 中注册的 import_type ,在该方式中调用注册的 __Register 回调去实际注册对象

[*]实例对象的创建方式 __CreateInstance 也是在此(__Register回调)通过注册到 Lua 侧 __call 元方式进行

[*]尔后,就可以实际工作了,查询 typeIdMap 是否存在对应类型,不存在则调用 TryDelayWrapLoader 进行类型实际初始化

[*]注:数组是单独措置的,LuaEnv 构造函数最后调用的注册
[*]注:若没有生成代码,则反射调用,由 Utils.ReflectionWrap 方式注册,即公共的反射调用回调替代生成代码

[*]调用时还会区分静态和非静态的实例调用(虽然都是注册的静态 wrap 方式,但实际操作还是有区此外):
[*]当 Lua 调过来的时候,会传递 LuaEnv 的指针,
[*]实例调用:

[*]通过字典查询得到实际 LuaEnv 对应的 ObjectTranslator
[*]通过正数索引 1 从 Lua虚拟栈获取对象索引,然后使用索引从 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 - 博客园
页: [1]
查看完整版本: C#与XLua交互道理