项目中活动都是用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);//7int valueB2 = LuaAPI.xlua_tointeger(L, -2);//10LuaTypes luaTypeB3 = LuaAPI.lua_type(L, -3);//functionint valueC4 = LuaAPI.xlua_tointeger(L, 3);//7int valueC3 = LuaAPI.xlua_tointeger(L, 2);//10LuaTypes luaTypeC2 = LuaAPI.lua_type(L, 1);//functionif (LuaAPI.lua_pcall(L, 2, 2, 0) != 0){Debug.LogError(LuaAPI.lua_tostring(L,-1));}int value = LuaAPI.xlua_tointeger(L, -1); //3int value2 = LuaAPI.xlua_tointeger(L, -2); //17LuaAPI.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 = rawgetlocal setmetatable = setmetatablelocal import_type = xlua.import_typelocal import_generic_type = xlua.import_generic_typelocal 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 thenreturn rawget(self, key)end-- Cache this lookuprawset(self, key, obj)return objend--既然是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 thenlocal gt = import_generic_type(fqn, ...)if gt thenreturn rawget(CS, gt)endenderror('No such type: ' .. fqn, 2)endCS = CS or {}setmetatable(CS, metatable)--定义typeof 这下知道了 typeof是xlua自己搞的,不是lua本身语言特性typeof = function(t) return t.UnderlyingSystemType endcast = xlua.castif not setfenv or not getfenv thenlocal function getfunction(level)local info = debug.getinfo(level + 1, 'f')return info and info.funcendfunction setfenv(fn, env)if type(fn) == 'number' then fn = getfunction(fn + 1) endlocal i = 1while true dolocal name = debug.getupvalue(fn, i)if name == '_ENV' thendebug.upvaluejoin(fn, i, (function()return envend), 1)breakelseif not name thenbreakendi = i + 1endreturn fnendfunction getfenv(fn)if type(fn) == 'number' then fn = getfunction(fn + 1) endlocal i = 1while true dolocal name, val = debug.getupvalue(fn, i)if name == '_ENV' thenreturn valelseif not name thenbreakendi = i + 1endendendxlua.hotfix = function(cs, field, func)if func == nil then func = false endlocal tbl = (type(field) == 'table') and field or {[field] = func}for k, v in pairs(tbl) dolocal cflag = ''if k == '.ctor' thencflag = '_c'k = 'ctor'endlocal f = type(v) == 'function' and v or nilxlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least onepcall(function()for i = 1, 99 doxlua.access(cs, cflag .. '__Hotfix'..i..'_'..k, f)endend)endxlua.private_accessible(cs)endxlua.getmetatable = function(cs)return xlua.metatable_operation(cs)endxlua.setmetatable = function(cs, mt)return xlua.metatable_operation(cs, mt)endxlua.setclass = function(parent, name, impl)impl.UnderlyingSystemType = parent[name].UnderlyingSystemTyperawset(parent, name, impl)endlocal base_mt = {__index = function(t, k)local csobj = t['__csobj']local func = csobj['<>xLuaBaseProxy_'..k]return function(_, ...)return func(csobj, ...)endend}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#之间通信会详解介绍
参考:
Lua与C#交互原理(转)_lua与c#的交互原理-CSDN博客
https://zhuanlan.zhihu.com/p/441169478