Lua篇 — xLua

Posted by Xun on Thursday, November 4, 2021

由于很多旧项目是以CSharp开发的,热更时就会遇到难题。而xLua框架的出现,让很多项目能够在改动最小的情况下,支持热更新功能。

简介

初始化

创建LuaEnv

  • Lua状态机的创建方法主要实现如下
    public LuaEnv()
    {

        ...
                
        rawL = LuaAPI.luaL_newstate();

        //Init Base Libs
        LuaAPI.luaopen_xlua(rawL);
        LuaAPI.luaopen_i64lib(rawL);

        translator = new ObjectTranslator(this, rawL);
        translator.createFunctionMetatable(rawL);
        translator.OpenLib(rawL);
        ObjectTranslatorPool.Instance.Add(rawL, translator);

        LuaAPI.lua_atpanic(rawL, StaticLuaCallbacks.Panic);

#if !XLUA_GENERAL
        LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print);
        if (0 != LuaAPI.xlua_setglobal(rawL, "print"))
        {
            throw new Exception("call xlua_setglobal fail!");
        }
#endif

        //template engine lib register
        TemplateEngine.LuaTemplate.OpenLib(rawL);

        AddSearcher(StaticLuaCallbacks.LoadBuiltinLib, 2); // just after the preload searcher
        AddSearcher(StaticLuaCallbacks.LoadFromCustomLoaders, 3);
#if !XLUA_GENERAL
        AddSearcher(StaticLuaCallbacks.LoadFromResource, 4);
        AddSearcher(StaticLuaCallbacks.LoadFromStreamingAssetsPath, -1);
#endif
        DoString(init_xlua, "Init");
        init_xlua = null;

#if (!UNITY_SWITCH && !UNITY_WEBGL) || UNITY_EDITOR
        AddBuildin("socket.core", StaticLuaCallbacks.LoadSocketCore);
        AddBuildin("socket", StaticLuaCallbacks.LoadSocketCore);
#endif

        AddBuildin("CS", StaticLuaCallbacks.LoadCS);

        LuaAPI.lua_newtable(rawL); //metatable of indexs and newindexs functions
        LuaAPI.xlua_pushasciistring(rawL, "__index");
        LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.MetaFuncIndex);
        LuaAPI.lua_rawset(rawL, -3);

        LuaAPI.xlua_pushasciistring(rawL, Utils.LuaIndexsFieldName);
        LuaAPI.lua_newtable(rawL);
        LuaAPI.lua_pushvalue(rawL, -3);
        LuaAPI.lua_setmetatable(rawL, -2);
        LuaAPI.lua_rawset(rawL, LuaIndexes.LUA_REGISTRYINDEX);

        LuaAPI.xlua_pushasciistring(rawL, Utils.LuaNewIndexsFieldName);
        LuaAPI.lua_newtable(rawL);
        LuaAPI.lua_pushvalue(rawL, -3);
        LuaAPI.lua_setmetatable(rawL, -2);
        LuaAPI.lua_rawset(rawL, LuaIndexes.LUA_REGISTRYINDEX);

        LuaAPI.xlua_pushasciistring(rawL, Utils.LuaClassIndexsFieldName);
        LuaAPI.lua_newtable(rawL);
        LuaAPI.lua_pushvalue(rawL, -3);
        LuaAPI.lua_setmetatable(rawL, -2);
        LuaAPI.lua_rawset(rawL, LuaIndexes.LUA_REGISTRYINDEX);

        LuaAPI.xlua_pushasciistring(rawL, Utils.LuaClassNewIndexsFieldName);
        LuaAPI.lua_newtable(rawL);
        LuaAPI.lua_pushvalue(rawL, -3);
        LuaAPI.lua_setmetatable(rawL, -2);
        LuaAPI.lua_rawset(rawL, LuaIndexes.LUA_REGISTRYINDEX);

        LuaAPI.lua_pop(rawL, 1); // pop metatable of indexs and newindexs functions

        LuaAPI.xlua_pushasciistring(rawL, MAIN_SHREAD);
        LuaAPI.lua_pushthread(rawL);
        LuaAPI.lua_rawset(rawL, LuaIndexes.LUA_REGISTRYINDEX);

        LuaAPI.xlua_pushasciistring(rawL, CSHARP_NAMESPACE);
        if (0 != LuaAPI.xlua_getglobal(rawL, "CS"))
        {
            throw new Exception("get CS fail!");
        }
        LuaAPI.lua_rawset(rawL, LuaIndexes.LUA_REGISTRYINDEX);

#if !XLUA_GENERAL && (!UNITY_WSA || UNITY_EDITOR)
        translator.Alias(typeof(Type), "System.MonoType");
#endif

        if (0 != LuaAPI.xlua_getglobal(rawL, "_G"))
        {
            throw new Exception("get _G fail!");
        }
        translator.Get(rawL, -1, out _G);
        LuaAPI.lua_pop(rawL, 1);

        errorFuncRef = LuaAPI.get_error_func_ref(rawL);

        if (initers != null)
        {
            for (int i = 0; i < initers.Count; i++)
            {
                initers[i](this, translator);
            }
        }

        translator.CreateArrayMetatable(rawL);
        translator.CreateDelegateMetatable(rawL);
        translator.CreateEnumerablePairs(rawL);
    }

  • xLua的状态机构造函数比较庞大,将所有初始化操作全部集中在这里,接下来将逐一来分析其作用(以lua-5.3.5为例)。

初始化相关库

  • LuaAPI.luaopen_xlua
    • 初始化lua侧的基本库,包括package、coroutine、table、io、os、string、math、utf8、debug等。
  • LuaAPI.luaopen_i64lib
    • 初始化64位整型,包括int64和uint64。

ObjectTranslator

  • ObjectTranslator中进行了一些初始化操作,主要为:
    • 缓存程序集,包括当前运行的程序集、mscorlib、System、System.Core。
    • 创建ObjectCaster,初始化基本类型、LuaTable、LuaFunction的转化委托(返回object)。
    • 创建ObjectCheckers,初始化基本类型、LuaTable、LuaFunction的类型检查。
    • 创建MethodWrapsCache,建立运行时方法Wrap的缓存。
    • 创建StaticLuaCallbacks,将一些常用的方法转成LuaFunction:
      • LuaGC:lua对象gc回收。
      • ToString:调用CSharp的ToString。
      • EnumAnd:枚举与操作。
      • EnumOr:枚举或操作。
      • StaticCSFunction:lua方法委托。
      • FixCSFunction:lua修复的方法委托。
      • DelegateConstructor:委托构造方法。
    • 将一些特殊方法转成LuaFunction:
      • StaticLuaCallbacks.ImportType:注册CSharp的class到lua侧。
      • StaticLuaCallbacks.LoadAssembly:加载程序集到ObjectTranslator.assemblies。
      • StaticLuaCallbacks.Cast:将某个对象的metatable设置为CSharp的class。
    • 创建一个表放入 LUA_REGISTRYINDEX 表,id为 ObjectTranslator.cacheRef,再创建一个值弱表({__mode = “v”}),设置为此表的metatable。
    • 创建 LuaCSFunction 的 metatable,包含__gc方法,放入 LUA_REGISTRYINDEX 表。
    • 注册方法到lua侧:
      • xlua.import_type:CSharp侧的StaticLuaCallbacks.ImportType。
      • xlua.import_generic_type:CSharp侧的StaticLuaCallbacks.ImportGenericType。
      • xlua.cast:CSharp侧的StaticLuaCallbacks.Cast。
      • xlua.load_assembly:CSharp侧的StaticLuaCallbacks.LoadAssembly。
      • xlua.access:CSharp侧的StaticLuaCallbacks.XLuaAccess。
      • xlua.private_accessible:CSharp侧的StaticLuaCallbacks.XLuaPrivateAccessible。
      • xlua.metatable_operation:CSharp侧的StaticLuaCallbacks.XLuaMetatableOperation。
      • xlua.tofunction:CSharp侧的StaticLuaCallbacks.ToFunction。
      • xlua.get_generic_method:CSharp侧的StaticLuaCallbacks.GetGenericMethod。
      • xlua.release:CSharp侧的StaticLuaCallbacks.ReleaseCsObject。
    • 创建新表存入 LUA_REGISTRYINDEX 表,作为array的metatable,id为 common_array_meta。
    • 创建新表存入 LUA_REGISTRYINDEX 表,作为delegate的metatable,id为 common_delegate_meta。

初始化lua环境

  • 执行 Lua.Env.init_xlua 脚本,对lua环境进行初始化:
    • 创建 CS 表,设置metatable方法:
      • __index方法:查找对应的类型,如:CS.UnityEngine.GameObject,则调起CSharp侧的 StaticLuaCallbacks.ImportType,将CSharp侧的类注册到lua侧,并将此类型缓存到 CS 表中。
      • __newindex方法:输出报错信息,当前没有这个类型。
      • __call方法:泛型类对象获取,如:CS.System.Collections.Generic.List(CS.System.String),则会调起 StaticLuaCallbacks.ImportGenericType,获取指定类型的泛型type。由于每次调用都会触发 StaticLuaCallbacks.ImportGenericType 方法,而此方法中使用Type.MakeGenericType来构造泛型对象,需要创建Type数组,所以会有gc产生,可以缓存对象重复使用。
    • 注册 typeof 方法为CSharp的 Type.UnderlyingSystemType。
    • 注册 setfenv 和 getfenv 方法。
    • 注册 xlua.hotfix 方法,可以进行热修复。
    • 注册 xlua.setmetatable 方法为 xlua.metatable_operation(cs)。
    • 注册 xlua.setmetatable 方法为 xlua.metatable_operation(cs, mt)。
    • 注册 xlua.setclass 方法,可实现CSharp的class或者struct在lua侧改造。
    • 注册 base 方法,可实现调用CSharp父类对象方法(Hotfix.BASE_RPOXY_PERFIX标识)。

方法设置

  • 注册lua侧的print方法为CSharp侧的StaticLuaCallbacks.Print。
  • 设置lua侧的package.seachers方法,用于require调起:
    • package.seachers[2](即 searcher_Lua)设置为CSharp侧的StaticLuaCallbacks.LoadBuiltinLib。
    • package.seachers[3](即 searcher_C)设置为CSharp侧的StaticLuaCallbacks.LoadFromCustomLoaders。
    • package.seachers[4](即 searcher_Croot)设置为CSharp侧的StaticLuaCallbacks.LoadFromResource。
    • package.seachers[5](即 NULL)设置为CSharp侧的StaticLuaCallbacks.LoadFromStreamingAssetsPath。
  • 设置CSharp侧LoadBuiltinLib的查找字典LuaEnv.buildin_initer,当调用package.seachers[2]时,会从字典中中找:
    • socket.core:CSharp侧的StaticLuaCallbacks.LoadSocketCore。
    • socket:CSharp侧的StaticLuaCallbacks.LoadSocketCore。
    • CS:CSharp侧的StaticLuaCallbacks.LoadCS,获取 LUA_REGISTRYINDEX 表里的 xlua_csharp_namespace 表。
  • 设置index和newindex相关表:
    • 创建新表meta,设置 __index 方法为CSharp侧的 StaticLuaCallbacks.MetaFuncIndex,查找当前表里对应type的内容。
    • 创建新表,key值为 LuaIndexs,存入 LUA_REGISTRYINDEX 中,设置其metatable为meta表。
    • 创建新表,key值为 LuaNewIndexs,存入 LUA_REGISTRYINDEX 中,设置其metatable为meta表。
    • 创建新表,key值为 LuaClassIndexs,存入 LUA_REGISTRYINDEX 中,设置其metatable为meta表。
    • 创建新表,key值为 LuaClassNewIndexs,存入 LUA_REGISTRYINDEX 中,设置其metatable为meta表。
  • 设置主线程到 LUA_REGIXTRYINDEX 中,key值为 xlua_main_thread。
  • 将 _G 中的 CS 表,存入 LUA_REGISTRYINDEX 中,key值为 xlua_csharp_namespace。
  • 初始化前面创建的array的metatable,进行相关注册:
    • __gc 方法设为CSharp侧的 StaticLuaCallbacks.LuaGC。
    • __tostring 方法设为CSharp侧的 StaticLuaCallbacks.ToString。
    • Length 方法设为CSharp侧的 StaticLuaCallbacks.ArrayLength。
    • __index 方法设为CSharp侧的 StaticLuaCallbacks.ArrayIndexer。
    • __newindex 方法设为CSharp侧的 StaticLuaCallbacks.ArrayNewIndexer。
  • 初始化前面创建的delegate的metatable,进行相关注册:
    • __gc 方法设为CSharp侧的 StaticLuaCallbacks.LuaGC。
    • __tostring 方法设为CSharp侧的 StaticLuaCallbacks.ToString。
    • __call 方法设为CSharp侧的 StaticLuaCallbacks.DelegateCall。
    • __add 方法设为CSharp侧的 StaticLuaCallbacks.DelegateCombine。
    • __sub 方法设为CSharp侧的 StaticLuaCallbacks.DelegateRemove。
    • __index 方法设为CSharp侧的 StaticLuaCallbacks.ArrayIndexer。
    • __newindex 方法设为CSharp侧的 StaticLuaCallbacks.ArrayNewIndexer。
  • 创建迭代访问Pairs方法,存入到 LUA_REGISTRYINDEX 中,id为 enumerable_pairs_func。

结构

  • 经过初始化后,当前lua侧的结构大致如下:
    _G = {
        ...

        xlua.import_type = CSharp侧的StaticLuaCallbacks.ImportType
        xlua.import_generic_type = CSharp侧的StaticLuaCallbacks.ImportGenericType
        xlua.cast = CSharp侧的StaticLuaCallbacks.Cast
        xlua.load_assembly = CSharp侧的StaticLuaCallbacks.LoadAssembly
        xlua.access = CSharp侧的StaticLuaCallbacks.XLuaAccess
        xlua.private_accessible = CSharp侧的StaticLuaCallbacks.XLuaPrivateAccessible
        xlua.metatable_operation = CSharp侧的StaticLuaCallbacks.XLuaMetatableOperation
        xlua.tofunction = CSharp侧的StaticLuaCallbacks.ToFunction
        xlua.get_generic_method = CSharp侧的StaticLuaCallbacks.GetGenericMethod
        xlua.release = CSharp侧的StaticLuaCallbacks.ReleaseCsObject

        CS(所有CSharp的对应表) = LUA_REGISTRYINDEX 表中的 xlua_csharp_namespace 表

        typeof = CSharp的 Type.UnderlyingSystemType
        setfenv = CSharp侧 LuaEnv.init_xlua 中的 setfenv
        getfenv = CSharp侧 LuaEnv.init_xlua 中的 getfenv
        xlua.hotfix = CSharp侧 LuaEnv.init_xlua 中的 xlua.hotfix
        xlua.setmetatable = xlua.metatable_operation(cs)
        xlua.setmetatable = xlua.metatable_operation(cs, mt)
        xlua.setclass = CSharp侧 LuaEnv.init_xlua 中的 xlua.setclass
        base = CSharp侧 LuaEnv.init_xlua 中的 base

        print = CSharp侧的 StaticLuaCallbacks.Print
    }

    -10000(即 LUA_REGISTRYINDEX) = {
        ...

        ref_cacheRef(分配的id,在CSharp侧为 ObjectTranslator.cacheRef)
            table = {

            },
            metatable = {
                __mode = v
            }

        ref_LuaCSFunction(分配的id,在CSharp侧存到 ObjectTranslator.typeIdMap) = {
            __gc = CSharp侧的 StaticLuaCallbacks.LuaGC,
            &tag = 1
        }

        ref_common_array_meta = {
            __gc = CSharp侧的 StaticLuaCallbacks.LuaGC
            __tostring = CSharp侧的 StaticLuaCallbacks.ToString
            Length = CSharp侧的 StaticLuaCallbacks.ArrayLength
            __index = CSharp侧的 StaticLuaCallbacks.ArrayIndexer
            __newindex = CSharp侧的 StaticLuaCallbacks.ArrayNewIndexer           
        }

        ref_common_delegate_meta = {
            __gc = CSharp侧的 StaticLuaCallbacks.LuaGC
            __tostring = CSharp侧的 StaticLuaCallbacks.ToString
            __call = CSharp侧的 StaticLuaCallbacks.DelegateCall

            __add = CSharp侧的 StaticLuaCallbacks.DelegateCombine
            __sub = CSharp侧的 StaticLuaCallbacks.DelegateRemove
            __index = CSharp侧的 StaticLuaCallbacks.ArrayIndexer
            __newindex = CSharp侧的 StaticLuaCallbacks.ArrayNewIndexer
        }

        ref_enumerable_pairs_func = CSharp侧 ObjectTranslator.CreateEnumerablePairs方法中的lua代码方法

        LuaIndexs :
            table = {},
            metatable = meta表 {
                __index = StaticLuaCallbacks.MetaFuncIndex
            }

        LuaNewIndexs :
            table = {},
            metatable = meta表 

        LuaClassIndexs :
            table = {},
            metatable = meta表 
        
        LuaClassNewIndexs :
            table = {},
            metatable = meta表 

        xlua_csharp_namespace :
            table = {

            },
            metatable = {
                __index = CSharp侧 LuaEnv.init_xlua 中的 __index(key),
                __newindex = CSharp侧 LuaEnv.init_xlua 中的 __newindex(),
                __call = CSharp侧 LuaEnv.init_xlua 中的 __call(...),
            }
    }

访问对象

Lua访问CSharp对象

  • 当我们在Lua侧,要获取CSharp对象时,以GameObject为例,当我们查找某个GameObject时,在lua侧会使用
    -- Test.lua

    local go = CS.UnityEngine.GameObject.Find("TestGameObject");
  • 调用CSharp侧的GameObject.Find来获取对象,这里有几个步骤:
    • 获取 _G.CS 表,查找 CS.UnityEngine 对象。
    • CS 表中不存在 UnityEngine,则触发CSharp侧的 StaticLuaCallbacks.ImportType 方法,查找对应类。
    • 由于UnityEngine不为class,所以设置 CS.UnityEngine 的 table 为 { “.fqn” = “UnityEngine” },metatable为CS表的metatable。
    • 查找GameObject对象,同样触发CSharp侧的 StaticLuaCallbacks.ImportType方法,查找UnityEngine.GameObject,并注册到lua侧中。
    • 查找Find方法,传入参数并执行,触发CSharp侧的 GameObject.Find 方法,找到对应的GameObject。
    • 调用ObjectTranslator.Push,创建index,返回lua侧。
    • lua侧使用index创建userdata,持有GameObject对象。
  • 在整个过程中,有两个步骤是比较重要的:
    • 把CSharp的类注册到Lua侧。
    • 将CSharp的对象绑定到Lua侧。

1. 注册CSharp类

  • 注册CSharp类的主要方法为 ObjectTranslator.GetTypeId ,其主要流程为:
    • 查找 ObjectTranslator.typeIdMap 中是否有对应类型,有则直接返回。
    • 如果是 Array 类型,则返回 common_array_meta 。
    • 如果是 MulticastDelegate 类型,调用 TryDelayWrapLoader 方法创建类型的metatable。
    • 如果 LUA_REGISTRYINDEX 表中没有此类型的metatable,则调用 TryDelayWrapLoader 方法创建(key值为type.FullName)。
    • 如果是 Enum 类型,则在metatable中设置方法:
      • __band 设置为 StaticLuaCallbacks.EnumAnd。
      • __bor 设置为 StaticLuaCallbacks.EnumOr。
    • 如果是 IEnumerable 类型,则设置 __pairs 为 ref_enumerable_pairs_func的方法。
    • 将此metatable存到 LUA_REGISTRYINDEX 表中,id为lua侧创建的引用 type_id(此table会存到多个地方)。
    • 如果为值类型,将 type 存入 typeMap中,key值为 type_id。
    • 将 type_id 存入 typeIdMap 中。
  • ObjectTranslator.TryDelayWrapLoader 为创建metatable的方法,主要有两种方式:
    • 预先生成CSharp侧的Wrap脚本,将类型和 XXXWrap.__Register 存入ObjectTranslator.delayWrap中,通过调用 __Register 方法,进行注册。
    • 通过反射进行注册。

1.1 Wrap注册

  • Wrap方式主要是预先对使用 [LuaCallCSharp] 特性的类型生成Wrap文件,然后通过 __Register 方法初始化,主要有以下方法:
    • Utils.BeginObjectRegister:注册类实例对象相关信息。
      • 将type的对应metatable压入栈上。
      • 如果传入type为空,或者type不为ObjectTranslator.custom_push_funcs(即[LuaCallCSharp]的Enum、[GCOptimize]),且不为decimal,则设置 __gc 方法为CSharp侧的 StaticLuaCallbacks.LuaGC。
      • 设置 __tostring 方法为CSharp侧的 StaticLuaCallbacks.ToString。
      • 如果方法数量(method_count)不为0,则创建method_count大小的table到栈上,否则压入nil。
      • 如果get属性数量(getter_count)不为0,则创建getter_count大小的table到栈上,否则压入nil。
      • 如果set属性数量(setter_count)不为0,则创建setter_count大小的table到栈上,否则压入nil。
      • 当前栈顶的情况为:
        • -1 : set的table
        • -2 : get的table
        • -3 : method的table
        • -4 : type的metatable
    • Utils.RegisterFunc:注册方法。
      • 传入注册的方法的类型idx,和入栈顺序对应上:
        • Utils.OBJ_META_IDX:-4,metatable。
        • Utils.METHOD_IDX、Utils.:-3,普通方法表。
        • Utils.GETTER_IDX:-2,get方法表。
        • Utils.SETTER_IDX:-1,set方法表。
        • Utils.CLS_IDX:-4,class的table。
        • Utils.CLS_META_IDX:-3,class的metatable。
        • Utils.CLS_GETTER_IDX:-2,class的get方法表。
        • Utils.CLS_SETTER_IDX:-1,class的set方法表。
      • 获取方法在栈上的位置(栈顶位置 + idx + 1)。
      • 将方法名和方法设置到栈上对应位置的table中。
    • Utils.EndObjectRegister:完成实例对象注册。
      • 设置metatable的__index:
        • 将"__index"字符串入栈。
        • 将method的table入栈。
        • 将get的table入栈。
        • 将CSharp侧的csIndexer方法入栈。
        • 将type的基类base_type入栈。
        • 将 LUA_REGISTRYINDEX 表中的 LuaIndexs 表入栈。
        • 将CSharp侧的arrayIndexer方法入栈。
        • 调用lua侧的 gen_obj_indexer 方法,将nil入栈,然后创建一个包括7个值的闭包,方法为 obj_indexer,将对应值出栈,闭包压入栈上。
          • 闭包的值对应为 [1]: methods, [2]:getters, [3]:csindexer, [4]:base, [5]:indexfuncs, [6]:arrayindexer, [7]:baseindex
          • 传入的参数对应为 [1]: obj, [2]: key
        • 将闭包存入 LuaIndexs 表,key为type的userdata。
        • 设置type的metatable方法,key值为__index,value为闭包。
      • 设置metatable的__newindex:
        • 将"__newindex"字符串入栈。
        • 将set的table入栈。
        • 将CSharp侧的csNewIndexer方法入栈。
        • 将type的基类base_type入栈。
        • 将 LUA_REGISTRYINDEX 表中的 LuaNewIndexs 表入栈。
        • 将CSharp侧的arrayNewIndexer方法入栈。
        • 调用lua侧的 gen_obj_newindexer 方法,将nil入栈,然后创建一个包括6个值的闭包,方法为 obj_newindexer,将对应值出栈,闭包压入栈上。
          • 闭包的值对应为 [1]:setters, [2]:csnewindexer, [3]:base, [4]:newindexfuncs, [5]:arrayindexer, [6]:basenewindex
          • 传入的参数对应为 [1]: obj, [2]: key, [3]: value
        • 将闭包存入 LuaNewIndexs 表,key为type的userdata。
        • 设置type的metatable方法,key值为__newindex,value为闭包。
      • 将set、get、method的table和type的metatable出栈,恢复栈顶信息。
    • Utils.BeginClassRegister:注册类相关信息,主要为静态对象、常量、枚举。
      • 创建一个新表 cls_table,大小为类的静态对象和方法、常量、枚举的数量总数量 + 1。
      • 设置type的userdata到cls_table中,key值为 UnderlyingSystemType(每个class都会注册这个key-value,所以cls_table的总大小+1)。
      • 调用CSharp的 Util.SetCSTable 方法,注册type对应lua侧表:
        • 将类型按 “.” 分割,创建对应table,存入 LUA_REGISTRYINDEX 表中的 xlua_csharp_namespace 表,如 UnityEngine.GameObject ,则创建key值 UnityEngine 的表存入 xlua_csharp_namespace 表,再将 cls_table 存入 UnityEngine 表中,key值为 GameObject。
        • 将 cls_table 表存到 xlua_csharp_namespace 表,key值为type的userdata。
      • 创建新表 meta_table,设置__call方法为CSharp传入的creator方法。
      • 创建get的table入栈。
      • 创建set的table入栈。
      • 设置为 cls_table 的metatable为 meta_table。
    • Utils.RegisterObject:注册对象,通常为Enum值、常量等。
      • 根据传入的类型idx,将对象设置到栈上对应table中,key值为传入的name。
    • Utils.EndClassRegister
      • 设置cls_table的index:
        • 将"__index"入栈。
        • 将get的table入栈。
        • 将cls的table入栈。
        • 将type的基类入栈。
        • 将 LUA_REGISTRYINDEX 表中的 LuaClassIndexs 表入栈。
        • 调用lua侧的 gen_cls_indexer 方法,将nil入栈,然后创建一个包括5个值的闭包,方法为 cls_indexer,将对应值出栈,闭包压入栈上。
          • 闭包的值对应为 [1]:getters, [2]:feilds, [3]:base, [4]:indexfuncs, [5]:baseindex
          • 传入的参数对应为 [1]: obj, [2]: key
        • 将闭包存入 LuaClassIndexs 表,key值为type的userdata。
        • 设置cls_table的metatable方法,key值为__index,value为闭包。
      • 设置cls的newindex:
        • 将"__newindex"入栈。
        • 将set的table入栈。
        • 将type的基类入栈。
        • 将 LUA_REGISTRYINDEX 表中的 LuaClassNewIndexs 表入栈。
        • 调用lua侧的 gen_cls_newindexer 方法,将nil入栈,然后创建一个包括4个值的闭包,方法为 cls_newindexer,将对应值出栈,闭包压入栈上。
          • 闭包的值对应为 [1]:setters, [2]:base, [3]:indexfuncs, [4]:baseindex
          • 传入的参数对应为 [1]: obj, [2]: key, [3]: value
        • 将闭包存入 LuaClassNewIndexs 表,key值为type的userdata。
        • 设置cls_table的metatable方法,key值为__newindex,value为闭包。

