|
五一开始了,正好趁这段时间,挺闲的。整理一下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[&#39;<>xLuaBaseProxy_&#39;..k]
return function(_, ...)
return func(csobj, ...)
end
end
}
base = function(csobj)
return setmetatable({__csobj = csobj}, base_mt)
end
&#34;;
这个变量太重要了,我认为它是这个项目的核心部分,藏在了这么不起眼的地方。
下面开始讲一下我理解的Lua调用C#流程,中间省略不说调用C语言库。
比如下面的这样一条语句,是怎么调用到C#的。
CS.UnityEngine.Debug.Log(&#39;hello world&#39;)先来看一下C#中LuaEnv这个脚本,
LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print);
if (0 != LuaAPI.xlua_setglobal(rawL, &#34;print&#34;))
{
throw new Exception(&#34;call xlua_setglobal fail!&#34;);
}
Lua里面的print方法就是通过这几行代码实现的,你会发现最终Lua里面的print调用的是引擎里面的一个最普通的Debug.Log();
然后是通过AddSearcher方法注册一些搜索Lua脚本的方法,一般我们用的时候需要自定义扩展一个。
DoString(init_xlua, &#34;Init&#34;);通过这行代码来Load init_xlua这个字符串中的Lua代码。
你会发现定义了一个叫CS的全局表,现在你们知道为什么Lua调用C#的时候需要以CS开头了吧。
接下来我们关注一下,这个CS表的元表,
function metatable:__index(key)
local fqn = rawget(self,&#39;.fqn&#39;)
fqn = ((fqn and fqn .. &#39;.&#39;) or &#39;&#39;) .. key
local obj = import_type(fqn)
if obj == nil then
-- It might be an assembly, so we load it too.
obj = { [&#39;.fqn&#39;] = 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, &#34;can not load type &#34; + type);
}
}
else
{
LuaAPI.lua_pushnil(L);
}
return 1;
}
catch (System.Exception e)
{
return LuaAPI.luaL_error(L, &#34;c# exception in xlua.import_type:&#34; + 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(&#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));
}
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(&#34;SetCSTable for [&#34; + type + &#34;] error: &#34; + 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(&#34;SetCSTable for [&#34; + type + &#34;] error: ancestors is not a table!&#34;);
}
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来调用特定的方法。
注册代码的看起来比较简单,我这里不做介绍了。
好长的文章,终于结束了!!!!!! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|