xlua通信
xlua的wrap文件:Xlua 生成wrap文件 - 柯腾_wjf - 博客园 (cnblogs.com)
相关扩展:XLua标签(转) - mc宇少 - 博客园 (cnblogs.com)
参考资料:
干货:xlua 是怎么和C#通信的?(二) - 知乎 (zhihu.com)
深入理解xLua基于IL代码注入的热更新原理 - iwiniwin - 博客园 (cnblogs.com)
Lua调用c#发生了什么? - 柯腾_wjf - 博客园 (cnblogs.com)
深入xLua实现原理之Lua如何调用C# - iwiniwin - 博客园 (cnblogs.com)
深入xLua实现原理之C#如何调用Lua - iwiniwin - 博客园 (cnblogs.com)
一.启动XLua
Lua和C++/C#交互的时候内部会维护一个堆栈,Lua通过索引来拿到栈里面的数据,1代表栈底,向上递增,-1代表栈顶,向下递减。
public LuaEnv() { ... // Create State,创建虚拟堆栈 RealStatePtr rawL = LuaAPI.luaL_newstate(); translator = new ObjectTranslator(this, rawL); ObjectTranslatorPool.Instance.Add(rawL, translator); ... }
这里面的 RealStatePtr 默认是IntPtr,它封装了调用WindowsAPI函数时使用的指针,根据平台的不同,底层指针可以是32,位,也可以是64位。这里用来表示虚拟的堆栈的引用
二.Lua与C#数据通信机制
1.传递C#对象到Lua
Lua中要使用C#的对象的情况,a = self.a,实际是调用生成的get_a方法,先将C#层的对象 a Push到Lua栈中,生成对应的userdata,蒋政引用赋给lua.a :
对于bool,int这样简单的值类型可以直接通过C API传递。但对于C#对象就不同了,Lua这边没有能与之对应的类型,因此传递到Lua的只是C#对象的一个索引。
// ObjectTranslator.cs public void Push(RealStatePtr L, object o) { ...//检查是否有缓存,有就直接用 //如果一个type的定义含本身静态readonly实例时,getTypeId会push一个实例,这时候应该用这个实例 if (is_first && needcache && (is_enum ? enumMap.TryGetValue(o, out index) : reverseMap.TryGetValue(o, out index))) { if (LuaAPI.xlua_tryget_cachedud(L, index, cacheRef) == 1) { return; } } // C#侧进行缓存 index = addObject(o, is_valuetype, is_enum); // 将代表对象的索引push到lua LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef); }
xlua_pushcsobj的主要逻辑是,代表对象的索引被push到Lua后,Lua会为其创建一个userdata,并将这个userdata指向对象索引,如果需要缓存则将userdata保存到缓存表中, 最后为userdata设置了元表(定义这个对象C#的类型信息(方法,属性等),通过getTypeId()获取该类型在lua注册表中的key,并将它设置成userdata的元表)。也就是说,C#对象在Lua这边对应的就是一个userdata,利用对象索引保持与C#对象的联系。
userdata:lua的一种数据类型,用于与其他语言的交互,userdata提供了一块原始的内存区域,可以用来存储任何东西,Lua负责这块内存区域的内存管理与回收。
2.注册C#类型信息到Lua(getTypeId())
函数的主要逻辑是以类的名称为key在lau的注册表中找对应的表,如果找不到就通过TryDelayWrapLoader函数生成后存在注册表中,并返回typeid(元表在Lua注册表中的索引)。
3.lua注册表的生成
1.生成函数填充元表
// TestXLua.cs
[LuaCallCSharp]
public class TestXLua
{
public string Name;
public void Test1(int a){
}
public static void Test2(int a, bool b, string c)
{
}
}
Generatecode后生成的XXXwrap文件:
public class TestXLuaWrap
{
public static void __Register(RealStatePtr L)
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
System.Type type = typeof(TestXLua);
//准备注册类的非静态成员(成员变量,成员方法等),主要为元表添加_gc和_tostring,并准备好method表、getter表、setter表。
Utils.BeginObjectRegister(type, L, translator, 0, 1, 1, 1);
//将下面生成的非静态包裹方法注册进对应的表中
Utils.RegisterFunc(L, Utils.METHOD_IDX, "Test1", _m_Test1);//成员方法放进method表中
Utils.RegisterFunc(L, Utils.GETTER_IDX, "Name", _g_get_Name);//成员变量get放进getter表中
Utils.RegisterFunc(L, Utils.SETTER_IDX, "Name", _s_set_Name);//成员变量get放进setter表中
//完成类的非静态成员的注册,主要为元表生成_index和_newIndex元方法。
//_index元方法:当访问userdata[key]时,先依次查询之前通过RegisterFunc填充的methods、getters等表中是否存有对应key的包裹方法,
//如果有就直接使用,没有则递归在父类中查找。
Utils.EndObjectRegister(type, L, translator, null, null,
null, null, null);
//对类的静态值(例如静态变量,静态方法等)进行注册前做一些准备工作。主要是为类生成对应的cls_table表,
//以及提前创建好static_getter表与static_setter表,后续用来存放静态字段对应的get和set包裹方法。注意这里还会为cls_table设置元表meta_table
//cls_table:存类数据的表,Lua注册表[xlua_csharp_namespace]实际上对应的就是CS全局表,所以要在xLua中访问C#类时才可以直接使用CS.A.B.C这样的形式
//Lua注册表 = {
//xlua_csharp_namespace = { -- 就是CS全局表
//A = {
// B = {
// C = cls_table
// }
//},
//},
// }
Utils.BeginClassRegister(type, L, __CreateInstance, 2, 0, 0);
//跟前面一样,注册到对应的表中
Utils.RegisterFunc(L, Utils.CLS_IDX, "Test2", _m_Test2_xlua_st_);
//结束对类的静态值的注册,为cls_table的元表meta_table设置_index元方法和_newIndex元方法。
Utils.EndClassRegister(type, L, translator);
}
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int __CreateInstance(RealStatePtr L)
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
TestXLua gen_ret = new TestXLua();
translator.Push(L, gen_ret);
return 1;
}
//类成员方法
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_Test1(RealStatePtr L)
{
try {
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
int _a = LuaAPI.xlua_tointeger(L, 2);
gen_to_be_invoked.Test1( _a );
return 0;
{
}
}
//类静态方法
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_Test2_xlua_st_(RealStatePtr L)
{
int _a = LuaAPI.xlua_tointeger(L, 1);
bool _b = LuaAPI.lua_toboolean(L, 2);
string _c = LuaAPI.lua_tostring(L, 3);
TestXLua.Test2( _a, _b, _c );
return 0;
}
//字段生成的get方法
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _g_get_Name(RealStatePtr L)
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
LuaAPI.lua_pushstring(L, gen_to_be_invoked.Name);
return 1;
}
//字段生成的set方法
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _s_set_Name(RealStatePtr L)
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
TestXLua gen_to_be_invoked = (TestXLua)translator.FastGetCSObj(L, 1);
gen_to_be_invoked.Name = LuaAPI.lua_tostring(L, 2);
return 0;
}
}
总结:
生成代码会为类的非静态值都生成对应的包裹方法(字段改成get和set方法),并将包裹方法以key = func的形势注册到不同的表中。userdata元表的_index和_newindex负责从这些不同的表中找到对应key的方法,通过包裹方法实现对c#层对象的控制.
生成代码会为每个类以命名空间为层次结构生成cls_table表。与类的非静态值相同,生成代码也会为类的静态值都生成对应的包裹方法并注册到不同的表中(静态方法会直接注册到cls_table中而不是function表中)。而cls_table元表的_index和_newIndex负责从不同的表中找到对应key的包裹方法,最终通过调用包裹方法实现对c#类的控制。
cls_table:包含所有类的静态方法
cls_meta:cls_table的元表,封装静态的属性设置、获取接口_index和_newindex
obj_meta:该元表封装了类的成员方法、成员变量的get,set调用接口_index和_newidnex。
local obj = CS.TestXLua()
obj.Name = "test" -- 赋值操作将触发obj元表的__newindex,__newindex在setter表中找到Name对应的set包裹方法_s_set_Name,然后通过调用_s_set_Name方法设置了TestXLua对象的Name属性为"test"
CS.TestXLua.Test2() -- CS.TestXLua获取到TestXLua类对应的cls_table,由于Test2是静态方法,在cls_table中可以直接拿到其对应的包裹方法_m_Test2_xlua_st_,然后通过调用_m_Test2_xlua_st_而间接调用了TestXLua类的Test2方法
2.使用反射填充元表
当没有生成代码时,会使用反射进行注册,与生成代码进行注册的逻辑基本相同。通过反射获取到类的各个静态值和非静态值,然后分别注册到不同的表中,以及填充__index和__newindex元方法
2.包裹方法意义
包裹方法就是用来处理参数传递问题的。
3.为什么需要为属性生成get、set方法
因为只有将Lua的访问或赋值操作转换成函数调用形式时,参数才能利用函数调用机制被自动的压栈,从而传递给C#
4.重载的情况
是通过同名函数被调用时传递的参数情况来判断到底应该调用哪个函数
5.GC
C#和Lua都是有自动垃圾回收机制的,并且相互是无感知的。如果传递到Lua的C#对象被C#自动回收掉了,而Lua这边仍毫不知情继续使用,则必然会导致无法预知的错误。所以基本原则是传递到Lua的C#对象,C#不能自动回收,只能Lua在确定不再使用后通知C#进行回收。
实现方式:C#层创建一个objects容器,创建的对象都存一份引用在这,保证对象有引用不会被自动回收。Lua层GC回收userdata的时候会调用userdata的_gc(BeginObjectRegister时注册)方法,来通知C#remove这个对象引用。
三.C#与lua数据通信机制
除了普通的值类型之外,Lua中比较特殊但又很常用的大概就是table和funciton这两种类型了,下面逐一来分析
1.传递Lua table到C#
例子:
Lua层为C#的对象赋值,并同步到C#
// 注意,这里添加的LuaCallCSharp特性只是为了使xLua为其生成代码,不添加并不影响功能,不打标签是通过反射 [LuaCallCSharp] public class TestXLua { public static LuaTable tab; }
//Generate Code生成的 TestXLuaWrap.cs [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] static int _g_get_tab(RealStatePtr L) { try { ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); translator.Push(L, TestXLua.tab); } catch(System.Exception gen_e) { return LuaAPI.luaL_error(L, "c# exception:" + gen_e); } return 1; } [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] static int _s_set_tab(RealStatePtr L) { try { ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); TestXLua.tab = (XLua.LuaTable)translator.GetObject(L, 1, typeof(XLua.LuaTable)); } catch(System.Exception gen_e) { return LuaAPI.luaL_error(L, "c# exception:" + gen_e); } return 0; }
-- Lua测试代码 local t = { num = 1 } CS.TestXLua.tab = t
上述代码赋值时,最终会调用到C#的_s_set_tab包裹方法,Lua这边调用_s_set_tab前,会先将参数table t压入到栈中,因此_s_set_tab内部需要通过translator.GetObject拿到这个table,并将其赋值给tab静态变量。
GetObject方法负责从栈中获取指定类型的对象,对于LuaTable类型是通过objectCasters.GetCaster获取转换器后,通过转换器函数转换得到(xlua定义了一堆转换器函数(委托、enum、class等),可以将栈里取出来的userdata转成C#的类型)。
映射成C#的class
无需打标签,只是将luatable的值复制过来,更改不会同步到Lua层。
DClass d = luaenv.Global.Get<DClass>("d");
DClass:C#的类
d:lua的表
过程与上面差不多。
映射到interface
需要打标签并生成代码,可以和Lua层交互,更改会同步到Lua层。
[XLua.CSharpCallLua] public interface TestCCallLua { int a { get; set; } int func(); }
生成包裹类
public class TestCCallLuaBridge : LuaBase, TestCCallLua { public static LuaBase __Create(int reference, LuaEnv luaenv) { return new TestCCallLuaBridge(reference, luaenv); } public TestCCallLuaBridge(int reference, LuaEnv luaenv) : base(reference, luaenv) { } int TestCCallLua.func() { //调用lua的func方法 } int TestCCallLua.a { get { //获取lua的a的值 } set { //更改lua的a的值 } } }
在XLuaGenAutoRegister中注册:
translator.AddInterfaceBridgeCreator(typeof(TestCCallLua), TestCCallLuaBridge.__Create);
过程与上面差不多,都是通过生成的包裹类,将每个属性封装成get、set方法,在生成的方法中实现出入栈,保存lua层引用的功能。
2.传递LuaFunction到C#
传LuaFunction到委托过程和上面大致一样:
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] static int _s_set_func(RealStatePtr L) { try { ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); TestXLua.func = translator.GetDelegate<TestXLua.Func>(L, 1); } catch(System.Exception gen_e) { return LuaAPI.luaL_error(L, "c# exception:" + gen_e); } return 0; }
GetDelegate:通过CreateDelegateBridege()创建一个对应的委托并返回。CreateDelegateBridge内部会创建一个DelegateBridge(里面有一堆“__Gen_Delegate_Imp”开头的方法)对象来对应Lua function,这个DelegateBridge对象同时保存着Lua function的索引。
用于接受LuaFunction的委托必须添加 [CSharpCallLua] 为其生成“__Gen_Delegate_Imp”开头的方法,不添加会抛出异常,这个函数封装了参数的压栈操作以及Lua function调用。
-- Lua测试代码 CS.TestXLua.func = function(s, b, i) end
这个赋值操作实际上是将生成的“__Gen_Delegate_Imp”开头的方法赋给了func。
所有打hotfix标签的方法打包后都会生成DelegateBridge bridge = __Hotfix0_OnGraphStart;通过检查bridge是否为空来判断是否执行lua修复代码(调用lua代码也是通过bridge)。所有可以热更的函数都会按类别生成一个这个委托(按参数和返回值类别,每种生成一个,不会每个函数都生成),用于bridge承载lua函数。
3.GC
和LuaCallC#一样,lua这边也要避免被 C#引用的对象 被GC回收,实现方式也大致相同:
维护一个注册表,被C#引用的对象放到注册表中防止被回收,C#这边为对应的Lua对象定义了LuaBase基类,这个类实现了IDisposable接口,回收的接口中调用lua的释放接口将lua对象从lua的注册表中移除。