1.2 反射注册

  • 反射注册(Utils.ReflectionWrap)的流程和Wrap流程基本一致,只是没有预先生成代码,主要流程为:
    • 创建type的metatable,为obj_meta,存到 LUA_REGISTRYINDEX 表,key值为type.FullName。
    • 设置 &tag 为 1。
    • 创建cls_meta、obj_field、obj_getter、obj_setter、cls_field表。
    • 调用CSharp侧的 Util.SetCSTable 方法,注册type对应lua侧表。
    • 创建cls_getter、cls_setter表。
    • 调用 makeReflectionWrap 方法,进行Wrap注册:
      • 定义flag为 BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | access,access根据需求设置,可为 BindingFlags.Public 或 BindingFlags.NonPublic 或两者同时设置。
      • 获取当前type符合flag类型的所有变量fields。
      • 获取当前type符合类型(flag | BindingFlags.Public | BindingFlags.NonPublic)的所有事件all_events。
      • 遍历fields对变量进行处理:
        • 静态变量,如果变量名以 “__Hotfix” 或 “_c__Hotfix” 开头,且为委托,则不处理。
        • 如果 all_events 中有这个变量名,则在 fieldName 前加 “&"。
        • 如果变量为静态,且有默认值并不能被修改,则存入 cls_field 中,key为 fieldName,value为变量的值。
        • 如果没有默认值或者能被修改,则使用 translator.PushFixCSFunction 方法,将get和set的方法存入 ObjectTranslator.fix_cs_functions中,将索引id作为 StaticLuaCallbacks.FixCSFunction 的参数,创建闭包,key值为 fieldName,存入表中。
          • 静态变量:存入cls_getter、cls_setter。
          • 成员变量:存入obj_getter、obj_setter。
      • 获取当前type符合flag类型的所有events。
      • 遍历events对事件进行处理:
        • 获取事件对应的Wrap方法
          • MethodWrapsCache.methodsCache[type].methodsOfType[eventName]中存在,则直接返回。
          • 如果没有缓存,则创建Wrap方法,Wrap会创建一个委托,绑定栈上 start_idx + 2 的方法,并根据 start_idx + 1 的值("+“或”-"),执行事件添加或移除。
        • 将event存入表中,key值为 eventInfo.Name,value为Wrap方法。
          • 静态事件:存入cls_field。
          • 成员事件:存入obj_field。
      • 获取当前type符合flag类型的所有有参属性。
      • 设置out的item_getter、item_setter为有参属性的get、set。
      • 获取当前type符合flag类型的所有方法methods。
      • 创建方法的容器 Dictionary<MethodKey, List<MemberInfo» pending_methods。
      • 遍历methods对方法进行处理:
        • 创建方法的MethodKey:new MethodKey { Name = method_name, IsStatic = method.IsStatic }。
        • 如果 pending_methods 中是否存在此key,则将方法加入 List<MemberInfo> 中。
        • 如果方法为有参属性(get为一个参数,set为两个参数),第一个参数类型不是继承string,则不处理此方法。
        • 如果方法为特殊名字方法(MethodBase.IsSpecialName),且以"add_"、“remove_“开头的,则不处理此方法。
        • 如果方法为特殊名字方法(MethodBase.IsSpecialName),且以"op_“开头的,如果 InternalGlobals.supportOp 中包含此方法,则加入 List<MemberInfo> 中,否则不处理。InternalGlobals.supportOp 包括:
          • { “op_Addition”, “__add” }
          • { “op_Subtraction”, “__sub” }
          • { “op_Multiply”, “__mul” }
          • { “op_Division”, “__div” }
          • { “op_Equality”, “__eq” }
          • { “op_UnaryNegation”, “__unm” }
          • { “op_LessThan”, “__lt” }
          • { “op_LessThanOrEqual”, “__le” }
          • { “op_Modulus”, “__mod” }
          • { “op_BitwiseAnd”, “__band” }
          • { “op_BitwiseOr”, “__bor” }
          • { “op_ExclusiveOr”, “__bxor” }
          • { “op_OnesComplement”, “__bnot” }
          • { “op_LeftShift”, “__shl” }
          • { “op_RightShift”, “__shr” }
        • 如果方法为特殊名字方法(MethodBase.IsSpecialName),且以"get_"、“set__“开头,则使用 translator.PushFixCSFunction 方法,将get、set方法存入 ObjectTranslator.fix_cs_functions中,将索引id作为 StaticLuaCallbacks.FixCSFunction 的参数,创建闭包,key值为 methodName,静态方法存入cls_getter、cls_setter,其他存入obj_getter、obj_setter。
        • 如果方法为构造函数,则不处理。
        • 其他情况,则将方法加入 List<MemberInfo> 中。
      • 遍历extension_methods对扩展方法进行处理:
        • 创建方法的MethodKey:new MethodKey { Name = method_name, IsStatic = method.IsStatic }。
        • 如果 pending_methods 中是否存在此key,则将方法加入 List<MemberInfo> 中。
      • 遍历pending_methods(Dictionary<MethodKey, List<MemberInfo»)中的所有对象:
        • 使用 MethodWrapsCache._GenMethodWrap 方法,创建 MethodWrap 对象。
          • 遍历 List<MemberInfo> 的方法,将方法包装成 OverloadMethodWrap 对象。
          • 对 OverloadMethodWrap 对象进行初始化,加入 overloads(List<OverloadMethodWrap>) 列表。
          • 将 overloads 列表包装成 MethodWrap 对象,即一个 methodName 对应一个 MethodWrap 对象。
        • 使用 translator.PushFixCSFunction 方法,将方法名的 MethodWrap 对象存入 ObjectTranslator.fix_cs_functions中,将索引id作为 StaticLuaCallbacks.FixCSFunction 的参数,创建闭包,key值为 MethodKey.Name,存入对应表中。即同一个方法名中会有各个参数数量的对应方法,具体调用哪个方法根据栈上压入的参数个数决定。
          • 如果 MethodKey.Name 以”__op"开头,则存入 obj_meta 表中。
          • 其他对象,如果 MethodKey.IsStatic 为静态,则存入 cls_field 表,否则存入 obj_field 表。
    • 初始化 obj 的 metatable:
      • 将CSharp侧的 StaticLuaCallbacks.LuaGC 方法存入 obj_meta 表,key值为 __gc。
      • 将CSharp侧的 StaticLuaCallbacks.ToString 方法存入 obj_meta 表,key值为 __tostring。
      • 设置 __index 方法:
        • 将”__index"入栈。
        • 将 obj_field 表入栈。
        • 将 obj_getter 表入栈。
        • 使用 translator.PushFixCSFunction 方法,将 makeReflectionWrap 返回的 item_getter 方法存入 ObjectTranslator.fix_cs_functions中,将索引id作为 StaticLuaCallbacks.FixCSFunction 的参数,创建闭包并入栈。
        • 将当前type的基类入栈(translator.PushAny)。
        • 将 LuaIndexs 表入栈。
        • 将 nil 入栈。
        • 调用lua侧的 gen_obj_indexer 方法,将nil入栈,然后创建一个包括7个值的闭包,方法为 obj_indexer,将对应值出栈,闭包压入栈上。
        • 将闭包存入 LuaIndexs 表,key值为type的userdata。
        • 将闭包存入 obj_meta 表,key值为 __index。
      • 设置 __newindex 方法:
        • 将”__newindex"入栈。
        • 将 obj_setter 表入栈。
        • 使用 translator.PushFixCSFunction 方法,将 makeReflectionWrap 返回的 item_setter 方法存入 ObjectTranslator.fix_cs_functions中,将索引id作为 StaticLuaCallbacks.FixCSFunction 的参数,创建闭包并入栈。
        • 将当前type的基类入栈(translator.Push)。
        • 将 LuaNewIndexs 表入栈。
        • 将 nil 入栈。
        • 调用lua侧的 gen_obj_newindexer 方法,将nil入栈,然后创建一个包括6个值的闭包,方法为 obj_newindexer,将对应值出栈,闭包压入栈上。
        • 将闭包存入 LuaNewIndexs 表,key值为type的userdata。
        • 将闭包存入 obj_meta 表,key值为 __newindex。
    • 将type存入cls_field表,key值为 UnderlyingSystemType。
    • 如果type为枚举,则使用 translator.PushFixCSFunction 方法,为 genEnumCastFrom 方法创建闭包,存入cls_field表,key值为 __CastFrom。
    • 初始化 cls 的 metatable
      • 设置 __index 方法:
        • 将”__index"入栈。
        • 将 cls_getter 表入栈。
        • 将 cls_field 表入栈。
        • 将当前type的基类入栈(translator.Push)。
        • 将 LuaClassIndexs 表入栈。
        • 调用lua侧的 gen_cls_indexer 方法,将nil入栈,然后创建一个包括5个值的闭包,方法为 cls_indexer,将对应值出栈,闭包压入栈上。
        • 将闭包存入 LuaClassIndexs 表,key值为type的userdata。
        • 将闭包存入 cls_meta 表,key值为 __index。
      • 设置 __newindex 方法:
        • 将”__newindex"入栈。
        • 将 cls_setter 表入栈。
        • 将当前type的基类入栈(translator.Push)。
        • 将 LuaClassNewIndexs 表入栈。
        • 调用lua侧的 gen_cls_newindexer 方法,将nil入栈,然后创建一个包括4个值的闭包,方法为 cls_newindexer,将对应值出栈,闭包压入栈上。
        • 将闭包存入 LuaClassNewIndexs 表,key值为type的userdata。
        • 将闭包存入 cls_meta 表,key值为 __newindex。
      • 将type的构造函数存入cls_meta表中,key值为 __call。
    • 将 cls_meta 表设置为 cls_field 表的metatable。
    • 遍历嵌套在type中的类型(GetNestedTypes),如果为需要传入类型构建的泛型类型(如 List<>),则不处理,其余则进行类型注册。

2. 结构

  • 注册后,lua侧的结构大致如下(以 GameObject 为例):
    _G = {
        ...

        CS(所有CSharp的对应表): LUA_REGISTRYINDEX 表中的 xlua_csharp_namespace 表
            
    }

    -10000(即 LUA_REGISTRYINDEX) = {
        ...

        LuaIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的obj的index闭包
            },
            metatable = {
                __index = StaticLuaCallbacks.MetaFuncIndex
            }

        LuaNewIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的obj的newindex闭包
            },
            metatable = meta表 

        LuaClassIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的cls的index闭包
            },
            metatable = meta表 
        
        LuaClassNewIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的cls的newindex闭包
            },
            metatable = meta表 

        xlua_csharp_namespace 表(即 _G.CS 表) = {
            table = {
                UnityEngine = {
                      GameObject = ref_GameObject 表
                        
                  }
            },
            metatable = {
                __index = CSharp侧 LuaEnv.init_xlua 中的 __index(key),
                __newindex = CSharp侧 LuaEnv.init_xlua 中的 __newindex(),
                __call = CSharp侧 LuaEnv.init_xlua 中的 __call(...),
            }

            ref_GameObject(分配的id,即 GameObject 的 userdata) = {
                table = {
                            UnderlyingSystemType = ref_GameObject(GameObject的userdata)

                            FindWithTag = CSharp侧的 GameObject.FindWithTag(Wrap为 UnityEngineGameObjectWrap._m_FindWithTag_xlua_st_,反射为 MethodWrap.Call)
                            Find = CSharp侧的 GameObject.Find(Wrap为 UnityEngineGameObjectWrap._m_Find_xlua_st_,反射为 MethodWrap.Call)
                            ...
                        }
                metatable(即 cls_meta 表) = {
                    __call = GameObject的实例化(Wrap为 UnityEngineGameObjectWrap.__CreateInstance,反射为构造函数)
                    __index = cls的index闭包(参数为 [1]: obj, [2]: key )
                        {
                            [1]: getters(即 cls_getter 表)
                                {
                                    get的table
                                }
                            [2]: feilds(即 cls_field 表)
                                {
                                    CS.UnityEngine.GameObject 表
                                }
                            [3]: base
                                {
                                    type的基类
                                }
                            [4]: indexfuncs
                                {
                                    LUA_REGISTRYINDEX 表中的 LuaClassIndexs 表
                                }
                            [5]: baseindex
                                {
                                    nil
                                }
                        }
                    __newindex = cls的newindex闭包(参数为 [1]: obj, [2]: key, [3]: value )
                        {
                            [1]: setters(即 cls_setter 表)
                                {
                                    set的table
                                }
                            [2]: base
                                {
                                    type的基类
                                }
                            [3]: indexfuncs
                                {
                                    LUA_REGISTRYINDEX 表中的 LuaClassNewIndexs 表
                                }
                            [4]: baseindex
                                {
                                    nil
                                }
                        }
                }
            }
        }

        ref_GameObject(分配的id,对应CSharp侧的UnityEngine.GameObject) = UnityEngine.GameObject 表

        UnityEngine.GameObject (CSharp侧UnityEngine.GameObject的type.FullName,由 luaL_newmetatable 创建):
            table = {},
            metatable(即 obj_meta 表) = {
                &tag = 1,
                
                __name = "UnityEngine.GameObject",
                __gc = CSharp侧的 StaticLuaCallbacks.LuaGC,
                __tostring = CSharp侧的 StaticLuaCallbacks.ToString,
                __index = obj的index闭包(参数为 [1]: obj, [2]: key )
                    {
                        [1]: methods(即 obj_field 表)
                            {
                                GetComponent = CSharp侧的 GameObject.GetComponent(Wrap为 UnityEngineGameObjectWrap._m_GetComponent,反射为 MethodWrap.Call)
                                SetActive = CSharp侧的 GameObject.SetActive(Wrap为 UnityEngineGameObjectWrap._m_SetActive,反射为 MethodWrap.Call)
                                ...
                            }
                        [2]: getters(即 obj_getter 表)
                            {
                                transform = CSharp侧的 GameObject.transform(Wrap为 UnityEngineGameObjectWrap._g_get_transform,反射为 MethodWrap.Call)
                                layer = CSharp侧的 GameObject.layer(Wrap为 UnityEngineGameObjectWrap._g_get_layer,反射为 MethodWrap.Call)
                                ...
                            }
                        [3]: csindexer
                            {
                                CSharp侧的csIndexer方法
                            }
                        [4]: base
                            {
                                type的基类base_type
                            }
                        [5]: indexfuncs
                            {
                                LUA_REGISTRYINDEX 表中的 LuaIndexs 表
                            }
                        [6]: arrayindexer
                            {
                                CSharp侧的arrayIndexer方法
                            }
                        [7]: baseindex
                            {
                                nil
                            }
                    }
                __newindex = obj的newindex闭包(参数为 [1]: obj, [2]: key, [3]: value )
                    {
                        [1]: setters(即 obj_setter 表)
                            {
                                transform = CSharp侧的 GameObject.transform(Wrap为 UnityEngineGameObjectWrap._g_set_transform,反射为 MethodWrap.Call)
                                layer = CSharp侧的 GameObject.layer(Wrap为 UnityEngineGameObjectWrap._g_set_layer,反射为 MethodWrap.Call)
                                ...
                            }
                        [2]: csnewindexer
                            {
                                CSharp侧的csNewIndexer方法
                            }
                        [3]: base
                            {
                                type的基类base_type
                            }
                        [4]: newindexfuncs
                        {
                            LUA_REGISTRYINDEX 表中的 LuaNewIndexs 表
                        }
                        [5]: arrayindexer
                        {
                            CSharp侧的arrayNewIndexer方法
                        }
                        [6]: basenewindex
                        {
                            nil
                        }
                    }
            }
    }

