maltadirk 发表于 2022-3-18 07:32

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("Fatal Exception! Array Metatable not inited!");
            return common_array_meta;
      }
      //如果是委托,就返回common_delegate_meta,也是在Openlib的时候被赋值
      if (typeof(MulticastDelegate).IsAssignableFrom(type))
      {
            if (common_delegate_meta == -1) throw new Exception("Fatal Exception! Delegate Metatable not inited!");
            TryDelayWrapLoader(L, type);
            return common_delegate_meta;
      }

      is_first = true;
      Type alias_type = null;
      aliasCfg.TryGetValue(type, out alias_type);
      //在注册表中获取该类型对应的元表,在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("Fatal: can not load metatable of type:" + type);
            }
      }

      //循环依赖,自身依赖自己的class,比如有个自身类型的静态readonly对象。
      if (typeIdMap.TryGetValue(type, out type_id))
      {
            LuaAPI.lua_pop(L, 1);
      }
      else
      {
            //枚举类型加入一些元方法
            if (type.IsEnum())
            {
                LuaAPI.xlua_pushasciistring(L, "__band");
                LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumAndMeta);
                LuaAPI.lua_rawset(L, -3);
                LuaAPI.xlua_pushasciistring(L, "__bor");
                LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumOrMeta);
                LuaAPI.lua_rawset(L, -3);
            }
            //迭代器类型加入__pairs元方法   
            if (typeof(IEnumerable).IsAssignableFrom(type))
            {
                LuaAPI.xlua_pushasciistring(L, "__pairs");
                LuaAPI.lua_getref(L, enumerable_pairs_func);
                LuaAPI.lua_rawset(L, -3);
            }            
            //上述代码给该类型对应的元表加入一些元方法

            //把该类型对应的元表在复制一份入栈
            LuaAPI.lua_pushvalue(L, -1);
            //type_id持有这个副本的引用
            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("__Register", BindingFlags.Static | BindingFlags.Public);
            method.Invoke(null, new object[] { L });
      }
      else
      {
            Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
      }
#else
      Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
#endif
#if NOT_GEN_WARNING
      if (!typeof(Delegate).IsAssignableFrom(type))
      {
#if !XLUA_GENERAL
            UnityEngine.Debug.LogWarning(string.Format("{0} not gen, using reflection instead", type));
#else
            System.Console.WriteLine(string.Format("Warning: {0} not gen, using reflection instead", type));
#endif
      }
#endif
    }
    if (top != LuaAPI.lua_gettop(L))
    {
      throw new Exception("top change, before:" + top + ", after:" + LuaAPI.lua_gettop(L));
    }
   
    //遍历类中的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, "__mode");
      LuaAPI.xlua_pushascistring(L, "v");
      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, "__mode");
      LuaAPI.xlua_pushasciistring(L, "v");      //设置成值弱引用表
      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,"__gc");        //在L栈中压入"__gc"
    LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.GcMeta); //压入GC方法
    LuaAPI.lua_rawset(L,-3); //table["__gc"] = 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,"__gc");
    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, "__gc");
        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, "c# exception in LuaGC:" + 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, "Init")初始化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]
查看完整版本: XLua框架原理(二)