# 前言
很久以前也有拿着 XLua C# 这边的源码看过,网上也找过资料... 就是搞不大清楚。
可惜没人提醒,后来才想明白,直接硬看 C# 这边的源码是不行的,想明白 C# 与 XLua 的交互原理,至少得先了解 C/C++ 与 Lua 的交互原理
—— 毕竟 C# 与 XLua 交互,依然是基于中间的 C API,了解了那边的概念,再看 C# 与 XLua 交互原理,才好理解。
# 基本介绍
- Lua 虚拟机由 C/C++ 实现,因此它可以直接与宿主进行通信
- C# 则可以依靠 C API 通过 P/Invoke 方式调用 Lua 虚拟机函数
- 即 C# 可以借助 C/C++ 来与 Lua 进行数据通信
- XLua 相关 P/Invoke 调用接口位于
LuaDLL.cs
文件
# Lua 和 C/C++ 的数据交互
- 基础:Lua 提供的一个虚拟栈
- 两者所有类型的数据交换都通过这个栈完成
- Lua 提供了两种索引方式操作虚拟栈
- 正数索引:1 表示栈底
- 反向索引:-1 表示栈顶
- 例如:
正向索引 反向索引 3 -1 2 -2 1 -3
# Lua 调用 C/C++ 函数
- 将 C++ 的函数包装成可供 Lua 调用的格式
- 接收一个 Lua 状态机指针 (
IntPtr
) 的静态方法,该方法返回值为 int,表示方法返回值数量
- 接收一个 Lua 状态机指针 (
- 在 Lua 环境注册包装好的函数
- Lua 调用
- 首先通过
lua_gettop
获取 Lua 参数数量 (因为可能有重载) - 继续通过正数索引从
1
开始在 Lua 栈上获取具体参数值 - 执行实际函数功能
- 将返回值压栈
- 包装函数的返回值为 int,表示返回值数量
- 首先通过
# C/C++ 调用 Lua 函数
- 使用
lua_getglobal(xlua_getglobal)
来获取函数,然后将其压入栈 - 若函数有参则依次将函数的参数也压入栈
- 调用
lua_pcall
让虚拟机执行函数- 参数分别为:
- 虚拟机指针
- 参数个数
- 返回值个数
- 错误处理函数,0 表示无,表示错误处理函数在栈中的索引
- 如果运行出错,
lua_pcall
会返回一个非零的结果 - 若调用完毕没有出错,则可以通过 Lua 虚拟栈从中取出调用结果
- 参数分别为:
# 基元类型传递
对于 bool、int 这样简单的值类型可以直接通过 C API 传递,见 LuaDLL.cs
- xlua_pushinteger
- lua_pushboolean
- lua_pushnumber
- xlua_pushuint
# 对象类型传递
# 基本流程
C# 与 Lua 交互依然还是依靠 C API 通过 P/Invoke 进行,为了正确的和 Lua 通讯,C# 与 Lua 通过相互保存的索引保持引用
对于 C# 对象,Lua 这边通过 Table 模拟,C# 对象在 Lua 对应的就是一个 userdata
,利用对象索引保持与 C# 对象的联系
- 传递到 Lua 的只是 C# 对象的一个索引,并需要注册 C# 类型信息到 Lua 以便使用
- 其中,对象的基本信息通过
XLua_Gen_Initer_Register__
中初始化通过调用ObjectTranslator.DelayWrapLoader
注册到 Lua 侧 userdata
:特指 C# 对象在 Lua 这边对应的代理 userdata- 为
userdata
设置的元表表示的实际是对象的类型信息,可以称为 “代理”
- 为
- 在将 C# 对象传递到 Lua 以后,还需要告知 Lua 该对象的类型信息,比如对象类型有哪些成员方法,属性或是静态方法等。将这些都注册到 Lua 后,Lua 才能正确的调用
- 对于
userdata
转index
,主要由两个 C API 提供:LuaAPI.xlua_tocsobj_safe
(实际上为 C API:lua_touserdata
)、LuaAPI.xlua_gettypeid
(实际上为 C APIlua_getmetatable
)从 Lua 虚拟栈取值- 注:取出的是代理对象在 C# 侧的 ObjectPool 实例对象数组索引
LUA_API int xlua_tocsobj_safe(lua_State *L,int index) { | |
int *udata = (int *)lua_touserdata (L,index); | |
if (udata != NULL) { | |
if (lua_getmetatable(L,index)) { | |
lua_pushlightuserdata(L, &tag); | |
lua_rawget(L,-2); | |
if (!lua_isnil (L,-1)) { | |
lua_pop (L, 2); | |
return *udata; | |
} | |
lua_pop (L, 2); | |
} | |
} | |
return -1; | |
} | |
LUA_API int xlua_tocsobj_fast (lua_State *L,int index) { | |
int *udata = (int *)lua_touserdata (L,index); | |
if(udata!=NULL) | |
return *udata; | |
return -1; | |
} | |
LUA_API int xlua_gettypeid(lua_State *L, int idx) { | |
int type_id = -1; | |
if (lua_type(L, idx) == LUA_TUSERDATA) { | |
if (lua_getmetatable (L, idx)) { | |
lua_rawgeti(L, -1, 1); | |
if (lua_type(L, -1) == LUA_TNUMBER) { | |
type_id = (int)lua_tointeger(L, -1); | |
} | |
lua_pop(L, 2); | |
} | |
} | |
return type_id; | |
} |
类型的元表数据是通过 ObjectTranslator
getTypeId
函数调用之前注册的 delayWrap
回调生成并注册到 Lua 侧的 (或通过反射生成)
主要的两个方法代码如下:
//ObjectTranslator.cs | |
internal int getTypeId(RealStatePtr L, Type type, out bool is_first, LOGLEVEL log_level = LOGLEVEL.WARN) | |
{ | |
int type_id; | |
is_first = false; | |
if (!typeIdMap.TryGetValue(type, out type_id)) // no reference | |
{ | |
if (type.IsArray) | |
{ | |
if (common_array_meta == -1) throw new Exception("Fatal Exception! Array Metatable not inited!"); | |
return common_array_meta; | |
} | |
if (typeof(MulticastDelegate).IsAssignableFrom(type)) | |
{ | |
if (common_delegate_meta == -1) throw new Exception("Fatal Exception! Delegate Metatable not inited!"); | |
TryDelayWrapLoader(L, type); | |
return common_delegate_meta; | |
} | |
is_first = true; | |
Type alias_type = null; | |
aliasCfg.TryGetValue(type, out alias_type); | |
LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName); | |
if (LuaAPI.lua_isnil(L, -1)) //no meta yet, try to use reflection meta | |
{ | |
LuaAPI.lua_pop(L, 1); | |
if (TryDelayWrapLoader(L, alias_type == null ? type : alias_type)) | |
{ | |
LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName); | |
} | |
else | |
{ | |
throw new Exception("Fatal: can not load metatable of type:" + type); | |
} | |
} | |
// 循环依赖,自身依赖自己的 class,比如有个自身类型的静态 readonly 对象。 | |
if (typeIdMap.TryGetValue(type, out type_id)) | |
{ | |
LuaAPI.lua_pop(L, 1); | |
} | |
else | |
{ | |
if (type.IsEnum()) | |
{ | |
LuaAPI.xlua_pushasciistring(L, "__band"); | |
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumAndMeta); | |
LuaAPI.lua_rawset(L, -3); | |
LuaAPI.xlua_pushasciistring(L, "__bor"); | |
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.EnumOrMeta); | |
LuaAPI.lua_rawset(L, -3); | |
} | |
if (typeof(IEnumerable).IsAssignableFrom(type)) | |
{ | |
LuaAPI.xlua_pushasciistring(L, "__pairs"); | |
LuaAPI.lua_getref(L, enumerable_pairs_func); | |
LuaAPI.lua_rawset(L, -3); | |
} | |
LuaAPI.lua_pushvalue(L, -1); | |
type_id = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX); | |
LuaAPI.lua_pushnumber(L, type_id); | |
LuaAPI.xlua_rawseti(L, -2, 1); | |
LuaAPI.lua_pop(L, 1); | |
if (type.IsValueType()) | |
{ | |
typeMap.Add(type_id, type); | |
} | |
typeIdMap.Add(type, type_id); | |
} | |
} | |
return type_id; | |
} | |
// 已加载类型列表 | |
Dictionary<Type, bool> loaded_types = new Dictionary<Type, bool>(); | |
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("__Register", 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("{0} not gen, using reflection instead", type)); | |
#else | |
System.Console.WriteLine(string.Format("Warning: {0} not gen, using reflection instead", type)); | |
#endif | |
} | |
#endif | |
} | |
if (top != LuaAPI.lua_gettop(L)) | |
{ | |
throw new Exception("top change, before:" + top + ", after:" + LuaAPI.lua_gettop(L)); | |
} | |
foreach (var nested_type in type.GetNestedTypes(BindingFlags.Public)) | |
{ | |
if (nested_type.IsGenericTypeDefinition()) | |
{ | |
continue; | |
} | |
GetTypeId(L, nested_type); | |
} | |
return true; | |
} |
- 先判断是否生成过对应元数据,若存在这直接返回
typeIdMap
字典中 type 对应的type_id
- 注:数组是单独处理的,
LuaEnv
构造函数最后调用的注册
- 若没有则先判断是否有生成代码,没有则反射 (
ReflectionWrap
) 填充元表
- 反射时注册的 __call 元方法是个公共的
ObjectTranslator.methodWrapsCache.GetConstructorWrap
反射创建对象的操作回调 - 反射创建的对象通过
PushAny
加入ObjectPool
(这里会判断实际类型去调用合适的添加操作,例如字符串直接调用lua_pushstring
,基元类型调用pushPrimitive
等) - 注:该方法会递归调用,若对象类型中有嵌套的公共类型,则递归注册
那么,Lua 如何知道调用呢?
- 可以注意到
Lua
调用C#
都是通过CS.XXX
的方式进行的(因为我自己项目不是 XLua 项目,所以是看示例那些看的) - 而在
LuaEnv.cs
的构造方法中,会有AddBuildin("CS", StaticLuaCallbacks.LoadCS)
的注册 - 我怀疑这个是否就是将『CS』 注册为 Lua 侧的一个空表以当做命名空间?
- 当
Lua
使用CS.XXX
的时候,就会到这边来查询
# 对象实例成员注册
生成代码中,通过 Utils.BeginObjectRegister
注册对象基本信息至 Lua 元表中
如方法数量、getter_count、setter_count
- 注:指实例方法及实例字段,静态变量和方法不在此列
- 注:一个实例字段会被分别生成为 getter 与 setter 的静态 wrap 方法
以及比较重要的
__gc
元方法等- 如果给对象设置了 __gc 元方法,那么当对象被 gc 回收时将会调用它的 __gc 元方法
- C# 注册主要是为了当 Lua 回收 Lua 侧对应的 C# Table 对象后,同时可以允许回收 C# 这边的实际对象 (移除 C# 侧的缓存引用)
//Utils.cs | |
//BeginObjectRegister 注册实例对象数据方法 | |
if ((type == null || !translator.HasCustomOp(type)) && type != typeof(decimal)) | |
{ | |
LuaAPI.xlua_pushasciistring(L, "__gc"); | |
LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta); | |
LuaAPI.lua_rawset(L, -3); | |
} | |
//StaticLuaCallbacks.cs | |
//Lua 侧代理对象被回收后执行的回调 | |
[MonoPInvokeCallback(typeof(LuaCSFunction))] | |
public static int LuaGC(RealStatePtr L) | |
{ | |
try | |
{ | |
int udata = LuaAPI.xlua_tocsobj_safe(L, 1); | |
if (udata != -1) | |
{ | |
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); | |
if ( translator != null ) | |
{ | |
translator.collectObject(udata); | |
} | |
} | |
return 0; | |
} | |
catch (Exception e) | |
{ | |
return LuaAPI.luaL_error(L, "c# exception in LuaGC:" + e); | |
} | |
} |
然后注册实例字段、方法
- 多个重载方法会被注册为一个静态 wrap 函数
调用Utils.EndObjectRegister
完成实例字段、方法注册
# 对象静态成员注册
调用 Utils.BeginClassRegister
注册创建该类型实例的回调及静态字段访问方法 static_getter_count、static_setter_count 数量
- 若传递了创建实例类型的回调,则会注册到 Lua 的
__call
元方法中 (当 table 名字做为函数名字的形式被调用的时候,会调用 __call 函数)
//Utils.cs | |
//BeginClassRegister 注册静态数据时传递,注册创建对象实例回调 | |
if (creator != null) | |
{ | |
LuaAPI.xlua_pushasciistring(L, "__call"); | |
#if GEN_CODE_MINIMIZE | |
translator.PushCSharpWrapper(L, creator); | |
#else | |
LuaAPI.lua_pushstdcallcfunction(L, creator); | |
#endif | |
LuaAPI.lua_rawset(L, -3); | |
} |
注:代码生成始终会生成 __CreateInstance
即上述代码中 creator
这个回调,哪怕是静态类。
区别在于静态类要是调到了,是会直接报错的:
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] | |
static int __CreateInstance(RealStatePtr L) | |
{ | |
return LuaAPI.luaL_error(L, "TestS does not have a constructor!"); | |
} |
然后 Lua
侧创建时,例如官方 LuaCallCs.cs
示例中通过 local newGameObj2 = CS.UnityEngine.GameObject('helloworld')
创建了一个新的 GameObejct
对象,此时就是通过 Lua
侧被注册的 __call
元方法回调调用到 UnityEngineGameObjectWrap
中的 __CreateInstance
方法而创建的一个新对象
创建出新对象后,该对象会被代表该 Lua
状态机的 ObjectTranslator.ObjectPool
所缓存
ObjectPool
默认容量为512
,若超出则会双倍扩容
随后,通过 LuaAPI.xlua_pushcsobj
将返回的索引、C# 对象类型对应的 Lua 元表 type_id
等信息推送至 Lua 虚拟栈上,Lua 那边取值并用 userdata
缓存下来
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) { | |
int* pointer = (int*)lua_newuserdata(L, sizeof(int)); | |
*pointer = key; | |
if (need_cache) cacheud(L, key, cache_ref); | |
lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref); | |
lua_setmetatable(L, -2); | |
} |
最后就是:
- 注册静态方法及字段
- 调用
Utils.EndClassRegister
结束静态字段、方法注册
# 其它
- xlua_pushlstring
- 需要注意的是,LuaAPI 中封装了重载的接口,直接传递
string
类型的话 - 会通过转化为 UTF8 编码的 bytes 数组传递
- 有大小为
256
的数组缓存,小于该字节的走缓存,否则直接 GetBytes 转成字节数组传递
- 需要注意的是,LuaAPI 中封装了重载的接口,直接传递
decimal
也是单独通过 LuaAPI.xlua_pushstruct 处理的
# 数据交互 - Lua 调 C#
- 首先通过
getTypeId
注册 C# 对象信息至 Lua 侧,并通过一个索引 (userdata) 保持联系 - Lua 这边调用 C 函数时的参数会被自动的压栈 (若为实例对象,则会将对象索引压栈至第一位)
- 然后,通过上述注册的元表信息,例如自动生成的
__Register
延迟注册的代码:
public static void __Register(RealStatePtr L) | |
{ | |
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); | |
System.Type type = typeof(Test); | |
Utils.BeginObjectRegister(type, L, translator, 0, 2, 4, 4); | |
Utils.RegisterFunc(L, Utils.METHOD_IDX, "Test1", _m_Test1); | |
Utils.RegisterFunc(L, Utils.GETTER_IDX, "Name", _g_get_Name); | |
Utils.RegisterFunc(L, Utils.SETTER_IDX, "Name", _s_set_Name); | |
Utils.EndObjectRegister(type, L, translator, null, null, | |
null, null, null); | |
Utils.BeginClassRegister(type, L, __CreateInstance, 3, 2, 2); | |
Utils.RegisterFunc(L, Utils.CLS_IDX, "Test2", _m_Test2_xlua_st_); | |
Utils.RegisterFunc(L, Utils.CLS_IDX, "Test4", _m_Test4_xlua_st_); | |
Utils.EndClassRegister(type, L, translator); | |
} |
- C# 这边的字段、方法都会被生成为 wrap 过的静态方法 (字段为两个 get、set 静态方法)
- Wrap 方法主要将 Lua 的访问或赋值操作转换成函数调用形式
- 生成的 wrap 方法是一个接收有一个参数,即接受 Lua 状态机指针 (
System.IntPtr
) 的静态方法
// 实例字段被编译而成的 getter,可视作普通实例方法的调用 | |
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] | |
static int _g_get_Name(RealStatePtr L) | |
{ | |
try { | |
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); | |
Test gen_to_be_invoked = (Test)translator.FastGetCSObj(L, 1); | |
LuaAPI.lua_pushstring(L, gen_to_be_invoked.Name); | |
} catch(System.Exception gen_e) { | |
return LuaAPI.luaL_error(L, "c# exception:" + gen_e); | |
} | |
return 1; | |
} |
- 生成的静态 Wrap 方法从 Lua 虚拟栈通过正数索引取参数值,然后调用实际方法,填入方法参数
- 实例方法编译出来的 lua 调用的方法,会先取缓存列表中实例对象,然后调用对应方法
- 重载函数必须通过同名函数被调用时传递的参数数量 (或类型) 来判断到底应该调用哪个函数
LuaAPI.lua_gettop(L)
获取参数数量(静态与实例均如此,当然若没有重载会省略这一步)- 后续
- 实例对象从
index 1
获取对象类型索引 (userdata),从index 2
开始获取实际方法参数值 - 静态调用直接从
index 1
开始获取参数值
- 实例对象从
// 这是原本就是静态的方法 | |
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] | |
static int _m_Test4_xlua_st_(RealStatePtr L) | |
{ | |
try { | |
{ | |
int _a = LuaAPI.xlua_tointeger(L, 1); | |
bool _b = LuaAPI.lua_toboolean(L, 2); | |
string _c = LuaAPI.lua_tostring(L, 3); | |
Test.Test4( _a, _b, _c ); | |
return 0; | |
} | |
} catch(System.Exception gen_e) { | |
return LuaAPI.luaL_error(L, "c# exception:" + gen_e); | |
} | |
} |
函数返回值为 int,代表返回值数量
- 当 Lua 调用时,会调用 C# 这边的 Wrap 静态方法,并通过索引获取到对应对象,再调用指定方法
- 函数通过 Lua 中的栈来接受 Lua 传递的参数,参数以正序入栈(第一个参数数量首先入栈)
- 因此,当函数开始的时候,
lua_gettop(L)
可以返回函数收到的参数个数 - 并根据正数索引从
索引 1
开始取值
- 因此,当函数开始的时候,
# 数据交互 - C# 调 XLua
- 可以通过数据映射进行
- 映射对象继承自
LuaBase
,如果是接口,并标记了[CSharpCallLua]
特性,则会由XLua
自动生成继承了LuaBase
的桥接代码,该代码与LuaTable
原理一致 - 主要通过
luaenv.Global
全局_G
表获取数据并映射至C#
这边类型- 根据
Tutorial.CSCallLua
示例,对于引用类型映射 (即两边修改同步) 主要有 标记特性接口 和 LuaTable、委托 (加入过生成列表,见LuaFunction.cs
) - 否则普通的类型或直接取值,均通过值传递 (获取后就无关联)
- 根据
- 映射原理
- 例如
Lua 侧 Table
被映射为C# LuaTable
类型 LuaTable
继承自LuaBase
LuaBase
中构造函数接受两个参数:reference
:Lua 中对象索引luaenv
:指定的 Lua 运行环境
- C# 这边通过调用
LuaAPI.luaL_ref(L)
将指定对象放入一张LUA_REGISTRYINDEX
的全局表
- 例如
//ObjectCasters.cs | |
private object getLuaTable(RealStatePtr L, int idx, object target) | |
{ | |
if (LuaAPI.lua_type(L, idx) == LuaTypes.LUA_TUSERDATA) | |
{ | |
object obj = translator.SafeGetCSObj(L, idx); | |
return (obj != null && obj is LuaTable) ? obj : null; | |
} | |
if (!LuaAPI.lua_istable(L, idx)) | |
{ | |
return null; | |
} | |
LuaAPI.lua_pushvalue(L, idx); | |
return new LuaTable(LuaAPI.luaL_ref(L), translator.luaEnv); | |
} |
因为 LuaBase
保存了对象在 LUA_REGISTRYINDEX
表的索引,因此可以再其中通过索引获取 Lua
侧对象,然后再通过虚拟栈进行交互
- 也就是说
reference
指的不是栈上索引,而是这个全局表 (LUA_REGISTRYINDEX
) 中的索引 - 根据相关信息解释,对于该全局表,C 代码可以自由使用,但 Lua 代码不能访问
当获取值时,通过 LuaAPI.lua_getref(L, luaReference)
传入存储的 reference
获取
- 见源码
public partial class LuaTable
中的Get
方法 - 也就是说对于映射的引用类型,并非是直接通过虚拟栈 (Lua 为每次函数调用都新分配了一个栈,因此在离开作用域之后,栈索引就失效了)
- 而是通过保存对象在
LUA_REGISTRYINDEX
中的索引实现映射,在实际调用相关方法或字段时,通过存储的索引获取对应 Lua 表,再通过虚拟栈进行交互 - 注:映射后函数调用与取值都是类似流程,区别在于函数调用会推入参数,调用
LuaAPI.lua_pcall
。见官方Tutorial.CSCallLua.ItfD
生成的TutorialCSCallLuaItfDBridge.add
方法 (委托啥的一样,反正都先要在 Table 取到)
当对象在 C#
这边被回收时,通过 LuaBase
析构函数的 Dispose
方法,调用 luaenv
的 ObjectTranslator.ReleaseLuaBase
将对象从 LUA_REGISTRYINDEX
表中删除 (随后该对象就能受 Lua
侧的垃圾回收了)
# 垃圾回收相关
C# 和 Lua 都有各自的垃圾回收机制,为了避免冲突,当使用了对方代理对象时,代理对象会被缓存,并在 真实对象
被回收后,移除缓存,使 代理对象
也能被回收
# Lua 传递至 C# 的对象
Lua
传递至 C#
的对象,会通过 LuaAPI.luaL_ref
保持引用 (取值也是通过这个) 而不被回收
C#
这边对象被回收后,将其从LUA_REGISTRYINDEX
表中移除使其可以被Lua
垃圾管理器回收
public void ReleaseLuaBase(RealStatePtr L, int reference, bool is_delegate) | |
{ | |
if(is_delegate) | |
{ | |
LuaAPI.xlua_rawgeti(L, LuaIndexes.LUA_REGISTRYINDEX, reference); | |
if (LuaAPI.lua_isnil(L, -1)) | |
{ | |
LuaAPI.lua_pop(L, 1); | |
} | |
else | |
{ | |
LuaAPI.lua_pushvalue(L, -1); | |
LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX); | |
if (LuaAPI.lua_type(L, -1) == LuaTypes.LUA_TNUMBER && LuaAPI.xlua_tointeger(L, -1) == reference) // | |
{ | |
//UnityEngine.Debug.LogWarning("release delegate ref = " + luaReference); | |
LuaAPI.lua_pop(L, 1);// pop LUA_REGISTRYINDEX[func] | |
LuaAPI.lua_pushnil(L); | |
LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX); // LUA_REGISTRYINDEX[func] = nil | |
} | |
else //another Delegate ref the function before the GC tick | |
{ | |
LuaAPI.lua_pop(L, 2); // pop LUA_REGISTRYINDEX[func] & func | |
} | |
} | |
LuaAPI.lua_unref(L, reference); | |
delegate_bridges.Remove(reference); | |
} | |
else | |
{ | |
LuaAPI.lua_unref(L, reference); | |
} | |
} |
# C# 传递至 Lua 的对象
至于 C#
传递至 Lua
的对象,我们知道 C# 这边对象在 Lua 侧会被注册为元表
- 在我们生成的元表数据,即 C# 对象的 Wrap 代码 (或反射生成) 的时候,就会将相关对象被 Lua 回收的回调注册到 Lua 中
LuaAPI.xlua_pushasciistring(L, "__gc"); | |
LuaAPI.lua_pushstdcallcfunction(L, translator.metaFunctions.GcMeta); | |
LuaAPI.lua_rawset(L, -3); |
其中的 translator.metaFunctions.GcMeta(StaticLuaCallbacks)
就是当对象在 Lua
那边回收后,会将回收对象压栈,然后回调到 C#
这边注册的静态函数
随后, C#
这边通过回调传过来的 Lua
状态机指针,通过正向索引从 Lua
虚拟栈中获取到对应对象索引,从缓存列表移除,后续该对象就会受 C#
垃圾回收器回收
[MonoPInvokeCallback(typeof(LuaCSFunction))] | |
public static int LuaGC(RealStatePtr L) | |
{ | |
try | |
{ | |
int udata = LuaAPI.xlua_tocsobj_safe(L, 1); | |
if (udata != -1) | |
{ | |
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); | |
if ( translator != null ) | |
{ | |
translator.collectObject(udata); | |
} | |
} | |
return 0; | |
} | |
catch (Exception e) | |
{ | |
return LuaAPI.luaL_error(L, "c# exception in LuaGC:" + e); | |
} | |
} |
# 问题:关于调用初始化
目前有点搞不清楚的问题就是: CS.UnityEngine.GameObject()
这种代码,实际上是什么时候被初始化的?
在 C# 这边源码中可以明显看到,自动生成的 wrap
代码是在 XLua_Gen_Initer_Register__
通过 ObjectTranslator.DelayWrapLoader
注册的 —— 也就是说并不会立即加载
- 在调用
ObjectTranslator.GetTypeId
才会判断是否注册过元表数据,判断是否反射或调用生成的 wrap 代码进行注册
例如上面提到过的官方示例 local newGameObj2 = CS.UnityEngine.GameObject('helloworld')
创建了一个新的 GameObejct
对象,在调用的时候这个元表应该还没被初始化设置到 Lua 侧
所以应该还有一个东西,让它可以在没有找到的时候,调用 ObjectTranslator.GetTypeId 注册的基本元表数据
- 目前怀疑是:
LuaEnv
构造函数中的对__index
设置的StaticLuaCallbacks.MetaFuncIndex
回调,但是看着又.... 不大确定,因为这里看着是固定加载索引为 2 的 Type,虽然调用了GetTypeId
,不过难道不是只会初始化这一个吗?调用的指定类型呢?光看 C# 这边代码还是相当有点疑惑。
//StaticLuaCallbacks.cs 文件 | |
[MonoPInvokeCallback(typeof(LuaCSFunction))] | |
public static int MetaFuncIndex(RealStatePtr L) | |
{ | |
try | |
{ | |
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L); | |
Type type = translator.FastGetCSObj(L, 2) as Type; | |
if (type == null) | |
{ | |
return LuaAPI.luaL_error(L, "#2 param need a System.Type!"); | |
} | |
//UnityEngine.Debug.Log("============================load type by __index:" + type); | |
//translator.TryDelayWrapLoader(L, type); | |
translator.GetTypeId(L, type); | |
LuaAPI.lua_pushvalue(L, 2); | |
LuaAPI.lua_rawget(L, 1); | |
return 1; | |
} | |
catch (System.Exception e) | |
{ | |
return LuaAPI.luaL_error(L, "c# exception in MetaFuncIndex:" + e); | |
} | |
} | |
//ObjectTranslator.cs 文件 | |
internal object FastGetCSObj(RealStatePtr L,int index) | |
{ | |
return getCsObj(L, index, LuaAPI.xlua_tocsobj_fast(L,index)); | |
} | |
private object getCsObj(RealStatePtr L, int index, int udata) | |
{ | |
object obj; | |
if (udata == -1) | |
{ | |
if (LuaAPI.lua_type(L, index) != LuaTypes.LUA_TUSERDATA) return null; | |
Type type = GetTypeOf(L, index); | |
if (type == typeof(decimal)) | |
{ | |
decimal v; | |
Get(L, index, out v); | |
return v; | |
} | |
GetCSObject get; | |
if (type != null && custom_get_funcs.TryGetValue(type, out get)) | |
{ | |
return get(L, index); | |
} | |
else | |
{ | |
return null; | |
} | |
} | |
else if (objects.TryGetValue(udata, out obj)) | |
{ | |
#if !UNITY_5 && !XLUA_GENERAL && !UNITY_2017 && !UNITY_2017_1_OR_NEWER && !UNITY_2018 | |
if (obj != null && obj is UnityEngine.Object && ((obj as UnityEngine.Object) == null)) | |
{ | |
//throw new UnityEngine.MissingReferenceException("The object of type '"+ obj.GetType().Name +"' has been destroyed but you are still trying to access it."); | |
return null; | |
} | |
#endif | |
return obj; | |
} | |
return null; | |
} |
也许还有另一个可能?那就是这里其实是指的虚拟栈正数索引?但是感觉又不像.... 上边使用 index 传入 GetTypeOf 获取类型的方法如下:
//ObjectTranslator.cs 文件 | |
public Type GetTypeOf(RealStatePtr L, int idx) | |
{ | |
Type type = null; | |
int type_id = LuaAPI.xlua_gettypeid(L, idx); | |
if (type_id != -1) | |
{ | |
typeMap.TryGetValue(type_id, out type); | |
} | |
return type; | |
} |
这里通过 LuaAPI.xlua_gettypeid
获取 type_id,然而 type_id 需求我们先注册了 (也就是) 才会有.... 陷入循环了?
还是说通过 ObjectTranslator.OpenLib
处理的?
后面还有诸如 AddBuildin("CS", StaticLuaCallbacks.LoadCS)
的代码,看着是将『CS』这个注册为一个 Lua 表当做命名空间?所以 Lua 那边调用,都是通过 CS. 调用的
疑惑.... 我们项目本身并不是 XLua 的,所以其实也不是很熟,研究了几天倒是一堆揣测。
# 对猜想的测试
突然想到 XLua C# 这边不是可以直接调试的么,何不直接调调看?硬看代码,不如实际来测试下看看。
# 猜测一:StaticLuaCallbacks.MetaFuncIndex 初始化
实际调试了一下, 初始化 Lua 虚拟机就向 __index
注册的 StaticLuaCallbacks.MetaFuncIndex
确实被调用到了
然后通过在 LuaCallCs 示例的 Lua 脚本前加上 print ,并查看 print 与 StaticLuaCallbacks.MetaFuncIndex 调用顺序:
- 注:print 是在 LuaEnv 初始化时,通过
LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print)
注册的功能函数。 - 结果 GameObject 创建完了,后续 print 都来了都没执行
该猜测 Pass
# 猜测二:ObjectTranslator.OpenLib 中注册的某个处理
直接在 ObjectTranslator.getTypeId
方法里边打断点,所有对象使用先必然先通过这里注册基本元数据,直接查看什么时候来的、怎么来的。
然后来到了... 之前猜测的 ObjectTranslator.OpenLib
中注册的 import_type
,即 StaticLuaCallbacks.ImportType
函数中:
(这好像就符合第二个猜测了)
然后进入 TryDelayWrapLoader
,因为之前我生成过代码,所以 delayWrap
,即之前提到过的生成代码注册的列表存在对应类型:
所以,在 Lua 侧调用不存在对象时,会调到 C# 侧的 import_type
代码,对类型进行实际注册,使其可以被调用。
- 注:LuaEnv init_xlua 会有初始化 __index 元方法的 lua 代码,其中会判断调用 import_type
然后,若 Lua 代码通过 __call
方式调用,则 C# 调用对象注册的创建对应实例方法,创建对应实例,并两者映射起来。
- 后续,不管是调用方法,还是获取变量,均通过正常交互流程进行了!
# 总结
最后,再来梳理一下流程:
Lua 调 C#
- 首先,在创建一个
LuaEnv
环境时,会保存该环境返回的指针,并注册一些初始的公共静态函数- 例如生成的 wrap 代码的延迟注册回调
__Register
从XLuaGenAutoRegister.cs
添加至D
elayWrapLoader`- 注:结构体、枚举等自定义值类型会在
WrapPusher.cs
中单独注册类型(前提是加了[XLua.LuaCallCSharp]
、[GCOptimize]
这类XLua
的特性、或者加到GenConfig
也可以)
- 注:结构体、枚举等自定义值类型会在
- 当
Lua
调用时,若对应类型还未进行实际数据注册,则会调到ObjectTranslator.OpenLib
中注册的import_type
,在该方法中调用注册的__Register
回调去实际注册对象- 实例对象的创建方法
__CreateInstance
也是在此 (__Register回调
) 通过注册到Lua
侧__call
元方法进行
- 实例对象的创建方法
- 而后,就可以实际工作了,查询
typeIdMap
是否存在对应类型,不存在则调用TryDelayWrapLoader
进行类型实际初始化- 注:数组是单独处理的,
LuaEnv
构造函数最后调用的注册 - 注:若没有生成代码,则反射调用,由
Utils.ReflectionWrap
方法注册,即公共的反射调用回调替代生成代码
- 注:数组是单独处理的,
- 调用时还会区分静态和非静态的实例调用 (虽然都是注册的静态 wrap 方法,但实际操作还是有区别的):
- 当
Lua
调过来的时候,会传递LuaEnv
的指针, - 实例调用:
- 通过字典查询得到实际
LuaEnv
对应的ObjectTranslator
- 通过正数索引
1
从Lua虚拟栈
获取对象索引,然后使用索引从ObjectTranslator
获取C#
侧实例对象- 若有调用方法有重载,则通过
lua_gettop
获取参数数量
- 若有调用方法有重载,则通过
- 通过正数索引
2
开始获取实际参数
- 通过字典查询得到实际
- 静态调用:
- 当
Lua
调过来的时候,直接以正数索引从1
开始取参数值- 若有调用方法有重载,则通过
lua_gettop
获取参数数量
- 若有调用方法有重载,则通过
- 当
- 调用实际方法
- 将方法调用结果压栈
- 返回调用方法后,方法的返回值数量
- Lua 侧拿到调用结果
- 例如生成的 wrap 代码的延迟注册回调
- 所以,静态字段或方法与实例的调用流程是一样的
- 两者的主要区别是:是否需要通过额外对象索引参数去查找实际对象
C# 调用 Lua 则通过映射实现
luaenv.Global
(初始化映射的Lua _G
表)- 然后后续则通过调用
luaenv.Global.Get
从_G
表获取数据,并映射至C#
侧对应对象结构- 继承
LuaBase
(添加特性会自动生动对应 wrap 代码) 通过引用映射,两边保持对应索引- 调用对象时,通过去 LUA_REGISTRYINDEX 获取对应对象,并通过虚拟栈传递信息进行实际调用
- 没有继承
LuaBase
的 会通过值传递,获取一次值后两边就无关系了
- 继承
C# 侧缓存的 Lua 对象被缓存至 LUA_REGISTRYINDEX
表
Lua 侧创建的 C# 对象被缓存至 ObjectTranslator.ObjectPool
避免相互之前 GC 导致对象回收,当一边的代理对象被回收后,通知对面从缓存表移除缓存,然后执行真实对象的回收。
最后:
- 对于静态方法,只需要根据虚拟机的
RealStatePtr
指针直接调用 C API 去Lua虚拟栈
取值,然后调用实际方法即可。 - 然而对于实例对象, 除了根据
RealStatePtr
去字典查询一次虚拟机ObjectTranslator
外,还得在ObjectTranslator.objects
中通过对象索引查找实际对象(当然因为ObjectPool
是数组结构,其实还是挺快的),然后通过正向索引从Lua虚拟栈
获取参数并调用 - 因此实例对象的调用,会比静态方法、字典慢些 —— 另外要是只有一个虚拟机环境的需求,是否可以直接把通过字典查
Lua虚拟机
这一步给省掉?毕竟这一步主要是为支持多虚拟机环境,如果没用多虚拟机环境感觉好像可以?
可能写得稍微有点重复啰嗦,毕竟是一边看一边猜测,又一边修改的,不过也算加深映象了。虽然我自己项目还是纯 C# 在搞,不过毕竟公司在推 XLua,研究这个主要是避免别人问起来,都说不出什么深点的原理。
# 参考文档
- 【最详细易懂】C++ 和 Lua 交互总结 鹅厂程序小哥的博客 - CSDN 博客
- lua_pcall 详解_lua lua_pcall_俊哥兜里有糖的博客 - CSDN 博客
- 为什么调用 lua_pcall
- 在 C 语言中调用 lua 实现的回调函数_luaapi.lua_getref 是干什么的_superarhow 的博客 - CSDN 博客
- lua gc 对象复活
- Lua 与 C 交互之 LUA_REGISTRYINDEX(3) - RubbyZhang - 博客园
- 深入 xLua 实现原理之 Lua 如何调用 C# - iwiniwin - 博客园
- 深入 xLua 实现原理之 C# 如何调用 Lua - iwiniwin - 博客园
- 5.1 函数和类型 - luaL_newstate - 《Lua 5.3 参考手册》 - 书栈网・BookStack
- Lua 与 C 语言的互相调用 - 掘金
- lua 源码编译及与 C/C++ 交互调用细节剖析
- lua_touserdata - byfei - 博客园