3. 绑定CSharp对象

  • CSharp侧的对象,传到Lua侧主要方法为 ObjectTranslator.Push ,主要流程为:
    • 非值类型对象和枚举对象,从字典中获取对象对应的id,调用lua侧的 xlua_tryget_cachedud 方法,检查 LUA_REGISTRYINDEX 表中的 ref_cacheRef 表中是否有此id,如果有则不再处理。
      • 非值类型对象:ObjectTranslator.reverseMap 字典。
      • 枚举对象:ObjectTranslator.enumMap 字典。
    • 调用 ObjectTranslator.getTypeId 方法,获取type对应的type_id。
    • 如果一个type的定义含本身静态readonly实例时,getTypeId会push一个实例,检查 LUA_REGISTRYINDEX 表中的 ref_cacheRef 表中是否有此id,如果有则不再处理。
    • 调用 ObjectTranslator.addObject 方法,将对象存入 ObjectTranslator.objects 中,返回对应的 index,并根据类型存入字典:
      • 非值类型对象:存入 ObjectTranslator.reverseMap 字典记录。
      • 枚举对象:存入 ObjectTranslator.enumMap 字典记录。
    • 调用lua侧的 xlua_pushcsobj 方法,在lua侧创建对应的userdata,存入 LUA_REGISTRYINDEX 表中的 ref_cacheRef 表中,设置userdata的metatable为 LUA_REGISTRYINDEX 表中的 type_id 对应的表(如 ref_GameObject)。
  • 示例代码大致如下:
    -- Test.lua

    local go = CS.UnityEngine.GameObject.Find("TestGameObject");
    // UnityEngineGameObjectWrap.cs
    ...
    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int _m_Find_xlua_st_(RealStatePtr L)
    {
        try {
        
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);

            {
                string _name = LuaAPI.lua_tostring(L, 1);
                
                    var gen_ret = UnityEngine.GameObject.Find( _name );
                    translator.Push(L, gen_ret);
                
                
                
                return 1;
            }
            
        } 
        catch(System.Exception gen_e) {
            return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
        }
        
    }
    // ObjectTranslator.cs

    ...
    public void Push(RealStatePtr L, object o)
    {
        if (o == null)
        {
            LuaAPI.lua_pushnil(L);
            return;
        }
        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;
            }
        }
        index = addObject(o, is_valuetype, is_enum);
        LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
    }
  • 可以看到,如果需要缓存的对象,会调用lua侧的 xlua_tryget_cachedud 方法,检查 LUA_REGISTRYINDEX 表中的 ref_cacheRef 表中是是否有此对象,如果返回值为1,表示表中有此对象,则不需要再次进行绑定。
  • Lua侧此时结构大致如下所示:
    _G = {
        ...

        CS(所有CSharp的对应表): LUA_REGISTRYINDEX 表中的 xlua_csharp_namespace 表


        Test = {
            go = userdata_GameObject(从 LUA_REGISTRYINDEX 表中的 ref_cacheRef 表中获取)
        }            
    }

    -10000(即 LUA_REGISTRYINDEX) = {
        ...

        ref_cacheRef(分配的id,在CSharp侧为 ObjectTranslator.cacheRef)
            table = {
                index_GameObject(CSharp侧的一个UnityEngine.GameObject,index为ObjectTranslator分配)= userdata_GameObject {
					
                    (头部 Udata*){
                      ...
                      tt = LUA_TUSERDATA
                      metatable = LUA_REGISTRYINDEX.ref_GameObject
                    }
                    (用户自定义数据 user domain*){
                      index_GameObject
                    }
                  }
                
                ...
            },
            metatable = {
                __mode = v(值弱表)
            }

        LuaIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的obj的index闭包
            },
            metatable = {
                __index = StaticLuaCallbacks.MetaFuncIndex
            }

        LuaNewIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的obj的newindex闭包
            },
            metatable = meta表 

        LuaClassIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的cls的index闭包
            },
            metatable = meta表 
        
        LuaClassNewIndexs :
            table = {
                ref_GameObject = UnityEngine.GameObject的cls的newindex闭包
            },
            metatable = meta表 

        xlua_csharp_namespace 表(即 _G.CS 表) = {
            table = {
                UnityEngine = {
                      GameObject = ref_GameObject 表
                        
                  }
            },
            metatable = {
                __index = CSharp侧 LuaEnv.init_xlua 中的 __index(key),
                __newindex = CSharp侧 LuaEnv.init_xlua 中的 __newindex(),
                __call = CSharp侧 LuaEnv.init_xlua 中的 __call(...),
            }

            ref_GameObject(分配的id,即 GameObject 的 userdata) = {
                table = {
                            UnderlyingSystemType = ref_GameObject(GameObject的userdata)

                            FindWithTag = CSharp侧的 GameObject.FindWithTag(Wrap为 UnityEngineGameObjectWrap._m_FindWithTag_xlua_st_,反射为 MethodWrap.Call)
                            Find = CSharp侧的 GameObject.Find(Wrap为 UnityEngineGameObjectWrap._m_Find_xlua_st_,反射为 MethodWrap.Call)
                            ...
                        }
                metatable = {
                    __call = GameObject的实例化(Wrap为 UnityEngineGameObjectWrap.__CreateInstance,反射为构造函数)
                    __index = cls的index闭包(参数为 [1]: obj, [2]: key )
                        {
                            [1]:getters
                                {
                                    get的table
                                }
                            [2]:feilds
                                {
                                    CS.UnityEngine.GameObject 表
                                }
                            [3]:base
                                {
                                    type的基类
                                }
                            [4]:indexfuncs
                                {
                                    LUA_REGISTRYINDEX 表中的 LuaClassIndexs 表
                                }
                            [5]:baseindex
                                {
                                    nil
                                }
                        }
                    __newindex = cls的newindex闭包(参数为 [1]: obj, [2]: key, [3]: value )
                        {
                            [1]:setters
                                {
                                    set的table
                                }
                            [2]:base
                                {
                                    type的基类
                                }
                            [3]:indexfuncs
                                {
                                    LUA_REGISTRYINDEX 表中的 LuaClassNewIndexs 表
                                }
                            [4]:baseindex
                                {
                                    nil
                                }
                        }
                }
            }
        }

        ref_GameObject(分配的id,对应CSharp侧的UnityEngine.GameObject) = UnityEngine.GameObject 表

        UnityEngine.GameObject (CSharp侧UnityEngine.GameObject的type.FullName,由 luaL_newmetatable 创建):
            table = {},
            metatable = {
                &tag = 1,
                
                __name = "UnityEngine.GameObject",
                __gc = CSharp侧的 StaticLuaCallbacks.LuaGC,
                __tostring = CSharp侧的 StaticLuaCallbacks.ToString,
                __index = obj的index闭包(参数为 [1]: obj, [2]: key )
                    {
                        [1]: methods
                            {
                                GetComponent = CSharp侧的 GameObject.GetComponent(Wrap为 UnityEngineGameObjectWrap._m_GetComponent,反射为 MethodWrap.Call)
                                SetActive = CSharp侧的 GameObject.SetActive(Wrap为 UnityEngineGameObjectWrap._m_SetActive,反射为 MethodWrap.Call)
                                ...
                            }
                        [2]: getters
                            {
                                transform = CSharp侧的 GameObject.transform(Wrap为 UnityEngineGameObjectWrap._g_get_transform,反射为 MethodWrap.Call)
                                layer = CSharp侧的 GameObject.layer(Wrap为 UnityEngineGameObjectWrap._g_get_layer,反射为 MethodWrap.Call)
                                ...
                            }
                        [3]: csindexer
                            {
                                CSharp侧的csIndexer方法
                            }
                        [4]: base
                            {
                                type的基类base_type
                            }
                        [5]: indexfuncs
                            {
                                LUA_REGISTRYINDEX 表中的 LuaIndexs 表
                            }
                        [6]: arrayindexer
                            {
                                CSharp侧的arrayIndexer方法
                            }
                        [7]: baseindex
                            {
                                nil
                            }
                    }
                __newindex = obj的newindex闭包(参数为 [1]: obj, [2]: key, [3]: value )
                    {
                        [1]: setters
                            {
                                transform = CSharp侧的 GameObject.transform(Wrap为 UnityEngineGameObjectWrap._g_set_transform,反射为 MethodWrap.Call)
                                layer = CSharp侧的 GameObject.layer(Wrap为 UnityEngineGameObjectWrap._g_set_layer,反射为 MethodWrap.Call)
                                ...
                            }
                        [2]: csnewindexer
                            {
                                CSharp侧的csNewIndexer方法
                            }
                        [3]: base
                            {
                                type的基类base_type
                            }
                        [4]: newindexfuncs
                        {
                            LUA_REGISTRYINDEX 表中的 LuaNewIndexs 表
                        }
                        [5]: arrayindexer
                        {
                            CSharp侧的arrayNewIndexer方法
                        }
                        [6]: basenewindex
                        {
                            nil
                        }
                    }
            }
    }
  • 当我们的Test不再使用时,Test表会删除(TestPanel = nil),此时整个table会变成可回收,而表里的变量也同样会标记成可回收,即userdata_GameObject也触发了luaL_unref方法,移除了userdata_GameObject引用,此时userdata_GameObject只有在 ref_cacheRef 中有引用,此时的结构如下:
Lua侧
    _G = {
        ...

        CS(所有CSharp的对应表): LUA_REGISTRYINDEX 表中的 xlua_csharp_namespace 表
        
    }

    -10000(即 LUA_REGISTRYINDEX) = {
        ...

        ref_cacheRef(分配的id,在CSharp侧为 ObjectTranslator.cacheRef)
            table = {
                index_GameObject(CSharp侧的一个UnityEngine.GameObject,index为ObjectTranslator分配)= userdata_GameObject {
					
                    (头部 Udata*){
                      ...
                      tt = LUA_TUSERDATA
                      metatable = LUA_REGISTRYINDEX.ref_GameObject
                    }
                    (用户自定义数据 user domain*){
                      index_GameObject
                    }
                  }
                
                ...
            },
            metatable = {
                __mode = v
            }

        ...
    }

CSharp侧
    ObjectTranslator.reverseMap = {
      { go , index_GameObject },
      ...
    }

    ObjectTranslator.objects[index_GameObject] = go
  • 由于 ref_cacheRef 表为值弱表,所以当lua gc触发时,userdata_GameObject由于没有其他引用,所以会被回收,并调起 userdata_GameObject.metatable的 __gc (即 LUA_REGISTRYINDEX.ref_GameObject.__gc) 方法,从而调起CSharp侧的 StaticLuaCallbacks.LuaGC 方法,删除 ObjectTranslator.objects 和 ObjectTranslator.reverseMap(或 ObjectTranslator.enumMap) 的引用。
  • CSharp侧的GC方法如下:
    // 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);
        }
    }
        // 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);
                    }
                }
            }
        }
		}
  • 可以看到,CSharp侧的GC流程为
    • 调用lua侧的 xlua_tocsobj_safe 方法,获取对象对应的userdata的index。
    • 移除CSharp侧的 ObjectTranslator.objects 中的 index。
    • 检查对应字典中是否有存在此对象,并且对象对应的index和从lua侧获得的index一致,则移除字典中此对象。
  • 前面提到,CSharp侧的 ObjectTranslator.Push 方法绑定CSharp侧对象到Lua侧时,会调用lua侧的 xlua_tryget_cachedud 方法,先检查 ref_cacheRef 表中是否有此对象。因为lua侧gc触发时,LUA_REGISTRYINDEX表中的 ref_cacheRef 表先移除了弱值引用,而__gc方法并不是马上触发,所以这中间可能出现此对象又再关联上lua,并且关联到一个新的index,而原来旧的index将会随lua的gc移除。
  • 同样,CSharp侧移除字典的对象前,还进行了一次index的比对,如果字典中此对象对应的index和gc要移除的index不一致,则表示此对象已经再次绑定到lua侧,关联到新的index,则不需要移除字典的引用。
  • 总的来说,lua侧对CSharp侧对象的引用,只有lua gc触发的时候,才能在CSharp侧对应释放掉引用。

CSharp访问Lua对象

  • 项目中往往需要将部分逻辑放到CSharp侧编写,因此不可避免有时候会需要读取Lua侧的对象,以LuaTable表示,当我们要获取一个Lua侧的table时,主要方法为
    // ObjectTranslator.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);
    }

    ...
  • 可见,获取table时,先将table压到栈上,然后使用lua侧的 luaL_ref 方法,获取lua侧的table的引用,并增加lua侧对此table的引用数,然后创建一个CSharp侧的LuaTable对象。
  • 当CSharp侧不再使用此LuaTable,则可以调用LuaTable.Dispose方法,释放对应的LuaTable。
        // LuaBase.cs
        ...
        public void Dispose()
        {
            Dispose(true);
            // 主动调起Dispose方法时,释放非托管资源,析构时不再触发
            GC.SuppressFinalize(this);
        }

        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);
                        }
                        else //will dispse by LuaEnv.GC
                        {
                            luaEnv.equeueGCAction(new LuaEnv.GCAction { Reference = luaReference, IsDelegate = is_delegate });
                        }
#if THREAD_SAFE || HOTFIX_ENABLE
                    }
