前面已经介绍了Lua与C#的基础通信原理,和Wrap中间文件的作用。有了前面2篇的基础,大概已经能搞清这块的原理。
为了加深对这块的印象,这里开始正式分析Xlua中的Lua和C#的通信。
一、Lua如何调用CS的过程
lua的初始化代码:
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
";
这段在该系列的一 里有介绍,就是弄了个CS的table。CS设置了原方法:
__index:查找方法,先看有没有加载过,没有就加载,设置元表是metatable(和CS同一个元表)。然后返回。
import_type:对应C#的ImportType方法。
映射的建立在这里:
压栈import_type字符,压栈C#ImportType方法,压栈raw_set并执行。
C#对应方法:先查有没有映射过,没有就映射然后缓存压栈。
__newindex:调用就报错,可能为了防止查bug不好查吧。
__call:对应C#的构造方法
查找C#的类,优先去调Wrap适配代码,没有就反射查找调用。
Wrap文件加载都会调到__Register进行Lua表的注册,如:
Util的RegisterFunc就是把Lua的元表元素与C#对应的方法进行映射
idx可以简单理解为定义的值
反射部分代码:
在Lua虚拟机创建新的table,然后为其先收集元数据。对应在table里同名进行映射。相比较Wrap模式代码,性能比较浪费。
注册方法为例图示:
最后不论哪种模式,都会走到SetCSTable,期目的是创建一个luatable并把对应C#方法塞进来。
反射的调用走的
把lua和C#靠一个映射关系联系起来,对应key
总结:
1.xlua在程序运行时,先调用init_xlua。目的是创建CS表并且设置__index,__call元方法
2.lua调用C#时,触发元方法,先找缓存没缓存优先加载Wrap文件注册,没有就反射查找。属性什么的都用rawset设置lua与C#方法进行映射
Wrap模式:
反射模式:
3.运行维护堆栈来进行通信(lua找C#对象也就是个Int索引,不会真把内存都压栈给lua)
二、CS如何调用Lua的过程
2.1 C#如何Get Lua代码的数据
ObjectCasters:负责转换lua的数据到C#
看个简单的:
从lua堆栈中取元素,然后转化。取LuaTable也差不多
从下面这段可以看出对应C#而言,lua的table就是一个指针,只需要对应的类来处理这个指针相关的信息就可以达到对lua table操作。
需要注意每次这里都是new个新的table,为了保证唯一性,项目里可以搞个缓存记录一下。避免出现好多个c#的LuaTable指向同一个Lua里的Table。
2.2 C#如何调用lua的方法
这块比较简单,就是简单的压栈操作调用call方法。直接看代码直观
public object[] Call(object[] args, Type[] returnTypes)
{
#if THREAD_SAFE || HOTFIX_ENABLE
lock (luaEnv.luaEnvLock)
{
#endif
int nArgs = 0;
var L = luaEnv.L;
var translator = luaEnv.translator;
int oldTop = LuaAPI.lua_gettop(L);
int errFunc = LuaAPI.load_error_func(L, luaEnv.errorFuncRef);
LuaAPI.lua_getref(L, luaReference);
if (args != null)
{
nArgs = args.Length;
for (int i = 0; i < args.Length; i++)
{
translator.PushAny(L, args[i]);
}
}
int error = LuaAPI.lua_pcall(L, nArgs, -1, errFunc);
if (error != 0)
luaEnv.ThrowExceptionFromError(oldTop);
LuaAPI.lua_remove(L, errFunc);
if (returnTypes != null)
return translator.popValues(L, oldTop, returnTypes);
else
return translator.popValues(L, oldTop);
#if THREAD_SAFE || HOTFIX_ENABLE
}
#endif
}
2.3 C#如何设置lua的属性
Set也比较简单,入栈lua部分的key和C#的value直接调用一个 LuaAPI.xlua_psettable。然后再还原堆栈信息
public void Set<TKey, TValue>(TKey key, TValue value)
{
#if THREAD_SAFE || HOTFIX_ENABLE
lock (luaEnv.luaEnvLock)
{
#endif
var L = luaEnv.L;
int oldTop = LuaAPI.lua_gettop(L);
var translator = luaEnv.translator;
LuaAPI.lua_getref(L, luaReference);
translator.PushByType(L, key);
translator.PushByType(L, value);
if (0 != LuaAPI.xlua_psettable(L, -3))
{
luaEnv.ThrowExceptionFromError(oldTop);
}
LuaAPI.lua_settop(L, oldTop);
#if THREAD_SAFE || HOTFIX_ENABLE
}
#endif
}
测试代码:
private void Demo5()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString(@"luaTable = {a=123,b=456,c=789}
function luaTable:Func()
print('testLua:'.. tostring(self.a))
end
go = CS.UnityEngine.GameObject()
go.name = 'luaGo'
");
LuaTable luaTable = luaenv.Global.Get<LuaTable>("luaTable");
LuaFunction func = luaTable.Get<LuaFunction>("Func");
int a = luaTable.Get<int>("a");
func.Call(luaTable);
luaTable.Set("a",999);
func.Call(luaTable);
GameObject go = luaenv.Global.Get<GameObject>("go");
go.name = "CSharpGo";
luaenv.Dispose();
}
Global:对应lua的_G表。
1.先从G表获取名为luaTable的表。调用C#注册通信的lua方法拿到lua的内存指针,并且创建一个C#的LuaTable类。
2.同样的方式拿到Function
3.拿到变量a有所区别,直接从堆栈获取,因为是基础类型
4.调用Call方法,进行压栈。走到 LuaAPI.lua_pcall
5.缓存堆栈信息,进行压栈操作后,设置lua属性。还原堆栈
6.GameObject的生成改名也差不多就不复述了。
更详细的部分得需要去查看lua的c语言实现部分,碍于时间和水平的原因。暂时不详细看了。大体上也是入栈出栈调用pcall方法