Lohanry

宠辱不惊,绝不妄自菲薄

少年,你对力量一无所知


一个浪荡的程序猿

XLua 源码学习(一)

现在工作中经常用到XLua修复Bug或者使用Lua作为逻辑脚本,是时候学习一下源码看看如何实现这些方法的了。 那么XLua中是如何在Lua代码中调用Unity中的C#方法的呢?

Lua调用C#方法

CS.UnityEngine.Debug.Log('hello world')

CS是一个全局的Table,所以CS.UnityEngine可以当做是在一个名为CS的Table表中查询名为UnityEngine的值。 获取其值是通过CS的元方法__index来实现的。其逻辑代码在创建LuaEnv时候调用下面的代码,进行CS表的初始化。

DoString(init_xlua, "Init");

下面代码是截取了部分的init_xlua代码。

这部分描述的是_index元方法的实现。元方法__index就是CS表中访问不存在的元素时候进行的操作。 比如CS={‘A=’a’,’B’=’b’},那么在Lua中直接访问CS.A就会返回a。但是如果访问C就会因为原来表中不存在这个记录,那么而调用__index这个方法。

代码实现注释如下

init_xlua.lua
local metatable = {}
local rawget = rawget
local setmetatable = setmetatable
local import_type = xlua.import_type
local import_generic_type = xlua.import_generic_type
local load_assembly = xlua.load_assembly

function metatable:__index(key)
    --查询自己Key为'.fqn'的值,并且不触发__index元方法
    local fqn = rawget(self,'.fqn')
    --拼接'.fqn'的值和本次调用的key 
    fqn = ((fqn and fqn .. '.') or '') .. key
    --尝试查询CS类型. 
    local obj = import_type(fqn)
    if obj == nil then
        -- It might be an assembly, so we load it too
        --如果为空,有可能这个字段还是类名的一部分,那么创建一个table记录,然后缓存返回.     
        obj = { ['.fqn'] = fqn }
        setmetatable(obj, metatable)
    elseif obj == true then
        return rawget(self, key)
    end
    -- Cache this lookup
    rawset(self, key, obj)
    return obj
end
CS = CS or {}
setmetatable(CS, metatable)

Lua中获取C#类对应的Table表

XLua中有两种方式来实现Lua调用CS中的方法,一种是反射来调用,一种是生成适配的代码。

在获取对应类的Lua表时候,使用的是import_type方法,也是在创建LuaEnv实例时候进行注册的代码如下。

ObjectTranslator.cs

public void OpenLib(RealStatePtr L)
{
          if (0 != LuaAPI.xlua_getglobal(L, "xlua"))
          {
              throw new Exception("call xlua_getglobal fail!" + LuaAPI.lua_tostring(L, -1));
          }
          LuaAPI.xlua_pushasciistring(L, "import_type");
          LuaAPI.lua_pushstdcallcfunction(L,importTypeFunction);
          LuaAPI.lua_rawset(L, -3);
          ...
}

上面代码中的importTypeFunction是一个C#委托当Lua中是调用import_type时候Lua会调用对应的C方法(Lua调用CFunction的原理,请查找Lua手册),最后会调用到对应的C#委托上来。

其中xlua全局table是在C中设置的代码如下:

XLua.c

LUA_API void luaopen_xlua(lua_State *L) {
   luaL_openlibs(L);
#if LUA_VERSION_NUM == 503
   luaL_newlib(L, xlualib);
   lua_setglobal(L, "xlua");
#else
   luaL_register(L, "xlua", xlualib);
    lua_pop(L, 1);
#endif
}

代码很简单,luaopen_xlua是一个c函数,属于xlua.dll在创建LuaEnv时候会调用。 调用后会设置一个全局变量xlua,也就是ObjectTranslator类中获取的xlua变量。 然后将键值对”import_type”=C#委托,压入xlua表中。这样就能在inti_xlua.lua中调用import_type方法了。

C#中查找指定Type对应的Lua表的实现

在C#的ImportType方法中会尝试在缓存中获取对应的Type。 如果Type为空,那么说明是第一次尝试引用对应的Type,代码就会判断是时使用生成适配代码还是反射模式,来生成对应的表。

代码如下

public static int ImportType(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        //需要查询的类名
        string className = LuaAPI.lua_tostring(L, 1);
        //查找C#对应的Type(此处还没去查找对应Lua的表)
        Type type = translator.FindType(className);
        if (type != null)
        {
            //这句查找Lua中Type对应的表
            if (translator.GetTypeId(L, type) >= 0)
            {
                LuaAPI.lua_pushboolean(L, true);
            }
            else
            {
                return LuaAPI.luaL_error(L, "can not load type " + type);
            }
        }
        else
        {
            LuaAPI.lua_pushnil(L);
        }
        return 1;
    }
}