#endif
                }
                disposed = true;
            }
        }
        // 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);
            }
        }
  • 当主动调用Dispose方法时,会通过执行 ObjectTranslator.ReleaseLuaBase 方法,直接释放table,主要流程为:
    • 委托对象
      • 后面进行说明。
    • 非委托对象
      • 调用lua侧的 lua_unref 方法,移除对lua侧对象的引用。
  • 当没有主动调起Dispose方法进行释放时,为了避免没有释放,在析构函数里做了处理。
        // LuaBase.cs
        ...
        ~LuaBase()
        {
            Dispose(false);
        }
  • 当通过析构函数触发释放时,不会马上触发释放,而是创建 GCAction 对象,保存要卸载的引用 reference,存入LuaEnv.refQueue中,当CSharp侧调用 LuaEnv.Tick 方法时,才进行释放。
        // LuaEnv.cs
        ...
        public void Tick()
        {
#if THREAD_SAFE || HOTFIX_ENABLE
            lock (luaEnvLock)
            {
#endif
                var _L = L;
                lock (refQueue)
                {
                    while (refQueue.Count > 0)
                    {
                        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
        }
  • LuaEnv.Tick 方法,可以定时执行,如放在Update中,进行定期释放,可根据项目需求进行设计。
  • 总的来说,LuaTable能释放的情况有以下情况
    • LuaTable为局部变量,方法执行完成后就触发析构函数(Unity中才会触发局部变量的析构函数)释放(立即释放)。
    • LuaTable为成员变量,手动执行了LuaTable.Dispose方法,进行释放(立即释放)。
    • LuaTable为MonoBehavior的成员变量,对应的GameObject销毁后,Mono对象没有其他引用,触发LuaTable析构函数释放(延迟释放)。
    • LuaTable为普通Class的成员变量,引用class的对象置为null后,触发LuaTable析构函数释放(延迟释放)。
    • LuaTable没有直接引用时,触发CSharp的GC,触发析构函数释放(立即释放)。

时序图

  • Lua侧访问CSharp对象的时序图为 LuaCallCSharpObject.png
  • CSharp访问Lua对象的时序图为 CSharpCallLuaObject.png

调用方法

Lua调用CSharp方法

  • 前面提到,CSharp侧的类,当我们在lua侧访问时才通过 StaticLuaCallbacks.ImportType 进行注册。
  • 当我们在lua侧调用 GameObject.Find 时,在lua侧需要执行 CS.UnityEngine.GameObject.Find(“test”),由于是首次调用GameObject类,注册完成后,此时lua侧大致为:
    _G = {
        ...

        CS(所有CSharp的对应表): LUA_REGISTRYINDEX 表中的 xlua_csharp_namespace 表
            
    }

    -10000(即 LUA_REGISTRYINDEX) = {
        ...

        xlua_csharp_namespace 表(即 _G.CS 表) = {
            table = {
                UnityEngine = {
                      GameObject = ref_GameObject 表
                        
                  }
            },
            metatable = {
                __index = CSharp侧 LuaEnv.init_xlua 中的 __index(key),
                __newindex = CSharp侧 LuaEnv.init_xlua 中的 __newindex(),
                __call = CSharp侧 LuaEnv.init_xlua 中的 __call(...),
            }

            ref_GameObject(分配的id,即 GameObject 的 userdata) = {
                table = {
                            UnderlyingSystemType = ref_GameObject(GameObject的userdata)

                            Find = CSharp侧的 GameObject.Find(Wrap为 UnityEngineGameObjectWrap._m_Find_xlua_st_,反射为 MethodWrap.Call)
                            ...
                        }
                metatable = {
                    __call = GameObject的实例化(Wrap为 UnityEngineGameObjectWrap.__CreateInstance,反射为构造函数)
                    __index = cls的index闭包(参数为 [1]: obj, [2]: key )
                        {
                            ...
                        }
                    __newindex = cls的newindex闭包(参数为 [1]: obj, [2]: key, [3]: value )
                        {
                            ...
                        }
                }
            }
        }

        ref_GameObject(分配的id,对应CSharp侧的UnityEngine.GameObject) = UnityEngine.GameObject 表

        UnityEngine.GameObject (CSharp侧UnityEngine.GameObject的type.FullName,由 luaL_newmetatable 创建):
            table = {},
            metatable = {
                &tag = 1,
                
                __name = "UnityEngine.GameObject",
                __gc = CSharp侧的 StaticLuaCallbacks.LuaGC,
                __tostring = CSharp侧的 StaticLuaCallbacks.ToString,
                __index = obj的index闭包(参数为 [1]: obj, [2]: key )
                    {
                        ...
                    }
                __newindex = obj的newindex闭包(参数为 [1]: obj, [2]: key, [3]: value )
                    {
                        ...
                    }
            }
    }
  • 最终调用时,有两种方式:
    • Wrap注册:调用CSharp侧的 UnityEngineGameObjectWrap.m_Find_xlua_st,读取栈上的参数string,并调起UnityEngine.GameObject.Find,最后再把GameObject传回lua。
              // UnityEngineGameObjectWrap.cs
              ...
              [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
              static int _m_Find_xlua_st_(RealStatePtr L)
              {
                  try {
      
                      ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
      
      
      
      
                      {
                          string _name = LuaAPI.lua_tostring(L, 1);
      
                              var gen_ret = UnityEngine.GameObject.Find( _name );
                              translator.Push(L, gen_ret);
      
      
      
                          return 1;
                      }
      
                  } catch(System.Exception gen_e) {
                      return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
                  }
      
              }
      
    • 反射注册:调用CSharp侧的 MethodWrap.Call,即 MethodWrap.overloads[0].Call,根据参数个数从栈上读取,反射调用 GameObject.Find 的 MethodBase,最后把对象传回lua。
              // MethodWrapsCache.cs 
              // class MethodWrap
              ...
              public int Call(RealStatePtr L)
              {
                  try
                  {
                      if (overloads.Count == 1 && !overloads[0].HasDefalutValue && !forceCheck) return overloads[0].Call(L);
      
                      for (int i = 0; i < overloads.Count; ++i)
                      {
                          var overload = overloads[i];
                          if (overload.Check(L))
                          {
                              return overload.Call(L);
                          }
                      }
                      return LuaAPI.luaL_error(L, "invalid arguments to " + methodName);
                  }
                  catch (System.Reflection.TargetInvocationException e)
                  {
                      return LuaAPI.luaL_error(L, "c# exception:" + e.InnerException.Message + ",stack:" + e.InnerException.StackTrace);
                  }
                  catch (System.Exception e)
                  {
                      return LuaAPI.luaL_error(L, "c# exception:" + e.Message + ",stack:" + e.StackTrace);
                  }
              }
      
  • Wrap注册由于是预先生成代码,所以比较具象。而反射注册则相对比较抽象,表面上看是对 MethodWrap 对象进行操作,具体如何映射,需要对 MethodWrap 进行剖析。
  • 前面提到,MethodWrap 对象的大致构成为:
    • 每个 methodName 方法名对应一个 MethodWrap 对象。
    • 每个 MethodWrap 对象由多个 OverloadMethodWrap 对象组成。
    • 每个 OverloadMethodWrap 对象对应一个符合条件的 MemberInfo 方法。
  • 主要有两个CSharp类,OverloadMethodWrap 和 MethodWrap。
    • OverloadMethodWrap
      • 对反射查询到的 MemberInfo 方法,检查是否需要进行创建 OverloadMethodWrap 对象。
        • 方法为null,不处理。
        • 方法为泛型方法,且为 IL2CPP(即 ENABLE_IL2CPP 宏) 模式,不处理(带有泛型参数的方法一定为泛型方法)。
        • 方法为泛型方法,不为 IL2CPP 模式(即 Mono 模式),使用 tryMakeGenericMethod 方法检查是否需要处理:
          • 方法不为 MethodInfo 类型,不处理。
          • 方法包含泛型参数,但是没有泛型约束,不处理。
          • 方法包含泛型参数,但是约束类型不为 class 或者约束类型为值类型,不处理。
          • 方法返回值为泛型类型,且没有对应的有效泛型参数(即具有非值类型的class约束),不处理。
          • 其他方法,则根据泛型约束类型,创建对应的泛型类型方法。
      • 对 OverloadMethodWrap 对象进行初始化,加入 overloads(List<OverloadMethodWrap>) 列表,提供给 MethodWrap 对象。OverloadMethodWrap 初始化的对象有:
        • targetType:当前对应的类型。
        • method:当前对应的方法。
        • luaStackPosStart:记录栈上开始读取的位置,默认为1,部分情况为2:
          • 当前类型不为 Delegate,且是 Delegate 的父类(IsAssignableFrom)。
          • 当前方法不为静态方法。
          • 当前方法为构造函数。
        • targetNeeded:当前方法不为构造函数时,为true。
        • outPosArray(int[]):记录执行完方法后需要更新值内容的参数对应的索引。有以下情况:
          • 参数为 IsOut 类型,即使用 out 关键字的参数。
          • 参数为 IsRef 类型,即使用 ref 关键字的参数。
        • inPosArray(int[]):记录需要传入到方法中的参数(即非 out 关键字类型的参数)对应的索引。
        • refPos(int[]):记录需要传入到方法中,且执行完方法后需要更新值内容的参数在 inPosArray 中的索引,-1 表示不需要记录。
          • 参数为 IsRef 类型,即使用 ref 关键字的参数,且为非枚举、非基本类型(int、bool等Primitive)、非decimal类型的值类型参数(object类型的参数会直接操作堆上的对象)。
        • checkArray(ObjectCheck[]):记录需要传入到方法中的参数,所包含或引用的对象的类型的检查方法。
          • 如果是 Nullable 类型,则当栈上数据为nil时,返回true,不为nil时,选择 Nullable.GetUnderlyingType 返回的类型对应的checker。
          • 如果是 ObjectCheckers.checkersMap 中有的类型,使用对应的方法,预定义的有:
            • sbyte : ObjectCheckers.numberCheck 方法。
            • byte : ObjectCheckers.numberCheck 方法。
            • short : ObjectCheckers.numberCheck 方法。
            • ushort : ObjectCheckers.numberCheck 方法。
            • int : ObjectCheckers.numberCheck 方法。
            • uint : ObjectCheckers.numberCheck 方法。
            • long : ObjectCheckers.int64Check 方法。
            • ulong : ObjectCheckers.uint64Check 方法。
            • double : ObjectCheckers.numberCheck 方法。
            • char : ObjectCheckers.numberCheck 方法。
            • float : ObjectCheckers.numberCheck 方法。
            • decimal : ObjectCheckers.decimalCheck 方法。
            • bool : ObjectCheckers.boolCheck 方法。
            • string : ObjectCheckers.strCheck 方法。
            • object : ObjectCheckers.objectCheck 方法。
            • byte[] : ObjectCheckers.bytesCheck 方法。
            • IntPtr : ObjectCheckers.intptrCheck 方法。
            • LuaTable : ObjectCheckers.luaTableCheck 方法。
            • LuaFunction : ObjectCheckers.luaFunctionCheck 方法。
          • 如果 ObjectCheckers.checkersMap 中没有对应类型,则需要使用 ObjectCheckers.genChecker 方法动态生成。
            • 类型type不是抽象类,且是 Delegate 的父类或相同类型(IsAssignableFrom),则检查方法为:
              • 栈上对象为 nil,返回true。
              • 栈上对象为 function,返回true。
              • 调用 fixTypeCheck 的检查方法:
                • 栈上对象为 userdata,当对应CSharp侧的对象不为null,且对象的类型为当前type的父类或相同类型(IsAssignableFrom),返回true。
                • 栈上对象为 userdata,当对应CSharp侧的对象为null,栈上的类型已经注册到typeMap中(即已经调用过 ObjectTranslator.getTypeId 注册类型,且为值类型),且栈上的类型为当前type的父类或相同类型(IsAssignableFrom),返回true。
            • 类型type为枚举,则检查方法为:
              • 调用 fixTypeCheck 的检查方法。
            • 类型type为接口,则检查方法为:
              • 栈上对象为 nil,返回true。
              • 栈上对象为 table,返回true。
              • 调用 fixTypeCheck 的检查方法。
            • 类型type为默认构造函数的class,检查方法为:
              • 栈上对象为 nil,返回true。
              • 栈上对象为 table,返回true。
              • 调用 fixTypeCheck 的检查方法。
            • 类型type为值类型,检查方法为:
              • 栈上对象为 table,返回true。
              • 调用 fixTypeCheck 的检查方法。
            • 类型type为数组,检查方法为:
              • 栈上对象为 nil,返回true。
              • 栈上对象为 table,返回true。
              • 调用 fixTypeCheck 的检查方法。
            • 其他类型,检查方法为:
              • 栈上对象为 nil,返回true。
              • 调用 fixTypeCheck 的检查方法。
        • castArray(ObjectCast[]):记录需要传入到方法中的参数,所包含或引用的对象的类型的转换方法。
          • 如果是 Nullable 类型,则当栈上数据为nil时,返回null,不为nil时,选择 Nullable.GetUnderlyingType 返回的类型对应的caster。
          • 如果是 ObjectCasters.castersMap 中有的类型,使用对应的方法,预定义的有:
            • char : ObjectCasters.charCaster 方法。
            • sbyte : ObjectCasters.sbyteCaster 方法。
            • byte : ObjectCasters.byteCaster 方法。
            • short : ObjectCasters.shortCaster 方法。
            • ushort : ObjectCasters.ushortCaster 方法。
            • int : ObjectCasters.intCaster 方法。
            • uint : ObjectCasters.uintCaster 方法。
            • long : ObjectCasters.longCaster 方法。
            • ulong : ObjectCasters.ulongCaster 方法。
            • double : ObjectCasters.getDouble 方法。
            • float : ObjectCasters.floatCaster 方法。
            • decimal : ObjectCasters.decimalCaster 方法。
            • bool : ObjectCasters.getBoolean 方法。
            • string : ObjectCasters.getString 方法。
            • object : ObjectCasters.getObject 方法。
            • byte[] : ObjectCasters.getBytes 方法。
            • IntPtr : ObjectCasters.getIntptr 方法。
            • LuaTable : ObjectCasters.getLuaTable 方法。
            • LuaFunction : ObjectCasters.getLuaFunction 方法。
          • 如果 ObjectCasters.castersMap 中没有对应类型,则需要使用 ObjectCasters.genCaster 方法动态生成。
            • 类型type为 Delegate 的父类或相同类型(IsAssignableFrom),转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
                • 如果栈上对象为CSharp侧对象,且当前type的父类或相同类型(IsAssignableFrom),则返回
              • 如果栈上对象不是 function,则返回null。
              • 使用 ObjectTranslator.CreateDelegateBridge 创建委托对象返回。
            • 类型type为 DelegateBridgeBase 的父类或相同类型(IsAssignableFrom),转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上对象不是 function,则返回null。
              • 使用 ObjectTranslator.CreateDelegateBridge 创建委托对象返回。
            • 类型type为接口,转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上对象不是 table,则返回null。
              • 使用 ObjectTranslator.CreateInterfaceBridge 创建对象返回。
            • 类型type为枚举,转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上类型为string,则使用 Enum.Parse 方法转换枚举返回。
              • 如果栈上类型为number,则将number转成int,使用 Enum.ToObject 方法转换枚举返回。
              • 如果为其他类型,则抛出异常。
            • 类型type为数组,转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上对象不是 table,则返回null。
              • 创建对应的数组Array,并将值设置到Array中,返回Array对象。
            • 类型type为List,转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上对象不是 table,则返回null。
              • 创建对应类型的List,并将值设置到List中,返回List对象。
            • 类型type为Dictionary<TKey,TValue>,转换类型为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上对象不是 table,则返回null。
              • 创建对应类型的Dictionary,并将值设置到Dictionary中,返回Dictionary对象。
            • 类型type为默认构造函数的class,或非枚举的值类型,转换方法为:
              • 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
              • 如果栈上对象不是 table,则返回null。
              • 反射创建对应类型的对象,并根据target设置变量值,返回对象。
          • isOptionalArray(bool[]):记录需要传入到方法中的参数,是否为可选参数。
          • defaultValueArray(object[]):记录需要传入到方法中的参数的默认参数值(不为可选参数时为null)。
          • paramsType:可变长参数的类型。
          • args(object[]):参数数组,预先创建,只创建一次。
          • isVoid:不为构造函数,且返回类型为void时为true。
          • HasDefalutValue:是否有默认参数值,如果有可选参数,则为true。
      • 检查方法 Check :
        • 如果栈上的参数个数超过CSharp方法的参数个数,且不为可选参数,则返回false。
        • 如果栈上的参数不能通过 checkArray 的检查,则返回false。
        • 如果有可变长参数,使用 checkArray 检查并返回结果。
        • 如果没有可变长参数,栈上的参数个数和CSharp侧的参数一致,则返回true,否则返回false。
      • 调用方法 Call :
        • 检查 luaStackPosStart ,如果大于1,则从栈上的位置1读取目标对象target。
        • 使用 castArray 中的方法,创建参数存入 arg[] 中。
        • 使用参数执行当前方法,获得返回值 ret。
        • 如果当前类型为值类型,且targetNeeded为true(即不为构造函数),则调用 ObjectTranslator.Update 方法,将当前对象target更新到栈上对应位置。ObjectTranslator.Update 方法的流程为:
          • 如果 obj 为userdata类型,更新 ObjectTranslator.objects 中的对象为新的 obj。
          • 如果 obj 不为userdata类型,则使用 ObjectTranslator.custom_update_funcs 中的update方法进行更新。
            • 工具生成的脚本WrapPusher,初始化注册了常用的结构体、枚举从Lua侧传到CSharp侧和从CSharp侧传到Lua侧的方法,对应存到ObjectTranslator中的custom_push_funcs、custom_get_funcs、custom_update_funcs ,避免传递过程中的GC产生。工具流程见后续介绍。
        • 如果返回值不为void(即 isVoid 为false),则将返回值压到栈上。(使用PushAny方法,参数为object对象,会有装拆箱的问题
        • 处理 outPosArray :
          • 如果对应的 refPos 不为 -1,即栈上对应的参数值需要更新,则使用 ObjectTranslator.Update 方法更新对应位置参数。(Update方法参数为object对象,会有装拆箱问题)。
          • 将 arg 对应的参数值压入栈上(使用PushAny方法,参数为object对象,会有装拆箱的问题)。
        • 遍历 arg ,将所有参数置为null,保证下一次的正常使用。
    • MethodWrap
      • MethodWrap主要有几个部分:
        • methodName:方法名。
        • overloads:OverloadMethodWrap列表,即同一个方法名的重载方法。
        • forceCheck:是否强制检查,构造函数为true,其他默认为false。
        • Call 方法:反射注册到lua侧调用的方法,主要流程为:
          • 如果 overloads 只有一个方法,且没有默认参数值,且不需要强制检查,则直接调用 overloads[0].Call,即调用 OverloadMethodWrap.Call。
          • 遍历 overloads 中的所有方法,调用 OverloadMethodWrap.Check 进行检查,找到第一个检查通过的方法后,执行 OverloadMethodWrap.Call 方法并返回。

CSharp调用Lua方法

  • 和Lua对象一样,有时候也需要调用Lua侧的方法,主要是通过table获取,以LuaFunction表示,如:
    • 直接通过方法名,找到对应表中的方法。
    • 找到某个LuaTable,查找表中的方法。
  • 当我们要获取一个Lua侧的function时,通常为
        // ObjectCasters.cs
        ...
        private object getLuaFunction(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 LuaFunction) ? obj : null;
            }
            if (!LuaAPI.lua_isfunction(L, idx))
            {
                return null;
            }
            LuaAPI.lua_pushvalue(L, idx);
            return new LuaFunction(LuaAPI.luaL_ref(L), translator.luaEnv);
        }
  • LuaFunction的创建流程,也是先将function压到栈上,然后使用lua侧的 luaL_ref 方法,获取lua侧的function的引用,并增加lua侧对此function的引用数,然后创建一个CSharp侧的LuaFunction对象。
  • 和LuaTable一样,LuaFunction也是继承LuaBase,因此释放机制也相同。

委托

  • 当我们需要把Lua方法注入CSharp侧的回调,通过CSharp调起时,需要使用委托的方式。即CSharp侧会调起对应的委托,而委托方法里会根据从栈上读取参数和方法并调起对应的lua方法,实现lua方法的回调。
  • 获取对应类型的委托方法为:
        // 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 方法创建CSharp侧的委托方法。
        // ObjectTranslator.cs
        ...
        Dictionary<int, WeakReference> delegate_bridges = new Dictionary<int, WeakReference>();
        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);
            LuaAPI.lua_pushvalue(L, idx);
            LuaAPI.lua_pushnumber(L, reference);
            LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX);
            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;
                }
                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.AddDelegate(delegateType, ret);
                delegate_bridges[reference] = new WeakReference(bridge);
                return ret;
            }
            catch(Exception e)
            {
                bridge.Dispose();
                throw e;
            }
        }

  • ObjectTranslator.CreateDelegateBridge 方法创建委托的流程主要为:
    • LUA_REGISTRYINDEX 表中没有对应lua方法的引用,即没有建立对应委托关系,进行创建。
      • 将lua方法压入栈上。
      • 调用lua侧的 luaL_ref 方法,对lua方法创建并返回一个在 LUA_REGISTRYINDEX 表中的引用 reference,将lua方法出栈。
      • 将 reference 存入 LUA_REGISTRYINDEX 表,key值为当前的lua方法。
      • 使用 reference 创建 DelegateBridge 对象 bridge,即一个lua方法对应一个CSharp侧的 DelegateBridge 。
        • DelegateBridge 继承 DelegateBridgeBase,当一个lua方法只对应多种type的委托时,内部才会创建字典 Dictionary<Type, Delegate> ,用字典进行管理,如果只有一种type,则不使用字典,避免创建太多字典。
      • 如果CSharp的委托类型 delegateType 为null,则将 bridge 以弱引用(WeakReference)形式,存入 delegate_bridges(Dictionary<int, WeakReference>) 中,key值为 reference,直接返回 bridge。
      • 使用 ObjectTranslator.getDelegate 方法,从当前 bridge 中创建对应 delegateType 的委托。
      • 使用 ObjectTranslator.AddDelegate 方法,将获得的委托存入 bridge 中。
      • 将 bridge 以弱引用(WeakReference)形式,存入 delegate_bridges 中。
      • 返回委托方法。
    • LUA_REGISTRYINDEX 表中已经有对应lua方法的引用,即已经有对应的委托,直接查找使用。
      • 获取lua方法对应的引用 reference。
      • 如果 delegate_bridges[reference].IsAlive 不为true,表示当前委托以销毁,需要重新建立委托关系。
      • 如果委托类型 delegateType 为null,则直接返回对应对象,即返回 bridge。
      • 如果当前 bridge 中已经有此委托类型的委托缓存,则直接返回,如果没有则重新创建,并存入 bridge 中。
  • 假设我们当前lua侧的方法为 lua_func_test,创建委托后大致为:
