由于很多旧项目是以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标识)。
- 创建 CS 表,设置metatable方法:
方法设置
- 注册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中。
- 传入注册的方法的类型idx,和入栈顺序对应上:
- 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出栈,恢复栈顶信息。
- 设置metatable的__index:
- 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为闭包。
- 设置cls_table的index:
- Utils.BeginObjectRegister:注册类实例对象相关信息。
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。
- 获取事件对应的Wrap方法
- 获取当前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 表。
- 使用 MethodWrapsCache._GenMethodWrap 方法,创建 MethodWrap 对象。
- 初始化 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。
- 设置 __index 方法:
- 将 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)。
- 非值类型对象和枚举对象,从字典中获取对象对应的id,调用lua侧的 xlua_tryget_cachedud 方法,检查 LUA_REGISTRYINDEX 表中的 ref_cacheRef 表中是否有此id,如果有则不再处理。
- 示例代码大致如下:
-- 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对象的时序图为
- CSharp访问Lua对象的时序图为
调用方法
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注册:调用CSharp侧的 UnityEngineGameObjectWrap.m_Find_xlua_st,读取栈上的参数string,并调起UnityEngine.GameObject.Find,最后再把GameObject传回lua。
- 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 的检查方法。
- 类型type不是抽象类,且是 Delegate 的父类或相同类型(IsAssignableFrom),则检查方法为:
- 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 创建委托对象返回。
- 使用 fixTypeGetter 转换,如果转换对象不为null,则返回对象。
- 类型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设置变量值,返回对象。
- 类型type为 Delegate 的父类或相同类型(IsAssignableFrom),转换方法为:
- 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,保证下一次的正常使用。
- 对反射查询到的 MemberInfo 方法,检查是否需要进行创建 OverloadMethodWrap 对象。
- MethodWrap
- MethodWrap主要有几个部分:
- methodName:方法名。
- overloads:OverloadMethodWrap列表,即同一个方法名的重载方法。
- forceCheck:是否强制检查,构造函数为true,其他默认为false。
- Call 方法:反射注册到lua侧调用的方法,主要流程为:
- 如果 overloads 只有一个方法,且没有默认参数值,且不需要强制检查,则直接调用 overloads[0].Call,即调用 OverloadMethodWrap.Call。
- 遍历 overloads 中的所有方法,调用 OverloadMethodWrap.Check 进行检查,找到第一个检查通过的方法后,执行 OverloadMethodWrap.Call 方法并返回。
- MethodWrap主要有几个部分:
- OverloadMethodWrap
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_REGISTRYINDEX 表中没有对应lua方法的引用,即没有建立对应委托关系,进行创建。
- 假设我们当前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对象创建委托。
- 根据传入的bridge,使用 DelegateBridgeBase.GetDelegateByType 方法获取。由于传入的bridge为 DelegateBridge ,即使用 DelegateBridge.GetDelegateByType 方法获取。DelegateBridge 声明为 partial 类型,具体有三个部分:
- 了解委托创建的流程后,再来看销毁的情况。前面提到,当主动调用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 方法进行获取。
- 如果栈上的对象类型为userdata,则
- 至此,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生成调用。即将栈上的对象,在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的,接口需要预生成代码)
- 使用 [CSharpCallLua] 配置接口,使用工具生成 Bridge 脚本。在 XLua_Gen_Initer_Register__.Init 方法中,使用 ObjectTranslator.AddInterfaceBridgeCreator 注册。
- 工具生成的接口对象类的相关信息为:
- 命名为 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为对应要替换的方法。
- cs
- 热修复启动的流程为:
- 获取需要修复的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代码比较多的项目,容易出现遗漏,等到线上才发现,所以需要重点关注。
- 由于大量使用反射,带来便利的同时,有些地方不可避免会出现装拆箱的问题,对于性能要求较高的可选择预生成代码方式解决。