internal int getTypeId(RealStatePtr L, Type type, out bool is_first, LOGLEVEL log_level = LOGLEVEL.WARN)
{
    int type_id;
    is_first = false;
    //查询是否缓存中有Type对应的Lua表,有就直接返回
    if (!typeIdMap.TryGetValue(type, out type_id)) // no reference
    {
        ...
        is_first = true;
        Type alias_type = null;
        aliasCfg.TryGetValue(type, out alias_type);
        LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
        if (LuaAPI.lua_isnil(L, -1)) //no meta yet, try to use reflection meta
        {
            LuaAPI.lua_pop(L, 1);
            //此处会去检查是使用反射还是生成适配代码的逻辑
            if (TryDelayWrapLoader(L, alias_type == null ? type : alias_type))
            {
                LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
            }
            else
            {
                throw new Exception("Fatal: can not load metatable of type:" + type);
            }
        }
        //循环依赖,自身依赖自己的class,比如有个自身类型的静态readonly对象。
        if (typeIdMap.TryGetValue(type, out type_id))
        {
            LuaAPI.lua_pop(L, 1);
        }
        else
        {
            ...
            LuaAPI.lua_pushvalue(L, -1);
            type_id = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);
            LuaAPI.lua_pushnumber(L, type_id);
            LuaAPI.xlua_rawseti(L, -2, 1);
            LuaAPI.lua_pop(L, 1);
            ...
            //缓存type与其对应到lua中的表
            typeIdMap.Add(type, type_id);
        }
    }
    return type_id;
}

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);
            //此处如果已经缓存,那么就是生成适配代码注册,
            //这边的逻辑也是为了用的时候才实例化对应的.
            //这个delayWrap是个字典,他的键值对在XLua_Gen_Initer_Register__类实例化时候自动填充
            if (delayWrap.TryGetValue(type, out loader))
            {
                delayWrap.Remove(type);
                //将类方法,字段,成员等加载上来
                loader(L);
            }
            //那么这里就是反射的逻辑了
            else
            {
                 ...
                //用反射将类方法,字段,成员等加载上来
                Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
                ...
            }
            ...
            ...
            return true;
}

在生成完Type对应的Lua表后还需要设置到Lua上去

下面的代码简单来说就是用前面代码生成的table表设置到CS.UnityEngine[Debug]中

//loader(L)和Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));中都会调用此函数来设置CS.UnityEngine[Debug]
public static void SetCSTable(RealStatePtr L, Type type, int cls_table)
{
   int oldTop = LuaAPI.lua_gettop(L);
   cls_table = abs_idx(oldTop, cls_table);
   LuaAPI.xlua_pushasciistring(L, LuaEnv.CSHARP_NAMESPACE);
   LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
   List<string> path = getPathOfType(type);
   for (int i = 0; i < path.Count - 1; ++i)
   {
      LuaAPI.xlua_pushasciistring(L, path[i]);
      if (0 != LuaAPI.xlua_pgettable(L, -2))
      {
         LuaAPI.lua_settop(L, oldTop);
         throw new Exception("SetCSTable for [" + type + "] error: " + LuaAPI.lua_tostring(L, -1));
      }
      if (LuaAPI.lua_isnil(L, -1))
      {
         LuaAPI.lua_pop(L, 1);
         LuaAPI.lua_createtable(L, 0, 0);
         LuaAPI.xlua_pushasciistring(L, path[i]);
         LuaAPI.lua_pushvalue(L, -2);
         LuaAPI.lua_rawset(L, -4);
      }
      else if (!LuaAPI.lua_istable(L, -1))
      {
         LuaAPI.lua_settop(L, oldTop);
         throw new Exception("SetCSTable for [" + type + "] error: ancestors is not a table!");
      }
      LuaAPI.lua_remove(L, -2);
   }
   LuaAPI.xlua_pushasciistring(L, path[path.Count - 1]);
   LuaAPI.lua_pushvalue(L, cls_table);
   LuaAPI.lua_rawset(L, -3);
   LuaAPI.lua_pop(L, 1);
   LuaAPI.xlua_pushasciistring(L, LuaEnv.CSHARP_NAMESPACE);
   LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
   ObjectTranslatorPool.Instance.Find(L).PushAny(L, type);
   LuaAPI.lua_pushvalue(L, cls_table);
   LuaAPI.lua_rawset(L, -3);
   LuaAPI.lua_pop(L, 1);
}

现在可以看到在调用到CS.UnityEngine.Debug时候,我们在Lua中已经获取到了这个类对应Lua Table了。 那么接下来调用CS.UnityEngine.Debug.Log(“hello world”),大提上与之前的获取Type类是一致的。 不过要注意的是调用static的方法字段和对象的方法字段使用的是不同的table。这次文章都讨论的是静态方法的调用.

调用指定的C#方法 上面的已经提到XLua中有两种方式来实现Lua调用CS中的方法,一种是反射来调用,一种是生成适配的代码.

使用生成适配代码调用

在XLua中生成适配代码后会在Gen目录生成代码如下