Lua侧
    -10000(即 LUA_REGISTRYINDEX) = {
        ...

                
        lua_func_test(lua侧的方法): ref_lua_func_test(LUA_REGISTRYINDEX 申请的id,即 reference)

        ref_lua_func_test : lua_func_test (由lua侧的 luaL_Ref 方法创建的映射)
    }


CSharp侧
    ObjectTranslator.delegate_bridges = {
        { ref_lua_func_test , new WeakReference( new DelegateBridge(reference, luaEnv)) },
        ...
    }

  • 当我们建立好lua方法和CSharp的联系后,我们得到的是 DelegateBridge 对象,而最终的委托是通过 ObjectTranslator.getDelegate 方法获取的,具体流程为:
    • 根据传入的bridge,使用 DelegateBridgeBase.GetDelegateByType 方法获取。由于传入的bridge为 DelegateBridge ,即使用 DelegateBridge.GetDelegateByType 方法获取。DelegateBridge 声明为 partial 类型,具体有三个部分:
      • 基本部分:DelegateBridge.cs ,主逻辑放在这里。
      • 工具生成部分:DelegatesGensBridge.cs ,为使用 [CSharpCallLua] 特性声明的委托,由工具生成的注册委托。这里重写了 GetDelegateByType 方法,根据传入的委托类型,使用生成的委托方法,创建对应类型的委托。
      • 泛型兼容部分:GenericDelegateBridge.cs ,为0 ~ 4个参数的 Action 和 Func 泛型方法。
    • 如果委托类型为 Delegate 或 MulticastDelegate ,则返回null。
    • 获取委托类型的 Invoke 方法,如果 DelegatesGensBridge.cs 中生成的 __Gen_Delegate_ImpXXX 方法 或 GenericDelegateBridge.cs 中的 Action() 方法的参数和 Invoke 方法一致,则使用此方法和bridge创建对应委托,并存入 DelegatesGensBridge.delegateCreatorCache 中进行缓存,减少反射查找的次数。
    • 如果找不到参数匹配的方法,则使用 DelegatesGensBridge.getCreatorUsingGeneric 方法进行动态创建:
      • 获取 GenericDelegateBridge.cs 中的所有 Action 和 Func 方法,如果某种方法数量不为5,则不进行创建(只支持0~4个参数,总计各5个方法)。
      • 不使用 XLUA_GENERAL 宏时,如果委托的 Invoke 方法返回值为值类型,或者参数超过4个,则创建空委托。
      • 如果委托的 Invoke 方法的参数类型有值类型,或者为out、ref类型,则创建空委托。(需要后续研究原因)
      • 根据参数个数,选择对应的 Action 或 Func 泛型方法,根据参数类型,构造对应类型的方法 methodInfo,以委托类型和bridge对象创建委托。
  • 了解委托创建的流程后,再来看销毁的情况。前面提到,当主动调用Dispose方法时,会通过执行 ObjectTranslator.ReleaseLuaBase 方法,直接释放 LuaBase 对象,此时委托释放的流程为:
    • 从 LUA_REGISTRYINDEX 表中,查找 reference 对应的lua方法。
    • 从 LUA_REGISTRYINDEX 表中,查找lua方法对应的引用id,如果和reference一样,则将lua方法对应的值设为nil,即 LUA_REGISTRYINDEX[func] = nil。
    • 调用lua侧的 lua_unref 方法,移除 reference 对应的lua方法的引用。
    • 移除 ObjectTranslator.delegate_bridges 中的 reference 对应的委托。
  • 注意,如果委托方法中引用了自身table(self),即产生了闭包,则table释放的时候,需要将CSharp侧对此方法的引用置为null,table才能被lua gc回收。否则即便table已经在CSharp侧Dispose了,但由于委托方法还引用了table,而CSharp还引用着方法,导致table还是不能释放。

