项目中活动都是用xlua开发的,项目周更热修也是用xlua的hotfix特性来做的。现研究底层原理,对于项目性能有个更好的把控。
本文认为看到该文章的人已具备使用xlua开发的能力,只研究介绍下xlua的底层实现原理。
一.lua和c#交互原理
概括:
通过栈来实现。lua调用c#就是将lua层的参数和c#导出函数入栈,然后执行函数。c#调用lua就是将c#层的参数和lua函数入栈,然后执行函数
1.1 C#访问lua
直接上代码:
private void Demo1()
{
IntPtr L = LuaAPI.luaL_newstate();
if (LuaAPI.luaL_loadbuffer(L, @"function addandsub(x,y)
return x+y , x-y end", "selfTagChunk") != 0)
{
Debug.LogError(LuaAPI.lua_tostring(L,-1));
}
//LuaAPI.lua_pushnumber(L,20);
//LuaAPI.lua_pushnumber(L,21);
LuaAPI.lua_pcall(L, 0, 0, 0);
LuaAPI.xlua_getglobal(L, "addandsub");
LuaAPI.lua_pushnumber(L,10);
LuaAPI.lua_pushnumber(L,7);
int valueB = LuaAPI.xlua_tointeger(L, -1);//7
int valueB2 = LuaAPI.xlua_tointeger(L, -2);//10
LuaTypes luaTypeB3 = LuaAPI.lua_type(L, -3);//function
int valueC4 = LuaAPI.xlua_tointeger(L, 3);//7
int valueC3 = LuaAPI.xlua_tointeger(L, 2);//10
LuaTypes luaTypeC2 = LuaAPI.lua_type(L, 1);//function
if (LuaAPI.lua_pcall(L, 2, 2, 0) != 0)
{
Debug.LogError(LuaAPI.lua_tostring(L,-1));
}
int value = LuaAPI.xlua_tointeger(L, -1); //3
int value2 = LuaAPI.xlua_tointeger(L, -2); //17
LuaAPI.lua_close(L);
}
Api备注:
LuaAPI.luaL_newstate:开辟lua虚拟机,执行lua程序
LuaAPI.lua_close:关闭lua虚拟机,释放资源
LuaAPI.luaL_loadbuffer:编译一段lua代码,但没执行
LuaAPI.lua_pcall:执行lua代码,这时候可以用
这里:先把addandsub方法压栈,然后把10和7分别压栈。所以看到的内存里的数据如批注所示。
在执行第2个lua_pcall。会把之前的堆栈信息清空,把函数执行返回的结果进行压栈操作
1.2 lua访问C#
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void TestCSFunction(IntPtr L);
[MonoPInvokeCallback(typeof(TestCSFunction))]
public static void TestLuaCallCSharp(IntPtr L)
{
Debug.Log("TestLuaCallCSharp");
}
private void Demo2()
{
IntPtr L = LuaAPI.luaL_newstate();
//Marshal 提供对非托管类型的操作
IntPtr function = Marshal.GetFunctionPointerForDelegate(new TestCSFunction(TestLuaCallCSharp));
//函数入栈
LuaAPI.lua_pushcclosure(L,function,0);
LuaAPI.lua_pcall(L, 0, 0, 0);
LuaAPI.lua_close(L);
}
UnmanagedFunctionPointer:定义为了让其不受C#托管管理
MonoPInvokeCallback:标记可以使其用C或C++调用
上述可以看到是把C#函数包装成指针,然后进行压栈操作供lua调用
二.xlua中的LuaEnv
private void Demo3()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");
luaenv.Dispose();
}
LuaEnv:是xlua封装好的lua环境,类似上面的luaL_newstate。
具体的事情:
XLua框架中最重要的一个类,那就是LuaEnv。它包含了lua中的状态机RealStatePrt。lua的G表,还有注册表LuaIndexes.LUA_REGISTRYINDEX等等,下面从LuaEnv的构造函数开始,看看这个类做了些什么事情
//节选LuaEnv构造函数部分代码
//拿到Lua中的注册表
LuaIndexes.LUA_REGISTRYINDEX = LuaAPI.xlua_get_registry_index
//创建Lua状态机
rawL = LuaAPI.luaL_newstate()
//十分重要的一个对象,用于c#和lua的交互
translator = new ObjectTranslator(this, rawL);
translator.createFunctionMetatable(rawL); //添加_gc元方法到注册表
translator.OpenLib(rawL); //将init_xlua中会用到的方法,全部定义出来
//添加搜索路径
AddSearcher(StaticLuaCallbacks.LoadBuiltinLib, 2);
//添加自定义解析Lua文件的方法,对应的是LuaEnv.CustomLoader
AddSearcher(StaticLuaCallbacks.LoadFromCustomLoaders, 3);
#if !XLUA_GENERAL
AddSearcher(StaticLuaCallbacks.LoadFromResource, 4);
AddSearcher(StaticLuaCallbacks.LoadFromStreamingAssetsPath, -1);
#endif
//十分重要!! 初始化xLua
DoString(init_xlua, "Init");
#if !UNITY_SWITCH || UNITY_EDITOR
AddBuildin("socket.core", StaticLuaCallbacks.LoadSocketCore);
AddBuildin("socket", StaticLuaCallbacks.LoadSocketCore);
#endif
AddBuildin("CS", StaticLuaCallbacks.LoadCS);
调用Init方法:
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
--fqn就是类型和命名空间名,通过import_type去获取对应的udata并且入栈
function metatable:__index(key)
--查询key不调用元方法,更简单的表达只在自己的表内查询
local fqn = rawget(self,'.fqn')
fqn = ((fqn and fqn .. '.') or '') .. key
--查询C#类型
local obj = import_type(fqn)
--如果不是 再次查询C#命名空间
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
--既然是C#对象 就不要再newindex了,避免产生未知的错误
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 这下知道了 typeof是xlua自己搞的,不是lua本身语言特性
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
";
设置了metatble扩充原方法,并且设置CS的元表示metatable:
__index:会把C#没有缓存的命名空间下的元数据导入进来。
__call:与index同理,这里能看出来了为什么在lua里调C#方法是CS.UnityEngine.Vector3(x,x,x)。
这步是让lua可以去访问C#对象。
因为在Lua中定义了全局的CS表,并且在lua中调用CS的时候,会先调用StaticLuaCallbacks.LoadCS获取到注册表中的CS表,然后在调用XXX的时候,如果访问到了CS表中不存在的元素,则会调用其元表,在元表中通过映射到c#的StaticLuaCallbacks.ImportType方法完成查找。
lua在查找时调用了import_type进行查找调用,提前在OpenLib里把方法进行注册:
而importType会从中转器查询,查询的过程就是从一个维护的字典里看能不能取到,取不到就加载命名空间并注入到lua虚拟机中。
后面介绍lua与C#之间通信会详解介绍
参考: