XLua框架原理(二)
上一篇介绍了Lua访问c#对象的实现原理,并且提出了一个问题——c#对象是如何传递给Lua的?我们都知道c#是强类型语言,而lua只有8种类型,显然lua中是无法识别c#对象的,如果我们在lua中打印c#对象,会发现其类型是userData,可以看出lua中持有c#对象的关键就是这个userData,接下来本文将会详细说明这其中的实现原理。
首先还是以上一篇结尾处的_m_GetComponent为例,看到代码末尾有translator.Push(L, gen_ret)这一行代码,这行代码正是关键所在
static int _m_GetComponent(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));
UnityEngine.Component gen_ret = gen_to_be_invoked.GetComponent( _type );
translator.Push(L, gen_ret);//重点!!
//返回一个参数
return 1;
}
//.....
}
}
ObjectTranslator.Push()
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
index = addObject(o, is_valuetype, is_enum);
//通过index把对象保存到注册表中,并且给该对象设置元表
LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
}
Push方法首先判断是否是需要缓存,如果是引用类型或枚举都是需要缓存的,枚举类型缓存到enumMap中,引用类型缓存到reverseMap中,如果已经缓存过了,则在缓存中获取。
如果没缓存过,就会调用getTypeId为每个类型都在注册表中生成一个type_id。重点所在,后面会详细介绍。
然后再判断是否已经缓存过了,如果依然没有缓存过,那么就会把obj保存到ObjectPool中,并且根据类型缓存到enumMap或reverseMap中,最后返回obj在ObjectPool中的索引。也就是说,这个Push函数真正会做的是,把c#对象缓存到ObjectPool中并且记录对应的索引。
然后再调用LuaAPI.xlua_pushcsobj(),在这个方法内部会给传入的index创建userData并且入栈。如果需要缓存的话,则会进行注册表 = userData这一步缓存操作,最后再通过type_id找到该类型的元表,之后再把该元表设置成userData的元表。
总结来说就是Push()方法会给传入的对象o创建userData,并且把该类型对应的元表设置为userData的元表,最后把userData入栈。_m_GetComponent方法return 1;代表返回一个参数,也就是userData这个对象的引用地址。这也就回答了开篇的那个问题。在Lua中,持有的并不是c#对象,而是c#对象在c#缓存池中对应的index所引用的那片地址。
下面看对应的代码
ObjectTranslator.getTypeId()
internal int getTypeId(RealStatePtr L, Type type, out bool is_first, LOGLEVEL log_level = LOGLEVEL.WARN)
{
int type_id;
is_first = false;
if (!typeIdMap.TryGetValue(type, out type_id)) // no reference
{
//如果是数组,就返回common_array_meta,这个值在Openlib的时候被赋值
if (type.IsArray)
{
if (common_array_meta == -1) throw new Exception(&#34;Fatal Exception! Array Metatable not inited!&#34;);
return common_array_meta;
}
//如果是委托,就返回common_delegate_meta,也是在Openlib的时候被赋值
if (typeof(MulticastDelegate).IsAssignableFrom(type))
{
if (common_delegate_meta == -1) throw new Exception(&#34;Fatal Exception! Delegate Metatable not inited!&#34;);
TryDelayWrapLoader(L, type);
return common_delegate_meta;
}
is_first = true;
Type alias_type = null;
aliasCfg.TryGetValue(type, out alias_type);
//在注册表中获取该类型对应的元表,在Utils.BeginObjectRegister中会为type创建元表
LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
if (LuaAPI.lua_isnil(L, -1)) //no meta yet, try to use reflection meta
{
//将上面压入栈的空表弹出
LuaAPI.lua_pop(L, 1);
//试图为该类型生成wrap文件并完成相关注册
if (TryDelayWrapLoader(L, alias_type == null ? type : alias_type))
{
//在注册表中拿到该类对应的元表压入栈
LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
}
else
{
throw new Exception(&#34;Fatal: can not load metatable of type:&#34; + type);
}
}
//循环依赖,自身依赖自己的class,比如有个自身类型的静态readonly对象。
if (typeIdMap.TryGetValue(type, out type_id))
{
LuaAPI.lua_pop(L, 1);
}
else
{
//枚举类型加入一些元方法
if (type.IsEnum())
{
LuaAPI.xlua_pushasciistring(L, &#34;__band&#34;);
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumAndMeta);
LuaAPI.lua_rawset(L, -3);
LuaAPI.xlua_pushasciistring(L, &#34;__bor&#34;);
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumOrMeta);
LuaAPI.lua_rawset(L, -3);
}
//迭代器类型加入__pairs元方法
if (typeof(IEnumerable).IsAssignableFrom(type))
{
LuaAPI.xlua_pushasciistring(L, &#34;__pairs&#34;);
LuaAPI.lua_getref(L, enumerable_pairs_func);
LuaAPI.lua_rawset(L, -3);
}
//上述代码给该类型对应的元表加入一些元方法
//把该类型对应的元表在复制一份入栈
LuaAPI.lua_pushvalue(L, -1);
//type_id持有这个副本的引用
type_id = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);
LuaAPI.lua_pushnumber(L, type_id); //type_id入栈
LuaAPI.xlua_rawseti(L, -2, 1); //metatable = type_id,然后type_id出栈
LuaAPI.lua_pop(L, 1); //metatable出栈
//缓存
if (type.IsValueType())
{
typeMap.Add(type_id, type);
}
typeIdMap.Add(type, type_id);
}
}
return type_id;
}
注释中解释的很清楚了,这个getTypeId方法首先判断有没有缓存,有直接返回,没有判断是否是数组或委托,是返回预定义的id,否则继续。
重点是LuaAPI.luaL_getmetatable(L, alias_type ==null? type.FullName : alias_type.FullName)这一行代码。会在注册表中获取类型对应的元表,如果还未创建过类型的元表,证明这个类还未生成过wrap文件,上一篇介绍过Wrap文件中会调用__Register方法,为类型在注册表中创建对应的元表。如果还没生成过元表,就会调用TryDelayWrapLoader()试图为该类型创建元表(具体实现下面马上介绍)。创建完对应的元表之后又进行了一次typeIdMap.TryGetValue(type,out type_id)判断,说实话我也没太看懂。如果判断条件不满足,则给该类型的元表添加元方法,最后把该类型原表在注册表中的引用保存到type_id中,这也是getTypeId()方法的核心功能——获取到某个类型在注册表中对应的元表。最后把该引用缓存起来。
ObjectTranslator.TryDelayWrapLoader()
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
{
//避免重复添加
if (loaded_types.ContainsKey(type)) return true;
loaded_types.Add(type, true);
LuaAPI.luaL_newmetatable(L, type.FullName); //先建一个metatable,因为加载过程可能会需要用到
LuaAPI.lua_pop(L, 1);
Action<RealStatePtr> loader;
int top = LuaAPI.lua_gettop(L);
//自定义warp生成1器
//delayWrap在LuaEnv初始化的时候,会调用相关的方法,把一些常用的类的wrap文件生成器加载进去
if (delayWrap.TryGetValue(type, out loader))
{
delayWrap.Remove(type);
loader(L);
}
else
{
#if !GEN_CODE_MINIMIZE && !ENABLE_IL2CPP && (UNITY_EDITOR || XLUA_GENERAL) && !FORCE_REFLECTION && !NET_STANDARD_2_0
if (!DelegateBridge.Gen_Flag && !type.IsEnum() && !typeof(Delegate).IsAssignableFrom(type) && Utils.IsPublic(type))
{
//非委托,枚举,委托的派生类,并且是公用类才可以生成wrap文件,并且调用__Register进行注册,否则使用下面反射的办法生成对应的warp文件
Type wrap = ce.EmitTypeWrap(type);
MethodInfo method = wrap.GetMethod(&#34;__Register&#34;, BindingFlags.Static | BindingFlags.Public);
method.Invoke(null, new object[] { L });
}
else
{
Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
}
#else
Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
#endif
#if NOT_GEN_WARNING
if (!typeof(Delegate).IsAssignableFrom(type))
{
#if !XLUA_GENERAL
UnityEngine.Debug.LogWarning(string.Format(&#34;{0} not gen, using reflection instead&#34;, type));
#else
System.Console.WriteLine(string.Format(&#34;Warning: {0} not gen, using reflection instead&#34;, type));
#endif
}
#endif
}
if (top != LuaAPI.lua_gettop(L))
{
throw new Exception(&#34;top change, before:&#34; + top + &#34;, after:&#34; + LuaAPI.lua_gettop(L));
}
//遍历类中的public嵌套类
foreach (var nested_type in type.GetNestedTypes(BindingFlags.Public))
{
if (nested_type.IsGenericTypeDefinition())
{
continue;
}
//获取相关嵌套类
GetTypeId(L, nested_type);
}
return true;
}
注释写的很清楚了,首先判断是否注册过,然后判断是否有自定义warp文件生成器,然后判断是使用哪种生成Wrap的方式生成Wrap文件,并且进行相关的注册,具体实现之前的文章中已经介绍过了,最后再遍历嵌套类,把嵌套类再走一次getTypeId()方法,把类型对应的元表缓存起来。
ObjectTranslator.Push()方法经过上述步骤之后,只剩下最重要的两行代码了,addObject()方法做的事情比较简单,就是把o缓存到ObjectPool中,并且返回在缓存池中的索引index,然后把对象o和index缓存到enumMap或reverseMap中。
index = addObject(o, is_valuetype, is_enum);
LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
static void cacheud(lua_State *L, int key, int cache_ref) {
lua_rawgeti(L, LUA_REGISTRYINDEX, cache_ref);
lua_pushvalue(L, -2);
lua_rawseti(L, -2, key);
lua_pop(L, 1);
}
//key 是对象o在ObjectPool中的index
// meta_ref是注册表中类型的元表
// cache_ref是注册表中的缓存表,这个缓存表是弱引用表
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
int* pointer = (int*)lua_newuserdata(L, sizeof(int));
*pointer = key;//创建userData,并且把key赋值给该userData
if (need_cache) cacheud(L, key, cache_ref);//缓存到注册表中的缓存表
lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
lua_setmetatable(L, -2);//把该类型对应的元表设置为userData的元表
}注释中写的很清楚了,值得注意的一个地方是cache_ref对应的是注册表中的缓存表,这个表是个弱引用表,这个表是在ObjectTranslator的构造函数里面注册的,如下所示
public ObjectTranslator(LuaEnv luaenv, RealStatePrt L)
{
// ...
LuaAPI.lua_newtable(L);
LuaAPI.lua_newtable(L);
LuaAPI.xlua_pushasciistring(L, &#34;__mode&#34;);
LuaAPI.xlua_pushascistring(L, &#34;v&#34;);
LuaAPI.lua_rawset(L, -3);
LuaAPI.lua_setmetatable(L, -2);
cacheRef = LuaAPI.lua_ref(L, LuaIndexes.LUA_REGISTRYINDEX);
// ...
}
至此,在Lua中访问c#对象和Lua中是如何持有c#对象这两个问题就已经彻底介绍完毕了,接下来还需要思考一个问题——如果lua中已经不再使用某个c#对象了,如何通知到c#这边把ObjectPool中将该缓存释放?
LuaGC
首先我们知道,注册表是弱引用表。在Lua中如果某个对象没有被引用的话,那么在执行Lua的gc方法时这个对象就会被回收,而这个对象被回收的时候,就会调用对象的__gc元方法。
//ObjectTranslator.cs
public ObjectTranslator(LuaEnv luaenv, RealStatePtr L)
{
// ......
LuaAPI.lua_newtable(L);
LuaAPI.lua_newtable(L);
LuaAPI.xlua_pushasciistring(L, &#34;__mode&#34;);
LuaAPI.xlua_pushasciistring(L, &#34;v&#34;); //设置成值弱引用表
LuaAPI.lua_rawset(L, -3);
LuaAPI.lua_setmetatable(L, -2);
cacheRef = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);
}
上述代码创建注册表,并将其设置为值弱引用表
//ObjectTranslator
internal void createFunctionMetatable(RealStatePtr L)
{
LuaAPI.lua_newtable(L);
LuaAPI.xlua_pushasciistring(L,&#34;__gc&#34;); //在L栈中压入&#34;__gc&#34;
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.GcMeta); //压入GC方法
LuaAPI.lua_rawset(L,-3); //table[&#34;__gc&#34;] = metaFunctions.GcMeta,然后将__gc和GC方法出栈
LuaAPI.lua_pushlightuserdata(L, LuaAPI.xlua_tag());
LuaAPI.lua_pushnumber(L, 1);
LuaAPI.lua_rawset(L, -3);
LuaAPI.lua_pushvalue(L, -1);
int type_id = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);
LuaAPI.lua_pushnumber(L, type_id);
LuaAPI.xlua_rawseti(L, -2, 1);
LuaAPI.lua_pop(L, 1);
typeIdMap.Add(typeof(LuaCSFunction), type_id); //把类型LuaCSFunction保存到typeIdMap中
}
//Utils.cs
public static void ReflectionWrap(RealStatePtr L, Type type, bool privateAccessible)
{
// ......
//反射法为类型生成代码的时候,会给某个对象加上__gc元方法对应的回调
LuaAPI.xlua_pushasciistring(L,&#34;__gc&#34;);
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.GcMeta);
LuaAPI.lua_rawset(L, obj_meta);
}
public static void BeginObjectRegister(Type type, RealStatePtr L, ObjectTranslator translator, int meta_count,
int method_count, int getter_count,int setter_count, int type_id = -1)
{
// ......
if ((type == null || !translator.HasCustomOp(type)) && type != typeof(decimal)
{
//同理
LuaAPI.xlua_pushasciistring(L, &#34;__gc&#34;);
LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta);
LuaAPI.lua_rawset(L, -3);
}
}
上述代码说明在给某个对象生成代码的时候,会设置该对象的__gc元方法。那么下面来看看c#这边对应的回调方法又做了些什么操作。
//StaticLuaCallbacks.cs
public static int LuaGC(RealStatePtr L)
{
try
{
//获取对象对应的udata
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, &#34;c# exception in LuaGC:&#34; + e);
}
}
//ObjectTranslator.cs
internal void collectObject(int obj_index_to_collect)
{
object o;
//从ObjectPool获取对应的object
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);
}
}
}
}
}
这一部分代码还是比较简单的,相信都能看的明白。
总结一下就是——当我们在Lua中不在引用某个对象的时候,因为注册表是弱引用表的关系,Lua内部调用gc的时候会把不再使用的对象回收掉并且调用对象的__gc元方法。
因为这个对象在生成代码的时候,会把StaticLuaCallbacks.LuaGC()设置其__gc元方法对应的回调,所以对象在Lua被回收的时候就会回调到c#这边的方法。
因为c#对象的实体是保存在c#的ObjectPool中的,只是把对象的地址(userdata, 这么说会好理解一点)传给了Lua,所以当Lua回收某个对象的时候,在LuaGC方法中,可以通过userdata拿到对应的c#对象实体本身然后再把该对象清除。
以上就解释清楚了c#对象的释放原理。
总结
最后再总结一下整个XLua框架的大体流程。
[*]LuaEnv()会初始化Lua虚拟机,创建注册表,并且在注册表中加入许多元方法
[*]调用DoString(init_xlua, &#34;Init&#34;)初始化Lua创建CS表
[*]XXXX.__Register方法为某个类型生成代码
[*]ObjectTranslator.Push()方法把c#对象缓存起来,并且为该对象绑定一个udata传递到Lua中
[*]当Lua中不再使用某个udata的时候,通过__gc回调方法把udata对应的c#对象释放掉。
结语
到此为止,整个XLua的框架也就大体介绍完毕了,当然还有很多细节没有介绍,有兴趣的可以再自行深入研究。有什么疑问或写错的地方,也欢迎在评论区指出。
参考
[*]^看懂Xlua实现原理——从宏观到微观(1)传递c#对象到Luahttps://zhuanlan.zhihu.com/p/146377267
[*]^XLua 源码学习原理(一)https://zhuanlan.zhihu.com/p/68406928
[*]^深入xLua实现原理之C#如何调用Lua https://www.cnblogs.com/iwiniwin/p/15323970.html
页:
[1]