|
前言
很久以前也有拿着 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 暗示栈顶
- 例如:
Lua 调用 C/C++ 函数
- 将 C++ 的函数包装成可供 Lua 调用的格式
- 接收一个 Lua 状态机指针(IntPtr)的静态方式,该方式返回值为 int,暗示方式返回值数量
- 在 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 API lua_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 转成字节数组传递
- 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表,再通过虚拟栈进行交互
当对象在 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 的,所以其实也不是很熟,研究了几天倒是一堆测度。
<hr/>对猜想的测试
俄然想到 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 代码,对类型进行实际注册,使其可以被调用。
然后,若 Lua 代码通过 __call 方式调用,则 C# 调用对象注册的创建对应实例方式,创建对应实例,并两者映射起来。
- 后续,不管是调用方式,还是获取变量,均通过正常交互流程进行了!
总结
最后,再来梳理一下流程:
Lua 调 C#
- 首先,在创建一个 LuaEnv 环境时,会保留该环境返回的指针,并注册一些初始的公共静态函数
- 例如生成的 wrap 代码的延迟注册回调 __Register 从 XLuaGenAutoRegister.cs 添加至 DelayWrapLoader`
- 注:布局体、枚举等自定义值类型会在 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 侧拿到调用成果
- 所以,静态字段或方式与实例的调用流程是一样的
- 两者的主要区别是:是否需要通过额外对象索引参数去查找实际对象
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 - 博客园
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|