找回密码
 立即注册
查看: 495|回复: 0

xlua 代码分析

[复制链接]
发表于 2021-8-10 15:08 | 显示全部楼层 |阅读模式
五一开始了,正好趁这段时间,挺闲的。整理一下xlua的原理,之前一直在lua来做热更新,觉得始终是一个API的搬运工,没有深入的了解过基础的原理。准备今天深入代码层面研究一下,而不是停留在怎么用,而是深入研究为什么会这样。
首先,先介绍一下xlua吧,用Unity开发的人对他应该挺熟悉的。对于热更新方案,现在主流方案主要是各种lua方案(tolua, xlua 等等),还有就是ILRuntime,基本就这两种吧。这里就挑选我们项目中的用到的xlua简单介绍一下,不敢说深入,怕水平不够,如果发现有什么我理解错误的地方,欢迎文章下面评论指正,不胜感激,共同进步。
下面,开始介绍吧。
xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。
xLua的突破

xLua在功能、性能、易用性都有不少突破,这几方面分别最具代表性的是:
    可以运行时把C#实现(方法,操作符,属性,事件等等)替换成lua实现;出色的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;编辑器下无需生成代码,开发更轻量;
上面这些说明来自xlua的github上面的介绍,好像有点泛泛。
快十天过去了,终于想到过来补充完这篇文章,总不能半途而废。
再看这边问问文章之前,你必须有一些基础的知识,我现在列举了两个文章,最好先看一下,
上面这篇文章,主要介绍了Lua和C的交互,谢谢这位的介绍,感觉补充了很多知识盲区,Lua并不是我想起来的那么简单。如果你想看懂xLua源码的话,必须要熟悉Lua和C语言之间的交付,因为C#都是通过调用C语言库来实现和Lua之间的交付的。


先介绍一下xLua项目的一些结构,主要介绍一下,我看源码过程中比较重要的几个文件夹。
一个比较重要的文件夹
这个文件夹中除了一些编译库文件需要的文件之外,有一个非常重要文件,xlua.c 主要是这个文件承担了C#和Lua之间的交互。要看实现的话,可以看一下这个文件。
这个文件夹中主要是一些辅助的功能,后面介绍到的Hotfix实现,需要这里面的这个工程来辅实现。看文件夹的名字好像还有一个帮助检查Lua内存泄漏的东东,这个东东这篇文章不做深入介绍(ps.其实我自己也没有看过,找个时间过来看一看)。
接下来介绍一下C#中,一些前期比较重要的一些东西。Asset下面的LuaEnv中,隐藏这一个终局大Boss。他既然是一个string变量,藏的好深。
private string init_xlua = @"
            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)
                local fqn = rawget(self,'.fqn')
                fqn = ((fqn and fqn .. '.') or '') .. key

                local obj = import_type(fqn)

                if obj == nil then
                    -- It might be an assembly, so we load it too.
                    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

            function metatable:__newindex()
                error('No such type: ' .. rawget(self,'.fqn'), 2)
            end

            -- A non-type has been called; e.g. foo = System.Foo()
            function metatable:__call(...)
                local n = select('#', ...)
                local fqn = rawget(self,'.fqn')
                if n > 0 then
                    local gt = import_generic_type(fqn, ...)
                    if gt then
                        return rawget(CS, gt)
                    end
                end
                error('No such type: ' .. fqn, 2)
            end

            CS = CS or {}
            setmetatable(CS, metatable)

            typeof = function(t) return t.UnderlyingSystemType end
            cast = xlua.cast
            if not setfenv or not getfenv then
                local function getfunction(level)
                    local info = debug.getinfo(level + 1, 'f')
                    return info and info.func
                end

                function setfenv(fn, env)
                  if type(fn) == 'number' then fn = getfunction(fn + 1) end
                  local i = 1
                  while true do
                    local name = debug.getupvalue(fn, i)
                    if name == '_ENV' then
                      debug.upvaluejoin(fn, i, (function()
                        return env
                      end), 1)
                      break
                    elseif not name then
                      break
                    end

                    i = i + 1
                  end

                  return fn
                end

                function getfenv(fn)
                  if type(fn) == 'number' then fn = getfunction(fn + 1) end
                  local i = 1
                  while true do
                    local name, val = debug.getupvalue(fn, i)
                    if name == '_ENV' then
                      return val
                    elseif not name then
                      break
                    end
                    i = i + 1
                  end
                end
            end

            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
                    local f = type(v) == 'function' and v or nil
                    xlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least one
                    pcall(function()
                        for i = 1, 99 do
                            xlua.access(cs, cflag .. '__Hotfix'..i..'_'..k, f)
                        end
                    end)
                end
                xlua.private_accessible(cs)
            end
            xlua.getmetatable = function(cs)
                return xlua.metatable_operation(cs)
            end
            xlua.setmetatable = function(cs, mt)
                return xlua.metatable_operation(cs, mt)
            end
            xlua.setclass = function(parent, name, impl)
                impl.UnderlyingSystemType = parent[name].UnderlyingSystemType
                rawset(parent, name, impl)
            end
            
            local base_mt = {
                __index = function(t, k)
                    local csobj = t['__csobj']
                    local func = csobj['<>xLuaBaseProxy_'..k]
                    return function(_, ...)
                         return func(csobj, ...)
                    end
                end
            }
            base = function(csobj)
                return setmetatable({__csobj = csobj}, base_mt)
            end
            ";