UnityEngineDebugWrap.cs
public class UnityEngineDebugWrap
{
     public static void __Register(RealStatePtr L)
     {
          ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
          System.Type type = typeof(UnityEngine.Debug);
          //注册成员方法等
          Utils.BeginObjectRegister(type, L, translator, 0, 0, 0, 0);
          Utils.EndObjectRegister(type, L, translator, null, null,null, null, null);
          //注册类方法等即Static
          Utils.BeginClassRegister(type, L, __CreateInstance, 17, 3, 1);
          ...
          //注册一个名为Log的回调
          Utils.CLS_IDX(L, Utils.CLS_IDX, "Log", _m_Log_xlua_st_);
          ...
          Utils.EndClassRegister(type, L, translator);
     }
}


[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_Log_xlua_st_(RealStatePtr L)
{
    //根据Log方法的参数数量来生成各种调用
    try {
    	ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        int gen_param_count = LuaAPI.lua_gettop(L);
        if(gen_param_count == 1&& translator.Assignable<object>(L, 1))
        {
            object _message = translator.GetObject(L, 1, typeof(object));
            UnityEngine.Debug.Log( _message );
            return 0;
        }
        if(gen_param_count == 2&& translator.Assignable<object>(L, 1)&& translator.Assignable<UnityEngine.Object>(L, 2))
        {
            object _message = translator.GetObject(L, 1, typeof(object));
            UnityEngine.Object _context = (UnityEngine.Object)translator.GetObject(L, 2, typeof(UnityEngine.Object));
            UnityEngine.Debug.Log( _message, _context );
            return 0;
		}
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return LuaAPI.luaL_error(L, "invalid arguments to UnityEngine.Debug.Log!");
}
//注册代码如下
Utils.cs
public static void RegisterFunc(RealStatePtr L, int idx, string name, LuaCSFunction func)
{
  //这里的idx指的是就是CLS_IDX,就是cls_table,也就是SetCSTable设置的表
   idx = abs_idx(LuaAPI.lua_gettop(L), idx);
   //压入方法名
   LuaAPI.xlua_pushasciistring(L, name);
   //压入C#委托指针
   LuaAPI.lua_pushstdcallcfunction(L, func);
   LuaAPI.lua_rawset(L, idx);
}

使用反射式调用

static int FixCSFunction(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        //这边获取闭包中的upvalue值
        int idx = LuaAPI.xlua_tointeger(L, LuaAPI.xlua_upvalueindex(1));
        //GetFixCSFunction很简单就是return fix_cs_functions[index]; 
        //fix_cs_functions这个是在PushFixCSFunction时候添加的,PushFixCSFunction是在之前ReflectionWrap中调用的
        LuaCSFunction func = (LuaCSFunction)translator.GetFixCSFunction(idx);
        return func(L);
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in FixCSFunction:" + e);
    }
}

那么到现在为止所有代码设置都已经完成,就差调用了。

当DoString到CS.UnityEngine.Debug.Log(“hello world”)时候,先从CS.UnityEngine.Debug[Log]获取到对应的value, 在lua中这个值是一个function,那么就执行call,压入参数然后就开始调用了。 如果是生成是适配代码的方式的话其对应的C#委托就是 m_Log_xlua_st(RealStatePtr L)了。 但是如果是反射式调用的话,其对应的C#委托永远都是StaticLuaCallbacks.FixCSFunctionWraper这个委托,就是上面代码的FixCSFunction。 当调用FixCSFunction后会从中取出upvalue,这个值是一个数字,是个索引。索引的是之前生成wrap时候缓存的方法。然后直接进行调用。

到此整一个调用就结束了.

最近的文章

XLua 源码学习(二)

在XLua源码学习(一)中讲的主要是Lua对C#静态类的调用方法,本次将会讲到的是对对象方法的调用。注意看代码中的注释,精华都在注释中。类的静态字段方法实现在XLua中对类的静态字段属性方法调用都是归属于对cls_table的映射操作。在生成适配代码的模式中,XLua将一个类的静态方法名都作为一个key存储在cls_table中,而对应的值是一个委托,这个委托将会调用到C#中对应的字段或者方法上,并且这个委托都是会在XLua的GenWrap代码阶段生成。但是在反射模式中XLua也是将一...…

继续阅读
更早的文章

Mono中的BOEHM GC 原理学习(1)

现在工作主要是游戏方面,游戏开发就必然绕不开游戏引擎,自己使用的是Unity的引擎,Unity引擎使用可以使用的语言也有多种,本人使用C# 而且跑在Mono-Runtime 上。Mono和Unity的关系不在赘述,本文默认你有C#的编写基础,跨平台语言基础。什么是GC GC 全称 Garbage Collection。也叫垃圾回收。在我们写C#或是Java代码时候我们并不关心一个对象在内存中的创建和释放,我们只是通过一个关键词new 来实例化出一个对象来,至于在内存中是如何被分配是不关心...…

继续阅读