重载

  • Wrap注册
    • 以 GameObject.GetComponentInChildren 方法为例,lua侧最终调用的CSharp侧代码为:
          // UnityEngine_GameObjectWrap.cs
          ...
          [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
          static int _m_GetComponentInChildren(RealStatePtr L)
          {
      	    try {
    
                  ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
    
    
                  UnityEngine.GameObject gen_to_be_invoked = (UnityEngine.GameObject)translator.FastGetCSObj(L, 1);
    
    
      		    int gen_param_count = LuaAPI.lua_gettop(L);
    
                  if(gen_param_count == 2&& translator.Assignable<System.Type>(L, 2)) 
                  {
                      System.Type _type = (System.Type)translator.GetObject(L, 2, typeof(System.Type));
    
                          var gen_ret = gen_to_be_invoked.GetComponentInChildren( _type );
                          translator.Push(L, gen_ret);
    
    
    
                      return 1;
                  }
                  if(gen_param_count == 3&& translator.Assignable<System.Type>(L, 2)&& LuaTypes.LUA_TBOOLEAN == LuaAPI.lua_type(L, 3)) 
                  {
                      System.Type _type = (System.Type)translator.GetObject(L, 2, typeof(System.Type));
                      bool _includeInactive = LuaAPI.lua_toboolean(L, 3);
    
                          var gen_ret = gen_to_be_invoked.GetComponentInChildren( _type, _includeInactive );
                          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 UnityEngine.GameObject.GetComponentInChildren!");
    
          }
    
    • 从例子中可以看到,Wrap注册的重载方法,会先检查栈上的参数个数,然后再检查参数的类型是否和方法对应上,如果检查通过则会执行对应的 GameObject.GetComponentInChildren 方法。
  • 反射注册
    • 前面Lua侧调用CSharp侧方法中有介绍,同一个方法名的方法会包装成一个 OverloadMethodWrap 的列表,存到 MethodWrap 中,通过对参数类型进行 Check 方法检查,来调用对应的方法。
  • 由于CSharp侧的多种类型(int、float等)对应Lua侧的都为number类型,所以对于此类方法,有两种解决方法:
    • 定义不同的方法,避免上述情况的出现。(建议
    • 通过 xlua.tofunction 结合反射来处理,xlua.tofunction 输入一个 MethodBase 对象,返回一个lua函数。(由于使用反射获取,需要进行cache,重复使用)
          // StaticLuaCallback.cs 
          ...
          [MonoPInvokeCallback(typeof(LuaCSFunction))]
          public static int ToFunction(RealStatePtr L)
          {
              try
              {
                  ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
                  MethodBase m;
                  translator.Get(L, 1, out m);
                  if (m == null)
                  {
                      return LuaAPI.luaL_error(L, "ToFunction: #1 argument must be a MethodBase");
                  }
                  translator.PushFixCSFunction(L,
                          new LuaCSFunction(translator.methodWrapsCache._GenMethodWrap(m.DeclaringType, m.Name, new MethodBase[] { m }).Call));
                  return 1;
              }
              catch (Exception e)
              {
                  return LuaAPI.luaL_error(L, "c# exception in ToFunction: " + e);
              }
          }
    

无GC传值

  • 前面说到,Lua侧获取CSharp对象是通过userdata的形式,即获取对应的id,而CSharp侧将id和对象存入ObjectTranslator.objects中,由于 ObjectTranslator.objects 中存的对象类型为object,对于值类型的对象,使用object就会需要装拆箱的操作,还会产生gc,因此对于此类对象,则需要有无gc的传值方法,来提升效率。
  • 对于大部分基本类型,如:bool、int,Lua和CSharp可以通过c的api进行交互,如:
    • lua_pushboolean/lua_toboolean
    • lua_pushinteger/lua_tointeger
  • 通过这些方法,将数据压到栈上,或从栈上读取,则实现了数据交互,而不会产生gc。而无gc传值也是基于这种方法,通过规定struct的数据类型和顺序,则可实现入栈和出栈的配套方法。尤其是对频繁调用的类型,如UnityEngine.Vector3,效率上会有很大的提升。
  • 以 UnityEngine.Vector3 为例,CSharp侧传Vector3到Lua侧的代码为:
        // WrapPusher.cs
        ...
        int UnityEngineVector3_TypeID = -1;
        public void PushUnityEngineVector3(RealStatePtr L, UnityEngine.Vector3 val)
        {
            if (UnityEngineVector3_TypeID == -1)
            {
			    bool is_first;
                UnityEngineVector3_TypeID = getTypeId(L, typeof(UnityEngine.Vector3), out is_first);
				
            }
			
            IntPtr buff = LuaAPI.xlua_pushstruct(L, 12, UnityEngineVector3_TypeID);
            if (!CopyByValue.Pack(buff, 0, val))
            {
                throw new Exception("pack fail fail for UnityEngine.Vector3 ,value="+val);
            }
			
        }
  • 可以看到,Vector3传到Lua侧的流程为:
    • 获取Vector3的type_id。
    • 根据type_id,调用lua侧的 xlua_pushstruct 方法,创建 userdata(xlua.c 中是 CSharpStruct * 类型,css)。
      • 设置 css->fake_id = -1 。
      • 设置 css->len = size 。
      • 设置metatable为 LUA_REGISTRYINDEX 表中对应type_id的table。
    • 调用 CopyByValue.Pack 方法,执行lua侧的 xlua_pack_float3 方法。
      • 检查 css->fake_id 是否为-1,css->len 是否为 12(即 sizeof(float) * 3 + offset,3个float加上0偏移)。
      • 获取 css->data[0] 的地址,加上offset偏移,得到 float * 的起始地址指针 pos,将Vector3的x、y、z赋值到 pos[0]、pos[1]、pos[2]中,实现CSharp侧通过c层的api,直接操作栈上数据。
  • 其中,CSharpStruct 类型在xlua.c中的定义为:
typedef struct {
	int fake_id;
    unsigned int len;
	char data[1];
} CSharpStruct;

  • Lua侧传Vector3到CSharp侧的代码为:
        // WrapPusher.cs
        ...
        public void Get(RealStatePtr L, int index, out UnityEngine.Vector3 val)
        {
		    LuaTypes type = LuaAPI.lua_type(L, index);
            if (type == LuaTypes.LUA_TUSERDATA )
            {
			    if (LuaAPI.xlua_gettypeid(L, index) != UnityEngineVector3_TypeID)
				{
				    throw new Exception("invalid userdata for UnityEngine.Vector3");
				}
				
                IntPtr buff = LuaAPI.lua_touserdata(L, index);if (!CopyByValue.UnPack(buff, 0, out val))
                {
                    throw new Exception("unpack fail for UnityEngine.Vector3");
                }
            }
			else if (type ==LuaTypes.LUA_TTABLE)
			{
			    CopyByValue.UnPack(this, L, index, out val);
			}
            else
            {
                val = (UnityEngine.Vector3)objectCasters.GetCaster(typeof(UnityEngine.Vector3))(L, index, null);
            }
        }
  • 可以看到,Vector3传到CSharp侧的流程为:
    • 如果栈上的对象类型为userdata,则
      • 调用lua侧的 xlua_gettypeid 方法,检查 userdata 中的 metatable[1](即设置的key值 type_id)是否为Vector3的type_id。
      • 调用lua侧的 lua_touserdata 方法, 获得 userdata 的地址。
      • 调用CSharp侧的 CopyByValue.UnPack 方法,执行lua侧的 xlua_unpack_float3 方法。
        • 检查 css->fake_id 是否为-1,css->len 是否为 12(即 sizeof(float) * 3 + offset,3个float加上0偏移)。
        • 获取 css->data[0] 的地址,加上offset偏移,得到 float * 的起始地址指针 pos,将pos[0]、pos[1]、pos[2]的值设置到3个 float * 指向的地址,传到CSharp侧作为x、y、z,实现c层直接赋值。
        • CSharp侧创建 Vector3 对象,并将x、y、z设置。
    • 如果栈上的对象类型为table,则
      • 调用CSharp侧的 Utils.LoadField 方法,获取table的 “x” 对应的值,压入栈上。
      • 调用CSharp侧的 ObjectTranslator.Get 方法,读取栈上的值,作为Vector3的x。
      • 将栈顶数据弹出。
      • 按照x坐标的获取步骤,依次进行y、z坐标的获取,最后得到 Vector3 对象。
    • 其他类型,使用 ObjectCaster.GetCaster 方法进行获取。
  • 至此,Vector3的无GC传值已实现,其实本质上是通过c层定义一个struct,设置好struct的大小和类型,对应创建一个userdata,而CSharp侧通过c层的api,直接对栈上数据进行读写,避免了对象的装拆箱。
  • 前面提到的方法,是将Vector3映射到userdata,方案的优缺点为:
    • 优点:userdata比table更省内存。
    • 缺点:userdata操作字段比table性能稍低。
  • 如果为了要提高访问性能,可以将Vector3改到lua侧实现,有两种方式:
    • 把Vector3的方法实现改为lua实现,通过xlua.genaccessor实现不经过C#直接操作内存。
    • struct映射到table改造。

泛型

  • CSharp侧调用Lua侧方法时,LuaFunction提供了两种方法,可简化CSharp侧的调用方式,且不会产生GC。
    • 无返回值:Action,支持2个参数以内的泛型调用。
    • 一个返回值(泛型TResult):Func,支持2个参数以内的泛型调用。
  • LuaFunction声明为partial,如果需要其他参数个数类型的方法,可以自行添加。
  • Lua侧调用CSharp的泛型方法,根据官方文档,有两种调用方式。
    • 方式一,Wrap生成调用。即将栈上的对象,在CSharp侧以限定类型表示,然后以限定类型调起泛型方法。此方式的条件有:
      • 需要带泛型类型的参数。
      • 泛型类型需要限定为class。
    • 方式二,反射调用。即使用 Type.MakeGenericType 方法,创建对应类型的泛型方法调用。此方式的条件有:
      • mono下可以使用。
      • il2cpp下
        • 泛型参数为引用类型可以使用。
        • 泛型参数为值类型,CSharp侧有同样的泛型参数调用过,则可以使用。

Wrap生成调用

  • 当我们的泛型方法为
    // TestGeneric.cs
    ...
    public void Test1<T>(T p) where T : Component
    {
        Debug.Log("TestGeneric.Test1<T> " + typeof(T));        
    }
  • 生成Wrap代码后,则将泛型方法绑定到Lua侧,即
        public static void __Register(RealStatePtr L)
        {
			...
			Utils.RegisterFunc(L, Utils.METHOD_IDX, "Test1", _m_Test1);
			...
        }


        [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
        static int _m_Test1(RealStatePtr L)
        {
		    try {
            
                ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            
            
                TestGeneric gen_to_be_invoked = (TestGeneric)translator.FastGetCSObj(L, 1);
            
            
                
                {
                    UnityEngine.Component _p = (UnityEngine.Component)translator.GetObject(L, 2, typeof(UnityEngine.Component));
                    
                    gen_to_be_invoked.Test1( _p );
                    
                    
                    
                    return 0;
                }
                
            } catch(System.Exception gen_e) {
                return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
            }
            
        }
  • lua侧调用方法为
    local test1 = TestGeneric()
    local comp = CS.UnityEngine.GameObject.Find("go"):GetComponent(typeof(CS.UnityEngine.Camera))
    test1:Test1(comp)
  • 可以看到,lua侧调起泛型方法的流程为,先获取 Component 类型的参数对象,然后直接执行 gen_to_be_invoked.Test1(_p) 方法,即调用 TestGeneric.Test1(Component p) 方法。
  • 也就是说,在生成对应的Wrap方法的时候,就已经确定此泛型方法的实例方法的类型为限定类型。因此如果此泛型方法需要使用泛型类型 T 来执行相关逻辑,则Lua侧无法正确调起对应类型(如上述示例,Lua侧绑定只能调用 Component 类型的泛型方法)。
  • 对于此方法的条件,有以下情况:
    • 泛型方法有限定类型,但没有对应类型参数。
      • 如果自己增加Wrap方法,同样以限定类型调起泛型,也可以调起。但是工具中对于这种类型不予以生成,这里可能考虑由于没有泛型参数,逻辑中较大可能会使用泛型 T 的类型进行逻辑判断,而Lua侧调起时固定为传入限定类型,则失去了泛型的作用。
          [LuaCallCSharp]
          public class TestGeneric
          {
              ...
              public void Test2<T>()
              {
                  Debug.Log("TestGeneric.Test2<T> " + typeof(T));
              }
      
              ...
          }
      
          public class TestGenericWrap 
          {
              public static void __Register(RealStatePtr L)
              {
                  ...
                  Utils.RegisterFunc(L, Utils.METHOD_IDX, "Test2", _m_Test2);
                  ...
              }
      
              [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
              static int _m_Test2(RealStatePtr L)
              {
                  try
                  {
      
                      ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
      
      
                      TestGeneric gen_to_be_invoked = (TestGeneric)translator.FastGetCSObj(L, 1);
      
      
      
                      {
      
                          gen_to_be_invoked.Test2<UnityEngine.Component>();
      
      
      
                          return 0;
                      }
      
                  }
                  catch (System.Exception gen_e)
                  {
                      return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
                  }
      
              }
      
              ...
          }
      
    • 泛型方法有限定类型,有对应类型参数,但是类型不为class(如 Interface)。
      • 当类型为 Interface 时,translator.GetObject 方法是通过调用 ObjectTranslator.CreateInterfaceBridge 方法来创建对象的,创建的对象是 XXXBridge 对象(见后续 Interface 说明),和原类型不匹配,所以不予以生成。
    • 泛型方法没有限定类型,包括带参数和不带参数。
      • Wrap生成的时候不知道该以什么类型调起,则无法提前生成Wrap方法。

反射调用

  • Lua调用CSharp侧方法中提到,当采用反射注册的方式,最后调用的对象为 MethodWrap ,每个 methodName 方法名对应一个 MethodWrap 对象,每个 MethodWrap 对象由多个 OverloadMethodWrap 对象组成。当方法为泛型方法,且为 IL2CPP(即 ENABLE_IL2CPP 宏) 模式,不创建 OverloadMethodWrap 对象,即没有Wrap提前生成代码的情况下,是不能直接调起泛型方法的。因此xlua提供了反射调用的方法。当CSharp侧的泛型方法为
    [LuaCallCSharp]
    public class TestGeneric
    {
        ...
        public void Test2<T>()
        {
            Debug.Log("TestGeneric.Test2<T> " + typeof(T));
        }
        ...
    }
  • Lua侧调用方法为
    local o = CS.TestGeneric()

    local test2_method = xlua.get_generic_method(CS.TestGeneric,'Test2')
    local Test2 = test2_method(CS.UnityEngine.Camera)
    Test2(o)
  • 本质上,即使用反射方式,调用 Type.MakeGenericMethod 方法,创建泛型方法的泛型实例方法,再直接调起实例方法,即完成对泛型方法的调用。
  • 然而,此方法在 mono 下可以使用。在 il2cpp 下,如果泛型参数为引用类型可以使用。如果泛型参数为值类型,CSharp侧有同样的泛型参数调用过,才可以使用。当我们使用 il2cpp 模式生成apk包,在Lua侧调用
    local o = CS.TestGeneric()

    local test2_method = xlua.get_generic_method(CS.TestGeneric,'Test2')
    local Test2 = test2_method(CS.System.Int32)
    Test2(o)
  • 会发现调用不了,报错信息为
E/Unity: LuaException: c# exception:Attempting to call method 'TestGeneric::Test2<System.Int32>' for which no ahead of time (AOT) code was generated.,stack:  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
      at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
      at XLua.OverloadMethodWrap.Call (System.IntPtr L) [0x00000] in <00000000000000000000000000000000>:0 
      at XLua.MethodWrap.Call (System.IntPtr L) [0x00000] in <00000000000000000000000000000000>:0 
      at XLua.LuaDLL.lua_CSFunction.Invoke (System.IntPtr L) [0x00000] in <00000000000000000000000000000000>:0 
      at XLua.StaticLuaCallbacks.FixCSFunction (System.IntPtr L) [0x00000] in <00000000000000000000000000000000>:0 
  • 当我们在CSharp侧中,新增一个没有调用的方法
    private void Compile()
    {
        var o = new TestGeneric();
        o.Test2<int>();
    }
  • 重新打包后,发现能正常调用
I/Unity: TestGeneric.Test2<T> System.Int32
    UnityEngine.Logger:Log(LogType, Object)
    System.Reflection.MonoMethod:Invoke(Object, BindingFlags, Binder, Object[], CultureInfo)
    System.Reflection.MethodBase:Invoke(Object, Object[])
    XLua.OverloadMethodWrap:Call(IntPtr)
    XLua.MethodWrap:Call(IntPtr)
    XLua.LuaDLL.lua_CSFunction:Invoke(IntPtr)
    XLua.StaticLuaCallbacks:FixCSFunction(IntPtr)
  • 一些平台不允许运行时代码生成。因此,任何依赖于目标设备上即时 (JIT) 编译的托管代码都将失败。相反,您必须提前 (AOT) 编译所有托管代码。当我们使用 IL2CPP 模式时,就确定了必须使用 AOT 编译,也就是说,所有Lua侧调用的代码,必须在编译期间就已经生成对应的代码。没有增加 Compile 方法前,AOT 编译器不承认它应该为 Test2 带有 T 的泛型方法生成 System.Int32 类型的代码 ,因此它继续,跳过此方法。当调用该方法并且运行时找不到要执行的正确代码时,它会返回此错误消息。
  • 当增加了 Compile 方法后,相当于强制编译器生成正确的代码。而此方法不需要调用,它只需要存在编译器就可以识别它。
  • 然而,当泛型类型全为引用类型时,则不需要增加强制编译的方法,但如果有一个泛型类型为值类型时,则需要增加强制编译方法。xlua文档中的说明,在CSharp侧有同样的泛型参数调用过,也就是编译器有对这个类型进行提前编译。而其中的 mono 下可以直接使用,目前应该是针对Android下的mono,可以使用 JIT,而iOS下只能使用 il2cpp。对于其他平台下,mono 也有 AOT 限制的,同样不能使用(XBox One、PS4等)。
  • 因此,如果需要对某个泛型方法进行强制编译,可以将常用的值类型对象进行强制编译,就可以在lua侧自由调用了。
    private void Compile()
    {
        var o = new TestGeneric();
        o.Test2<int>();
        o.Test2<float>();
        o.Test2<bool>();
        ...
        o.Test3<object, int>();
        o.Test3<object, float>();
        o.Test3<object, bool>();
        ...
        o.Test3<int, object>();
        o.Test3<float, object>();
        o.Test3<bool, object>();
    }
  • 注意,此方法创建的泛型实例方法,由于是使用反射方式创建的,需要自己缓存起来,重复使用,避免频繁反射导致性能下降。

反射

  • 前面提到,CSharp侧的类注册的时候,如果没有提前生成wrap脚本,则都会通过反射进行创建。
  • 反射占据了xlua框架的很大一部分,为了降低反射带来的性能问题,反射后的对象都会进行缓存,避免多次反射。
  • AOT 平台不能实现 System.Reflection.Emit 命名空间中的任何方法。其余的 System.Reflection 都是可以接受的,只要编译器可以推断出通过反射使用的代码需要在运行时存在。

64位整型

  • 初始化流程中,调用了lua侧的 luaopen_i64lib 方法,进行64位整型库的加载,初始化完成后如下:
    _G = {
        uint64 = {
            tostring = i64lib.c的 uint64_tostring 方法
            compare = i64lib.c的 uint64_compare 方法
            divide = i64lib.c的 uint64_divide 方法
            remainder = i64lib.c的 uint64_remainder 方法
            parse = i64lib.c的 uint64_parse 方法
        }
    }

    -10000(即 LUA_REGISTRYINDEX) = {
        ...

        -- 5.1 版本才有
        8(即 INT64_META_REF) = {
            __add = i64lib.c的 int64_add 方法
            __sub = i64lib.c的 int64_sub 方法
            __mul = i64lib.c的 int64_mul 方法
            __div = i64lib.c的 int64_div 方法
            __mod = i64lib.c的 int64_mod 方法
            __unm = i64lib.c的 int64_unm 方法
            __pow = i64lib.c的 int64_pow 方法
            __tostring = i64lib.c的 int64_tostring 方法
            __eq = i64lib.c的 int64_eq 方法
            __lt = i64lib.c的 int64_lt 方法
            __le = i64lib.c的 int64_le 方法
            __tostring = i64lib.c的 int64_tostring 方法
        }

    }
  • 可以看到,如果是lua 5.1版本,会在 LUA_REGISTRYINDEX 表中创建一个 INT64_META_REF 表,并添加各种计算方法。由于xlua支持多个lua版本,可根据需要选择不同的lua版本库。而对于不同的lua版本,对64位整型的支持方法不一样。
  • lua 5.1 版本,lua侧的所有数字都使用double类型,即int、float都用number(double类型)表示。因此,对于int64、uint64,则不能正常使用number表示,在lua侧使用 Integer64* 的userdata表示。Integer64* 的结构如下:
enum IntegerType {
	Int,    // 64位整型
	UInt,   // 64位无符号整型
	Num     // 其他number
};

typedef struct {
	int fake_id;    // 标识,如果有使用则设为-1
	int8_t type;    // 类型,使用 IntegerType 枚举
    union {
		int64_t i64;    // int64的数据
		uint64_t u64;   // uint64的数据
	} data;
} Integer64;
  • 创建userdata后,会将userdata的metatable设置为 LUA_REGISTRYINDEX 表中的 INT64_META_REF 表,对64位整型进行计算的时候,则调用metatable中的元方法进行计算。
  • lua 5.3版本后,lua增加了int64的支持,则不需要使用userdata进行处理,因此lua 5.3版本不再需要创建 INT64_META_REF 表。

协程

  • 由于xlua存在热修复功能,所以协程需要兼容CSharp侧的协程,不能直接使用lua侧的原生协程,需要进行封装。
  • 当我们在lua侧开启一个Unity协程时,大致代码为:
-- coroutine_test.lua
...

local cs_coroutine = (require 'cs_coroutine')

cs_coroutine.start(function()
    print('stop coroutine a after 5 seconds')
	coroutine.yield(CS.UnityEngine.WaitForSeconds(5))
	cs_coroutine.stop(a)
    print('coroutine a stoped')
end)
  • 可以看到,开启协程的方法为调用 cs_coroutine.start 方法,传入协程执行的方法。cs_coroutine 的代码如下:
-- cs_coroutine.lua

local util = require 'xlua.util'

local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.XLuaTest.Coroutine_Runner))

return {
    start = function(...)
	    return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
	end;

	stop = function(coroutine)
	    cs_coroutine_runner:StopCoroutine(coroutine)
	end
}
  • cs_coroutine.start 方法,获取了一个CSharp侧的Monobehaviour对象,并调起 StartCoroutine 方法开启协程。传入的方法由 util.cs_generator 创建,即将 cs_coroutine_test 中传入的方法传入 util.cs_generator 进行构建。util.cs_generator 的代码如下:
-- util.lua
...

local unpack = unpack or table.unpack

...

local move_end = {}

local generator_mt = {
    __index = {
        MoveNext = function(self)
            self.Current = self.co()
            if self.Current == move_end then
                self.Current = nil
                return false
            else
                return true
            end
        end;
        Reset = function(self)
            self.co = coroutine.wrap(self.w_func)
        end
    }
}

local function cs_generator(func, ...)
    local params = {...}
    local generator = setmetatable({
        w_func = function()
            func(unpack(params))
            return move_end
        end
    }, generator_mt)
    generator:Reset()
    return generator
end

  • cs_generator 方法,创建了一个table,对table进行设置:
    • 设置 w_func 的值为一个function,对传入的func进行了包装,即执行 coroutine_test 传入的方法,并最后返回 move_end。
    • 设置table的metatable为 generator_mt 表,generator_mt 表实现了 MoveNext 、Reset方法。
    • 执行table的Reset方法,即将 w_func 使用 coroutine.wrap 创建协程。
  • 创建完成后,要将创建完成的对象传到CSharp侧开启协程,StartCoroutine 方法的实现为:
        // UnityEngine_MonoBehaviourWrap.cs
        ...
        [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
        static int _m_StartCoroutine(RealStatePtr L)
        {
		    try {
            
                ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            
            
                UnityEngine.MonoBehaviour gen_to_be_invoked = (UnityEngine.MonoBehaviour)translator.FastGetCSObj(L, 1);
            
            
			    int gen_param_count = LuaAPI.lua_gettop(L);
            
                if(gen_param_count == 2&& (LuaAPI.lua_isnil(L, 2) || LuaAPI.lua_type(L, 2) == LuaTypes.LUA_TSTRING)) 
                {
                    string _methodName = LuaAPI.lua_tostring(L, 2);
                    
                        var gen_ret = gen_to_be_invoked.StartCoroutine( _methodName );
                        translator.Push(L, gen_ret);
                    
                    
                    
                    return 1;
                }
                if(gen_param_count == 2&& translator.Assignable<System.Collections.IEnumerator>(L, 2)) 
                {
                    System.Collections.IEnumerator _routine = (System.Collections.IEnumerator)translator.GetObject(L, 2, typeof(System.Collections.IEnumerator));
                    
                        var gen_ret = gen_to_be_invoked.StartCoroutine( _routine );
                        translator.Push(L, gen_ret);
                    
                    
                    
                    return 1;
                }
                if(gen_param_count == 3&& (LuaAPI.lua_isnil(L, 2) || LuaAPI.lua_type(L, 2) == LuaTypes.LUA_TSTRING)&& translator.Assignable<object>(L, 3)) 
                {
                    string _methodName = LuaAPI.lua_tostring(L, 2);
                    object _value = translator.GetObject(L, 3, typeof(object));
                    
                        var gen_ret = gen_to_be_invoked.StartCoroutine( _methodName, _value );
                        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 UnityEngine.MonoBehaviour.StartCoroutine!");
            
        }
  • 可以看出,CSharp侧需要使用 translator.Assignable<System.Collections.IEnumerator> 方法进行类型检查,即最终会通过 ObjectCasters.genChecker 方法创建检查器。根据前面提到的,由于类型为接口类型,所以检查的条件为:
    • 栈上对象为 nil,返回true。
    • 栈上对象为 table,返回true。
    • 调用 fixTypeCheck 的检查方法。
  • 而我们通过 cs_generator 方法创建的是一个table对象,所以会通过 ObjectCasters.genChecker 方法的检查,并通过 ObjectCasters.genCaster 方法创建对象,即最后会创建 SystemCollectionsIEnumeratorBridge 对象,继承 System.Collections.IEnumerator 接口,提供给 StartCoroutine 方法执行。
  • 协程执行的时候,遇到yield会挂起,等到条件满足后,执行CSharp侧的 MoveNext 方法。此时会调用lua侧的 MoveNext 方法,调用 self.co() (由于协程是以 coroutine.wrap 创建的,直接执行协程方法相当于 coroutine.resume 方法)赋值给 Current 启动协程,继续执行后续逻辑,直到 Current 为 move_end,即协程结束。

接口(Interface)

  • 通过 ObjectCasters.genCaster 方法创建对象时,如果对象为接口,使用 fixTypeGetter 转换为null,且栈上对象是 table,则使用 ObjectTranslator.CreateInterfaceBridge 创建对象返回。 ObjectTranslator.CreateInterfaceBridge 的方法如下:
        // ObjectTranslator.cs
        ...
		public object CreateInterfaceBridge(RealStatePtr L, Type interfaceType, int idx)
        {
            Func<int, LuaEnv, LuaBase> creator;
            
            if (!interfaceBridgeCreators.TryGetValue(interfaceType, out creator))
            {
#if (UNITY_EDITOR || XLUA_GENERAL) && !NET_STANDARD_2_0
                var bridgeType = ce.EmitInterfaceImpl(interfaceType);
                creator = (int reference, LuaEnv luaenv) =>
                {
                    return Activator.CreateInstance(bridgeType, new object[] { reference, luaEnv }) as LuaBase;
                };
                interfaceBridgeCreators.Add(interfaceType, creator);
#else
                throw new InvalidCastException("This type must add to CSharpCallLua: " + interfaceType);
#endif
            }
            LuaAPI.lua_pushvalue(L, idx);
            return creator(LuaAPI.luaL_ref(L), luaEnv);
        }
  • ObjectTranslator.interfaceBridgeCreators 为接口对象的创建方法字典,注册的方式有两种:
    • 使用 [CSharpCallLua] 配置接口,使用工具生成 Bridge 脚本。在 XLua_Gen_Initer_Register__.Init 方法中,使用 ObjectTranslator.AddInterfaceBridgeCreator 注册。
      • reference 为栈上对象在 LUA_REGISTRYINDEX 表中的引用。
    • 使用 System.Reflection.Emit 动态生成。(Unity使用的.NET Standard 2.0不支持,因此如果使用.NET Standard 2.0的,接口需要预生成代码
  • 工具生成的接口对象类的相关信息为:
    • 命名为 XXXBridge,即接口名+Bridge,继承 LuaBase 和对应接口。
    • __Create:静态方法,创建一个 XXXBridge 对象,以 LuaBase 类型返回。
    • 接口的各个方法转Lua侧实现,具体流程为:
      • 获取 LUA_REGISTRYINDEX 表中的 luaEnv.errorFuncRef 对应的对象,入栈。
      • 获取 LUA_REGISTRYINDEX 表中的 reference 对应的对象,入栈。
      • 将接口名字符串入栈。
      • 调用lua侧的 xlua_pgettable 方法,尝试获取接口名对应的方法,成功则入栈。
      • 检查栈顶的对象是否为function,不是则抛异常。
      • 将 reference 对应的对象入栈,将原有的栈的对象移除。
      • 将接口需要的参数入栈。
      • 调用lua侧的 lua_pcall 方法,安全调用接口方法。
      • 还原栈顶数据。
    • XXXBridge 对象,创建时调用lua侧的 luaL_ref 方法,对栈上的table增加一次引用。由于XXXBridge 对象继承 LuaBase,因此释放的时候对应会执行一次lua侧的 lua_unref 方法,减少一次引用,保证table引用次数正确,能够正常移除。

热修复

  • 热修复是xlua最初设计的目的,为了解决用CSharp开发的项目线上无法修复bug,只能换包的问题。前面提到,LuaEnv.init_xlua 中,初始化了热更功能代码。
-- CSharp中的LuaEnv.init_xlua
...
xlua.hotfix = function(cs, field, func)
    if func == nil then func = false end
    local tbl = (type(field) == 'table') and field or {[field] = func}
    for k, v in pairs(tbl) do
        local cflag = ''
        if k == '.ctor' then
            cflag = '_c'
            k = 'ctor'
        end
        xlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least one
        local f = type(v) == 'function' and v or nil
        pcall(function()
            for i = 1, 99 do
                xlua.access(cs, cflag .. '__Hotfix'..i..'_'..k, f)
            end
        end)
    end
    xlua.private_accessible(cs)
end
  • xlua.hotfix 方法,即热修复的入口,参数含义为:
    • cs
      • CSharp类,两种表示方法,CS.Namespace.TypeName或者字符串方式"Namespace.TypeName”,字符串格式和CSharp的Type.GetType要求一致,如果是内嵌类型(Nested Type)是非Public类型的话,只能用字符串方式表示"Namespace.TypeName+NestedTypeName"。
    • field
      • 如果传的是table,则表示对应CSharp类的一组方法的替换。
      • 如果传的是string,则表示对CSharp类的某个方法的替换。
    • func
      • 如果field传的是string,func为对应要替换的方法。
  • 热修复启动的流程为:
    • 获取需要修复的table。
    • 遍历整个table,调用 xlua.access 方法,将对应的方法设置到对应变量名上(方法类型为 BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)。
      • 如果方法名为 .ctor ,即构造函数,变量名为 _c__Hotfix0_ctor 。
      • 其他方法名,变量名为 __Hotfix + 同名函数索引(从0开始)_方法名,如 __Hotfix1_Start 。(这里会尝试检查索引1~99的同名方法,如果同名方法超过100个,则无法进行修复)
    • 调用 xlua.private_accessible 方法,将CSharp类及其父类的私有对象变成可访问的(即调用 makeReflectionWrap 方法,使用 BindingFlags.NonPublic 类型进行反射注册)。
  • 而对应的CSharp侧变量,则是通过修改 “./Library/ScriptAssemblies/Assembly-CSharp.dll” 程序集,注入IL代码。
  • 注入前代码示例为:
public class TestHotfix 
{
    public TestHotfix()
    {

    }

    private int index = 0;

    void Call()
    {
        Debug.Log("Call");
    }

    void Call(int i)
    {
        Debug.Log("Call " + i);
    }

    int Get()
    {
        return index;
    }
}
  • 注入后大致如下:
// Token: 0x02000004 RID: 4
[Hotfix(HotfixFlag.Stateless)]
public class TestHotfix
{
	// Token: 0x0600000E RID: 14 RVA: 0x00002350 File Offset: 0x00000550
	public TestHotfix()
	{
		DelegateBridge c__Hotfix0_ctor = TestHotfix._c__Hotfix0_ctor;
		if (c__Hotfix0_ctor != null)
		{
			c__Hotfix0_ctor.__Gen_Delegate_Imp14(this);
		}
	}

	// Token: 0x0600000F RID: 15 RVA: 0x00002388 File Offset: 0x00000588
	private void Call()
	{
		DelegateBridge _Hotfix0_Call = TestHotfix.__Hotfix0_Call;
		if (_Hotfix0_Call != null)
		{
			_Hotfix0_Call.__Gen_Delegate_Imp14(this);
			return;
		}
		Debug.Log("Call");
	}

	// Token: 0x06000010 RID: 16 RVA: 0x000023C0 File Offset: 0x000005C0
	private void Call(int i)
	{
		DelegateBridge _Hotfix1_Call = TestHotfix.__Hotfix1_Call;
		if (_Hotfix1_Call != null)
		{
			_Hotfix1_Call.__Gen_Delegate_Imp19(this, i);
			return;
		}
		Debug.Log("Call " + i.ToString());
	}

	// Token: 0x06000011 RID: 17 RVA: 0x00002404 File Offset: 0x00000604
	private int Get()
	{
		DelegateBridge _Hotfix0_Get = TestHotfix.__Hotfix0_Get;
		if (_Hotfix0_Get != null)
		{
			return _Hotfix0_Get.__Gen_Delegate_Imp24(this);
		}
		return this.index;
	}

	// Token: 0x04000006 RID: 6
	private int index = 0;

	// Token: 0x04000007 RID: 7
	private static DelegateBridge _c__Hotfix0_ctor;

	// Token: 0x04000008 RID: 8
	private static DelegateBridge __Hotfix0_Call;

	// Token: 0x04000009 RID: 9
	private static DelegateBridge __Hotfix1_Call;

	// Token: 0x0400000A RID: 10
	private static DelegateBridge __Hotfix0_Get;
}
  • 使用IL注入代码后,会创建对应的 DelegateBridge 变量,lua侧调用 xlua.access 方法,将lua侧的方法创建 DelegateBridge 对象,赋值到CSharp侧对应的 DelegateBridge 变量上。执行方法前会先检查 DelegateBridge 对象是否为空,不为空则执行 DelegateBridge 的委托方法,即实现热修复功能。如果为空,表示不需要进行修复,继续执行CSharp侧原有的逻辑。
  • 由于 xlua.hotfix 方法只能替换原有CSharp侧的方法,有时候我们只需要增加一些执行逻辑,就不得不把CSharp侧的方法整个重新在lua侧重新实现,所以xlua又提供了另一个方法 util.hotfix_ex 方法,可以在修复方法中调用原有方法。util.hotfix_ex 的代码为:
-- util.lua
...
local function hotfix_ex(cs, field, func)
    assert(type(field) == 'string' and type(func) == 'function', 'invalid argument: #2 string needed, #3 function needed!')
    local function func_after(...)
        xlua.hotfix(cs, field, nil)
        local ret = {func(...)}
        xlua.hotfix(cs, field, func_after)
        return unpack(ret)
    end
    xlua.hotfix(cs, field, func_after)
end
  • 当使用 util.hotfix_ex 方法时,实现流程为:
    • 将CSharp类对应的方法,替换为 func_after 方法。
    • 当执行 func_after 方法时,会将CSharp类对应的方法先替换为nil,即取消CSharp侧的方法修改。
    • 执行 hotfix_ex 传入的需要替代的lua侧方法,将结果存到table中。
    • 再将CSharp类对应的方法,重新替换为 func_after 方法。
  • 因此,当执行我们替换的lua侧方法时,如果我们在其中调用CSharp侧的原有方法,由于在执行lua方法前已经移除CSharp侧的替换方法,因此会按原有方法的逻辑执行,即不需要在lua侧重写CSharp方法。
  • 注意:由于每次执行 xlua.hotfix 方法,对需要替换的每个方法都要进行100次 xlua.access 方法,尝试使用反射修改CSharp侧重载函数。而使用 util.hotfix_ex 方法替换的lua侧方法,每次执行都要进行两次 xlua.hotfix ,所以效率会比较低。

生成工具

  • (待补充)

总结

  • xLua使用起来相对比较简单,相关的文档也比较齐全,对初次使用的用户相对比较友好,容易上手。
  • 通过反射机制,提供了一套运行时动态注册CSharp侧类的方式,为使用者提供了很大的便利。对于CSharp代码占比较多的项目,也可以不用全都预生成Wrap文件,减小IL2CPP模式时的包体大小。
  • 由于所有的CSharp类都使用懒加载的模式,即使用的时候才进行注册,也避免了需要一开始注册大量CSharp类而引起的问题。
  • 委托和接口类型,还是需要提前生成,CSharp代码比较多的项目,容易出现遗漏,等到线上才发现,所以需要重点关注。
  • 由于大量使用反射,带来便利的同时,有些地方不可避免会出现装拆箱的问题,对于性能要求较高的可选择预生成代码方式解决。