这个变量太重要了,我认为它是这个项目的核心部分,藏在了这么不起眼的地方。
下面开始讲一下我理解的Lua调用C#流程,中间省略不说调用C语言库。
比如下面的这样一条语句,是怎么调用到C#的。
CS.UnityEngine.Debug.Log('hello world')先来看一下C#中LuaEnv这个脚本,
LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print);
if (0 != LuaAPI.xlua_setglobal(rawL, "print"))
{
      throw new Exception("call xlua_setglobal fail!");
}
Lua里面的print方法就是通过这几行代码实现的,你会发现最终Lua里面的print调用的是引擎里面的一个最普通的Debug.Log();
然后是通过AddSearcher方法注册一些搜索Lua脚本的方法,一般我们用的时候需要自定义扩展一个。
DoString(init_xlua, "Init");通过这行代码来Load init_xlua这个字符串中的Lua代码。
你会发现定义了一个叫CS的全局表,现在你们知道为什么Lua调用C#的时候需要以CS开头了吧。
接下来我们关注一下,这个CS表的元表,
function metatable:__index(key)
                local fqn = rawget(self,'.fqn')
                fqn = ((fqn and fqn .. '.') or '') .. key

                local obj = import_type(fqn)

                if obj == nil then
                    -- It might be an assembly, so we load it too.
                    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它是通过import_type这个方法来实现新值的载入,接下来我们来看看这个import_type方法具体是怎么实现的。
在ObjectTranslator.cs中,我截一段代码我们来看看这个函数的实现。
        [MonoPInvokeCallback(typeof(LuaCSFunction))]
        public static int ImportType(RealStatePtr L)
        {
            try
            {
                ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
                string className = LuaAPI.lua_tostring(L, 1);
                Type type = translator.FindType(className);
                if (type != null)
                {
                    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;
            }
            catch (System.Exception e)
            {
                return LuaAPI.luaL_error(L, "c# exception in xlua.import_type:" + e);
            }
        }
最后会走到这个接口中
        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);
            
            // 这一块主要用来生成代码的一些类型的处理
            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))
                {
                    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));
            }

            foreach (var nested_type in type.GetNestedTypes(BindingFlags.Public))
            {
                if (nested_type.IsGenericTypeDefinition())
                {
                    continue;
                }
                TryDelayWrapLoader(L, nested_type);
            }
            
            return true;
        }
我们走到运用反射的接口看看实际的情况。
这个接口主要创建了两个元表,这个其中涉及了Lua中的注册表和upvalue的概念,不太清楚的同学,拿一本教材重新复习一下。
里面的obj meta用来管理类的成员方法和成员变量,cls meta主要是用来管理静态方法和静态变量。
接下来我们看一下,SetCSTable这个方法:
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);
                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);
                        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);
        }
}
下面的代码简单来说就是用前面代码生成的table表设置到CS.UnityEngine[Debug]中,最后的方法调用,应用反射的情况下,
internal void PushFixCSFunction(RealStatePtr L, LuaCSFunction func)
{
        if (func == null)
        {
                LuaAPI.lua_pushnil(L);
        }
        else
        {
                LuaAPI.xlua_pushinteger(L, fix_cs_functions.Count);
                fix_cs_functions.Add(func);
                LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.FixCSFunctionWraper, 1);
        }
}


public static void lua_pushstdcallcfunction(IntPtr L, lua_CSFunction function, int n = 0)//[-0, +1, m]
{
#if XLUA_GENERAL || (UNITY_WSA && !UNITY_EDITOR)
        GCHandle.Alloc(function);
#endif
        IntPtr fn = Marshal.GetFunctionPointerForDelegate(function);
        xlua_push_csharp_function(L, fn, n);
}
我们来看看c语言中的实现,打开xlua.c
LUA_API void xlua_push_csharp_function(lua_State* L, lua_CFunction fn, int n)
{
    lua_pushcfunction(L, fn);
        if (n > 0) {
                lua_insert(L, -1 - n);
        }
        lua_pushboolean(L, 0);
        if (n > 0) {
                lua_insert(L, -1 - n);
        }
    lua_pushcclosure(L, csharp_function_wrap, 2 + (n > 0 ? n : 0));
}你会发现这里发现了一个闭包方法,带一个特定的upvalue,后面会通过这个upvalue来调用特定的方法。
注册代码的看起来比较简单,我这里不做介绍了。
好长的文章,终于结束了!!!!!!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 04:48 , Processed in 0.092974 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表