Lua篇 — ToLua

Posted by Xun on Wednesday, November 3, 2021

ToLua是很多项目都使用的lua框架,为了更好地使用,需要对框架有比较清晰的认识。

简介

  • 所有的Lua框架,都是为了解决热更的问题。但无论代码框架怎么设计,都避免不了需要和CSharp侧交互。而一旦出现交互,就需要了解整体的结构和运作流程,才能更好地使用。
  • ToLua的整体结构,主要是以表的形式存在,大致如下:
表类型 说明 内容
_G 全局表,可直接操作 可自定义设置,如:
_G[“TestPanel”] = { }
-10000
(LUA_REGISTRYINDEX)
注册表,只能通过ref和unref操作 tolua在这里定义了多类表,常用的有:

4(LUA_RIDX_UBOX):存放lua引用C#对象的userdata

5(LUA_RIDX_FIXEDMAP):存放被C#引用的对象,如table、function

7(LUA_RIDX_PACKVEC3):指向lua的Vector3.New方法

26(LUA_RIDX_LOADED):存放C#类对应的lua表

另外,注册表还存放了C#的类结构绑定到lua侧的信息,如GameObject、Component等类在C#的各种属性、方法等。
  • 通过上表,可以对ToLua的结构有一个基本的概念。接下来将对整个框架进行分析,从而对ToLua能有一个更加全面的认识。

初始化

创建LuaState

  • Lua状态机创建方法主要实现如下
    public LuaState()            
    {
    	...
        InitTypeTraits();
        InitStackTraits();
        L = LuaNewState();            
        LuaException.Init(L);
        stateMap.Add(L, this);                        
        OpenToLuaLibs();            
        ToLua.OpenLibs(L);
        OpenBaseLibs();
    	...
    }       
  • InitTypeTraits()
    • 注册常用类型检查函数。
  • InitStackTraits()
    • 注册常用类型数据入栈和栈上数据弹出解析函数 。
  • OpenToLuaLibs()
    • 打开指定的标准库,注册方法到全局表,包括package、preload、loadlib、seeall、module、require等。
    • 创建int64和uint64的table,注册到registry表中的LUA_RIDX_INT64和LUA_RIDX_UINT64中。
    • 注册其他到全局表,如:Mathf、tolua等。
  • ToLua.OpenLibs(L)
    • AddLuaLoader方法,初始化loader,将lua侧的package_loaders[2]绑定为CSharp的ToLua.Loader方法。lua侧执行require时,会遍历所有loader(即 package_loaders)尝试加载,因此package_loaders[2],即CSharp的ToLua.Loader会加载对应的lua脚本,所以可以通过修改ToLua.Loader的流程来自定义加载。
    • 往全局表中注册print、dofile、loadfile方法。
    • 往tolua表中注册isnull(判断是否为Lua侧的nil或者CSharp侧null)、typeof、tolstring(CSharp侧的数据转lua字符串)、toarray。
  • OpenBaseLibs()
    • 注册System、LuaInterface、UnityEngine模块,其中List和Dictionary没有注册创建方法。
    • 创建Layer表到全局表中,将LayerMask的层级添加到Layer表中。
    • 初始化反射相关,加载mscorlib和UnityEngine程序集并缓存,注册反射相关方法到tolua表中,包括findtype、loadassembly、getmethod、getconstructor、gettypemethod、getfield、getproperty、createinstance。
    • 注册CSharp反射相关Wrap的初始化方法,key值为tolua.reflection。
  • 创建完成后,lua侧大致为:
	_G(即 -10002) = {
		Mathf = {
			NextPowerOfTwo = tolua.c 中的 mathf_nextpoweroftwo 方法,
    		ClosestPowerOfTwo = tolua.c 中的 mathf_closestpoweroftwo 方法,
    		IsPowerOfTwo = tolua.c 中的 mathf_ispoweroftwo 方法,
    		GammaToLinearSpace = tolua.c 中的 mathf_gammatolinearspace 方法,
    		LinearToGammaSpace = tolua.c 中的 mathf_lineartogammaspace 方法,
    		Normalize = tolua.c 中的 mathf_normalize 方法,
    		NULL = NULL 
		}
		
		tolua = {
			gettime = tolua.c 中的 tolua_gettime 方法,
    		typename = tolua.c 中的 tolua_bnd_type 方法,
    		setpeer = tolua.c 中的 tolua_bnd_setpeer 方法,
    		getpeer = tolua.c 中的 tolua_bnd_getpeer 方法,
    		getfunction = tolua.c 中的 tolua_bnd_getfunction 方法,
    		initset = tolua.c 中的 tolua_initsettable 方法,
    		initget = tolua.c 中的 tolua_initgettable 方法,
    		int64 = tolua.c 中的 tolua_newint64 方法,
    		uint64 = tolua.c 中的 tolua_newuint64 方法,
    		initset = tolua.c 中的 tolua_initsettable 方法,

			gettag = &gettag ,
			settag = &settag ,
			
			version = 版本号信息(如:1.0.7),

			isnull = CSharp侧的 ToLua.IsNull 方法(用于lua侧判断CSharp对象是否为空),
			typeof = CSharp侧的 ToLua.GetClassType 方法,
			tolstring = CSharp侧的 ToLua.BufferToString 方法,
			toarray = CSharp侧的 ToLua.TableToArray 方法,

    		NULL = NULL 
		}

		print = CSharp侧的 ToLua.Print 方法,
		dofile = CSharp侧的 ToLua.DoFile 方法,
		loadfile = CSharp侧的 ToLua.LoadFile 方法,
		...
	}

初始化相关库

  • lua侧需要使用的相关库函数,要进行初始化
    • luaopen_pb
    • luaopen_struct
    • luaopen_lpeg
    • cjson(可选)

绑定

  • CSharp的相关方法要进行绑定,即注册到lua侧
    protected virtual void Bind()
    {        
        LuaBinder.Bind(luaState);
        DelegateFactory.Init();   
        LuaCoroutine.Register(luaState, this);        
    }
public static class LuaBinder
{
    ... 

	public static void Bind(LuaState L)
	{
        ...
		L.BeginModule("UnityEngine");
		UnityEngine_ComponentWrap.Register(L);
		UnityEngine_TransformWrap.Register(L);
		...
        L.BeginModule("Events");
		L.RegFunction("UnityAction", UnityEngine_Events_UnityAction);
		L.EndModule();
        ...
		L.EndModule();
		L.BeginPreLoad();
		L.AddPreLoad("UnityEngine.MeshRenderer", LuaOpen_UnityEngine_MeshRenderer, typeof(UnityEngine.MeshRenderer));
		L.AddPreLoad("UnityEngine.BoxCollider", LuaOpen_UnityEngine_BoxCollider, typeof(UnityEngine.BoxCollider));
		...
		L.EndPreLoad();
		...
	}

    ...
  • LuaBinder.Bind 主要的方法有:
    • L.BeginModule(…)
      • 调用lua的tolua_beginmodule方法,将CSharp中的namespace转化为table表示。如:namespace为A.B,则会创建table A存到全局表中,table B存到table A中,B.name = “A.B”。
    • L.EndModule()
      • 调用lua的tolua_endmodule方法,将栈上的table弹出(beginmodule执行完后栈上会留下当前table),结束module创建,并更新stringbuffer。
    • XXXWrap.Register(L)
      • XXXWrap:CSharp侧的类生成的Wrap文件,用于将CSharp类注册到lua中。生成格式为 命名空间_类名 + Wrap,如:命名空间为A.B,类名为C,则生成的Wrap为 A_B_CWrap。
    • L.BeginClass(…)
      • 如果基类没有注册过,则在Lua侧创建一个table,放到registry表(索引为LUA_REGISTRYINDEX)中。
      • 在CSharp建立table和基类的映射关系(metaMap、typeMap、genericSet)。
      • 调用lua的tolua_beginclass方法,创建一个table(tb_loaded),放到registry表中的已加载表(索引为LUA_RIDX_LOADED)中,key值为命名空间+类名,如:A.B.C。
      • 如果当前类没有创建过table,则创建一个新的table(tb_type)。
      • 将基类table设置为当前类table的metatable。
      • 注册元方法__call为class_new_event,主要实现获取元表里的New方法,并将参数传入后调起。
      • 设置__index和__newindex元方法。
      • CSharp注册__gc元方法为CSharp的Collect方法。
      • 在CSharp侧建立table和当前类的映射关系。
    • L.EndClass()
      • 调用lua的tolua_endclass方法,将当前类的table(tb_type)设置为LUA_RIDX_LOADED中table(tb_loaded)的metatable,再将LUA_RIDX_LOADED中table(tb_loaded)设置到当前module的table中,key值为类名,即module中不会直接创建当前类的table,而是持有当前类table的引用。因此获取数据时,则是通过查找LUA_RIDX_LOADED中table(tb_loaded),触发当前类table(tb_type)的__index方法获取数据的。
    • L.RegFunction(…)
      • 将CSharp类方法转成LuaCSFunction,再获取方法指针。
      • 调用lua的tolua_function,将方法注册到当前类的table中,key值为方法名。
    • L.RegVar(…)
      • 将CSharp类的public变量和属性转成get和set的LuaCSFunction,再获取方法指针。
      • 调用lua的tolua_variable,创建get和set的table,存入当前类的table中,key值为lightuserdata的&gettag和&settag。
      • 把get和set的方法存入各自的table中,key值为属性名。
    • L.BeginPreLoad()
      • 获取全局表中的package,找到表里的preload,将preload表入栈。
    • L.AddPreLoad(…)
      • CSharp侧将类型type加入preLoadMap字典中,值为对应的LuaCSFunction。
      • 将LuaCSFunction压入preload表中,key值为命名空间 + 类名,如:A.B。
      • 调用lua的tolua_addpreload方法,将命名空间作为module在全局表上创建对应的table。
      • 在CSharp侧将命名空间加入moduleSet,保证相同命名空间只创建一次table。
    • L.EndPreLoad()
      • 将package和preload出栈。实际上并没有直接注册lua表,而是等到调用对应的LuaCSFunstion时,再通过BeginPreModule和EndPreModule进行注册。
    • L.BeginEnum(…)
      • 调用lua的tolua_beginenum,创建一个table(tb_loaded)加入loaded中。
      • 创建一个table(tb_enum),放入registry表中。
      • 设置table(tb_enum)的name、__index、__newindex、__gc。
      • 在CSharp侧建立table(tb_enum)和当前类的映射关系。
      • 完成BeginEnum后,将Enum的每个值作为变量注册到table(tb_enum)中,设置get方法。
      • 所有Enum变量注册完后,注册IntToEnum方法到table(tb_enum)中,可将int转成lua侧的Enum表示。
    • L.EndEnum()
      • 将当前Enum的table(tb_enum)设置为LUA_RIDX_LOADED中table(tb_loaded)的metatable,再将LUA_RIDX_LOADED中table(tb_loaded)设置到当前module的table中,key值为Enum名。
      • 完成EndEnum后,设置CSharp的ypeTraits.Check和StackTraits.Push。
  • 以UnityEngine.Component和UnityEngine.Space为例,进行绑定后,Lua侧的大致表结构如下
	_G = {
		UnityEngine = 
			table = {
				__index = tolua.c中的module_index_event,
				.name = "UnityEngine",

				Component =  LUA_REGISTRYINDEX表中LUA_RIDX_LOADED表的UnityEngine.Component,
				Space = LUA_REGISTRYINDEX表中LUA_RIDX_LOADED表的UnityEngine.Space,
				...
			}
			metatable = table
		
		...
	}

	-10000(即 LUA_REGISTRYINDEX) = {

		...

		26(即 LUA_RIDX_LOADED) {
			UnityEngine.Component 
				table = {},
				metatable = ref_Component
				
			UnityEngine.Space 
				table = {},
				metatable = ref_SpaceEnum
		},	

		...

		ref_Component(分配的id,UnityEngine.Component)
			table = {
				.name = "UnityEngine.Component",
				ref = ref_Component分配的id,
				&tag = 1,

				GetComponent = CSharp的UnityEngine_ComponentWrap.GetComponent,
				TryGetComponent = CSharp的UnityEngine_ComponentWrap.TryGetComponent,
				...
				New = CSharp的UnityEngine_ComponentWrap._CreateUnityEngine_Component,

				&gettag = {
					transform = CSharp的UnityEngine_ComponentWrap.get_transform,
					gameObject = CSharp的UnityEngine_ComponentWrap.get_gameObject,
					tag = CSharp的UnityEngine_ComponentWrap.get_tag
				},

				&settag = {
					tag = CSharp的UnityEngine_ComponentWrap.set_tag
				},

				__call = tolua.c中的class_new_event,
				__index = tolua.c中的class_index_event,
				__newindex = tolua.c中的class_newindex_event,
				__gc = CSharp的LuaState.Collect,
				__eq = CSharp的UnityEngine_ComponentWrap.op_Equality,
				__tostring = CSharp的ToLua.op_ToString
			},
			metatable = ref_UObject
		
		ref_UObject(分配的id,UnityEngine.Object)
			table = {
				...
			},
			metatable = {
				...
			}
		
		ref_SpaceEnum(分配的id,UnityEngine.Space)
			table = {
				.name = "Space"
				__index = tolua.c中的enum_index_event,
				__newindex = tolua.c中的enum_newindex_event,
				__gc = CSharp的LuaState.Collect,
			},
		...
	}

	...
  • lua 侧检查某个 C# 类、方法、属性、字段是否有生成 Wrap ,可以通过以下方式:
	local function TypeWrapped(typeName)
		local wrap = false
		local wrapType = _G
		local result = pcall(function()
			for key in string.gmatch(typeName, "[^%.]+") do 
				wrapType = wrapType[key]
			end
			wrap = wrapType ~= nil and wrapType ~= _G 
		end)
		if not wrap then
			wrapType = nil
		end
		return wrap, wrapType
	end

	local function PropertyOrFieldWrapped(wrapType, memberName)
		local wrap = false
		if wrapType ~= nil then
			local gettag = wrapType[tolua.gettag]
			if gettag ~= nil and gettag[memberName] ~= nil then
				wrap = true
			end
		end
		return wrap
	end

	local function MethodWrapped(wrapType, methodName)
		local wrap = false
		if wrapType ~= nil and wrapType[methodName] ~= nil then
			wrap = true
		end
		return wrap
	end
  • DelegateFactory 对委托相关的类型进行管理
public class DelegateFactory
{
	public static void Init()
	{
		Register();
	}

	public static void Register()
	{
		dict.Clear();
		dict.Add(typeof(System.Action), factory.System_Action);
		dict.Add(typeof(UnityEngine.Events.UnityAction), factory.UnityEngine_Events_UnityAction);
		...

		DelegateTraits<System.Action>.Init(factory.System_Action);
		DelegateTraits<UnityEngine.Events.UnityAction>.Init(factory.UnityEngine_Events_UnityAction);
		...

		TypeTraits<System.Action>.Init(factory.Check_System_Action);
		TypeTraits<UnityEngine.Events.UnityAction>.Init(factory.Check_UnityEngine_Events_UnityAction);
		...

		StackTraits<System.Action>.Push = factory.Push_System_Action;
		StackTraits<UnityEngine.Events.UnityAction>.Push = factory.Push_UnityEngine_Events_UnityAction;
		...
	}
}
  • DelegateFactory.Init

    • 完成委托类型的注册
      • dict.Add(…)
        • 在CSharp侧,将委托类型和委托创建方法作为key-value加入字典中
      • DelegateTraits.Init(…)
        • 在CSharp侧,使用泛型记录委托创建方法,可直接调用
      • TypeTraits.Init(…)
        • 在CSharp侧,使用泛型将委托的类型检查方法添加到对应类型的Check,Check方法只返回true或者false,不返回对象,多用于重载变量类型检查
      • StackTraits.Push = …
        • 在CSharp侧,使用泛型将委托的入栈方法添加到对应类型的Push
        • StackTraits也有Check,但返回的一个对应的T
    • 委托创建方法,继承LuaDelegate,流程为
      • 先创建LuaDelegate对象,将LuaFunction设置到LuaDelegate的func中
      • 设置LuaDelegate的method为CSharp的Call或CallWithSelf
      • 以LuaFunction的ref为key,将LuaDelegate以弱引用形式(WeakReference)缓存到delegateMap中
  • DelegateTraits

    • Init:在CSharp侧,将各种委托类型的委托创建方法加入字典中

访问对象

Lua访问CSharp对象

  • Lua侧获取一个CSharp侧对象,常用有几种方式
    • CSharp侧将对象注册到Lua的对应table。
    • 使用CSharp的静态方法,如:GameObject.Find 等。
    • 使用对象的CSharp的new创建。
  • 无论使用那种方式,最终都是通过 ToLua.PushUserData 方法来建立映射关系。
	public static void PushUserData(IntPtr L, object o, int reference)
    {
        int index;
        ObjectTranslator translator = ObjectTranslator.Get(L);
		// 检查CSharp是否已经对这个对象建立了索引
        if (translator.Getudata(o, out index))
        {
			// 将索引对应的userdata压到Lua栈上
            if (LuaDLL.tolua_pushudata(L, index))
            {
                return;
            }
			// 如果lua侧已经没有这个userdata,则移除CSharp侧的 ObjectTranslator.objects[index].obj,但此index还保持,等lua侧gc触发才能真正移除。
			// 因为lua侧gc是延迟触发的,移除LUA_RIDX_UBOX表中的userdata后,在lua侧gc触发前可能同一个对象又重新关联到lua侧,需要分配一个新的index,避免旧的index触发CSharp侧GC方法错误移除正在使用的对象。
            translator.Destroyudata(index);
        }
		// CSharp侧为object创建索引index_GameObject
        index = translator.AddObject(o);
		// 在Lua侧创建userdata,userdata的值设置为index_GameObject
        LuaDLL.tolua_pushnewudata(L, reference, index);
    }
  • CSharp调用lua的tolua_pushnewudata创建userdata,主要流程为
    • 创建一个userdata结构(userdata_GameObject),值为index_GameObject,类型为LUA_TUSERDATA。
    • 将ref_GameObject设置为userdata_GameObject的metabtable。
    • 将userdata_GameObject存到LUA_REGISTRYINDEX表中的LUA_RIDX_UBOX表,key值为index_GameObject,和userdata_GameObject的值一致。
    • 保留userdata_GameObject在栈顶(tolua_pushudata方法也是从LUA_RIDX_UBOX表中查找userdata_GameObject并压入栈,即userdata_GameObject位于栈顶)。
  • 此时,Lua栈顶放着userdata_GameObject,而我们要对此GameObject进行操作时,如获取GameObject的activeSelf,则
    • 查找TestPanel表的GameObject_A,拿到userdata_GameObject。
    • 调用userdata_GameObject.activeSelf,即查询metatable的__index。
    • 找到CSharp的UnityEngine_GameObjectWrap.get_activeSelf方法并调用。
    • ToLua.ToObject调用lua的tolua_rawnetobj方法,查询当前栈上的userdata的值,即CSharp侧的GameObject对象对应的index_GameObject。
    • 从ObjectTranslator中获取对应的GameObject,拿到activeSelf值。
    • 调用lua_pushboolean将activeSelf传到lua侧。
  • 示例代码大致如下:
	-- TestPanel.lua
	local state = TestPanel.GameObject_A.activeSelf
	// UnityEngine_GameObjectWrap.cs

	...

	[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
	static int get_activeSelf(IntPtr L)
	{
		object o = null;

		try
		{
			o = ToLua.ToObject(L, 1);
			UnityEngine.GameObject obj = (UnityEngine.GameObject)o;
			bool ret = obj.activeSelf;
			LuaDLL.lua_pushboolean(L, ret);
			return 1;
		}
		catch(Exception e)
		{
			return LuaDLL.toluaL_exception(L, e, o, "attempt to index activeSelf on a nil value");
		}
	}

	...
	// ToLua.cs

	...

	public static object ToObject(IntPtr L, int stackPos)
    {
        int udata = LuaDLL.tolua_rawnetobj(L, stackPos);

        if (udata != -1)
        {
            ObjectTranslator translator = ObjectTranslator.Get(L);
            return translator.GetObject(udata);
        }

        return null;
    }

	...
  • Lua侧此时的结构大致如下所示
	_G = {

		TestPanel = {
				GameObject_A : userdata_GameObject(从 LUA_RIDX_UBOX 中获取)
				...
			}
	}

	-10000(即 LUA_REGISTRYINDEX) = {
		...

		4(即 LUA_RIDX_UBOX) = {

			__mode = v (弱表,value为弱引用)

			index_GameObject(CSharp侧的一个UnityEngine.GameObject,index为ObjectTranslator分配)= userdata_GameObject {
					
					(头部 Udata*){
						...
						tt = LUA_TUSERDATA
						metatable = LUA_REGISTRYINDEX.ref_GameObject
					}
					(用户自定义数据 user domain*){
						index_GameObject
					}
				}
		}

		ref_GameObject(分配的id,UnityEngine.GameObject)
			table = {
				...
				&gettag = {
					...
					activeSelf = CSharp的UnityEngine_GameObjectWrap.get_activeSelf,
					...
				},
			},
			metatable = ref_UObject


		...
	}
  • 当我们的TestPanel不再使用时,TestPanel表会删除(TestPanel = nil),此时整个table会变成可回收,而表里的变量也同样会标记成可回收,即userdata_GameObject也触发了luaL_unref方法,移除了userdata_GameObject引用,此时userdata_GameObject只有在 LUA_RIDX_UBOX 中有引用,此时的结构如下:
Lua侧
	_G = {

	}

	-10000(即 LUA_REGISTRYINDEX) = {
		...

		4(即 LUA_RIDX_UBOX) = {
			
			index_GameObject(CSharp侧的一个UnityEngine.GameObject,index为ObjectTranslator分配)= userdata_GameObject {
					
					(头部 Udata*){
						...
						tt = LUA_TUSERDATA
						metatable = LUA_REGISTRYINDEX.ref_GameObject
					}
					(用户自定义数据 user domain*){
						index_GameObject
					}
				}
			
			...
		}
		...
	}

CSharp侧
	ObjectTranslator.objectsBackMap = {
		{ go , index_GameObject },
		...
	}

	ObjectTranslator.objects[index_GameObject] = go
  • 由于 LUA_RIDX_UBOX 表为值弱表,所以当lua gc触发时,userdata_GameObject由于没有其他引用,所以会被回收,并调起 userdata_GameObject.metatable的.__gc (即 LUA_REGISTRYINDEX.ref_GameObject.__gc) 方法,从而调起CSharp的LuaState.Collect方法,删除 ObjectTranslator.objects 和 ObjectTranslator.objectsBackMap 的引用。
  • CSharp侧的GC方法为:
		// LuaState.cs
		...
        [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
        public static int Collect(IntPtr L)
        {
            int udata = LuaDLL.tolua_rawnetobj(L, 1);

            if (udata != -1)
            {
                ObjectTranslator translator = GetTranslator(L);
                translator.RemoveObject(udata);
            }

            return 0;
        }
		// ObjectTranslator.cs
		...

        public void RemoveObject(int udata)
        {            
            //只有lua gc才能移除
            object o = objects.Remove(udata);
            Debug.LogFormat("lua gc RemoveObject, udata = {0}" , udata);
            if (o != null)
            {
                if (!TypeChecker.IsValueType(o.GetType()))
                {
                    RemoveObject(o, udata);
                }

                if (LogGC)
                {
                    Debugger.Log("gc object {0}, id {1}", o, udata);
                }
            }
        }

        void RemoveObject(object o, int udata)
        {
            int index = -1;
            
            if (objectsBackMap.TryGetValue(o, out index) && index == udata)
            {
                objectsBackMap.Remove(o);
            }
        }
  • 可以看到,CSharp侧的GC流程为
    • 调用lua侧的 tolua_rawnetobj 方法,获取对象对应的userdata对应的index。
    • 移除CSharp侧 ObjectTranslator.objects 中的index。
    • 如果 ObjectTranslator.objectsBackMap 中的index和要GC的index不一致,说明此对象已经绑定到新的index,不需要移除引用。
  • 对于枚举对象,userdata 的 metatable 不为对应 Type 的 metatable,而是统一使用 System.Enum 对应的 metatable。
		// ToLua.cs
		...

        public static void Push(IntPtr L, System.Enum e)
        {
            object obj = null;
            int enumMetatable = LuaStatic.GetEnumObject(L, e, out obj);
            PushUserData(L, obj, enumMetatable);
        }
		// LuaStatic.cs
		...

		public static int GetEnumObject(IntPtr L, System.Enum e, out object obj)
        {
            LuaState state = LuaState.Get(L);
            obj = state.GetEnumObj(e);
            return state.EnumMetatable;
        }    
		// LuaState.cs
		...

		void OpenBaseLibs()
        {            
            ...
            System_EnumWrap.Register(this);

            ...
            EnumMetatable = metaMap[typeof(System.Enum)];
            
			...
        }
  • 其中,枚举值 userdata 对象,经常需要获取其对应的值来进行逻辑处理,System_EnumWrap 中增加了 ToInt 方法,能将枚举对象转成 int 值。
	local intValue = UnityEngine.Space.Self:ToInt()
  • 为了释放已经销毁的UnityEngine.Object的引用关系,CSharp侧提供了一个 LuaState.StepCollect() 方法,放在Update中触发,可以检查已经销毁的对象,移除 ObjectTranslator.objectsBackMap 的已销毁对象,避免字典频繁扩容。
  • 总的来说,lua侧对CSharp侧对象的引用,只有lua gc触发的时候,才能在CSharp侧对应释放掉引用。

CSharp访问Lua对象

  • 项目中往往需要将部分逻辑放到CSharp侧编写,因此不可避免有时候会需要读取Lua侧的对象,以LuaTable表示,如:
    • CSharp侧创建Lua的table(创建并注册数据给Lua侧使用)。
    • Lua侧调用CSharp方法传入table。
  • 当我们要获取一个Lua侧的table时,通常为
    stackPos = LuaDLL.abs_index(L, stackPos);
	// 将栈上stackPos的lua table压到栈上
    LuaDLL.lua_pushvalue(L, stackPos);
	// 获取lua侧的table的引用
    int reference = LuaDLL.toluaL_ref(L);
	// CSharp侧获取对应的LuaTable
    return LuaStatic.GetTable(L, reference);
  • 其中,lua侧的 toluaL_ref 方法,先通过stackPos对应的值,查找 LUA_RIDX_FIXEDMAP 表中是否存在有这个key值,如果存在则返回对应的value,即table的reference。如果 LUA_RIDX_FIXEDMAP 表中不存在,则在 LUA_REGISTRYINDEX 中创建一个引用reference,将table作为key,reference作为value,存入 LUA_RIDX_FIXEDMAP 表,然后返回reference值,后续再获取就直接从 LUA_RIDX_FIXEDMAP 表中即可。
  • 可以看出,lua侧并不会记录CSharp侧对table有多少引用,只保持一次引用,所以需要CSharp侧来管理引用情况。
	// LuaState.cs

    public LuaTable GetTable(int reference)
    {
        LuaTable table = TryGetLuaRef(reference) as LuaTable;

        if (table == null)
        {                
            table = new LuaTable(reference, this);
            funcRefMap.Add(reference, new WeakReference(table));
        }

        RemoveFromGCList(reference);
        return table;
    }
  • 在CSharp侧,LuaTable都在LuaState.funcRefMap中缓存,每次调用TryGetLuaRef,都会触发LuaBaseRef.AddRef,使LuaBaseRef.count的值+1,表示引用加1。此时的结构如下:
Lua侧
	_G = {

	}

	-10000(即 LUA_REGISTRYINDEX) = {
		...

		5(即 LUA_RIDX_FIXEDMAP) = {
			testtable = test_ref
		}

		test_ref = testtable (由lua侧的 luaL_ref 方法创建的映射)
		...
	}

CSharp侧
	LuaState.funcRefMap = {
		{ test_ref , new WeakReference(LuaTable) },
		...
	}

	ObjectTranslator.objects[index_GameObject] = go
  • 当CSharp侧不再使用此LuaTable,则会调用LuaTable.Dispose方法,释放对应的LuaTable。
	// LuaBaseRef.cs

    public virtual void Dispose()
    {
        --count;

        if (count > 0)
        {
            return;
        }

        IsAlive = false;
        Dispose(true);            
    }

	...

    public virtual void Dispose(bool disposeManagedResources)
    {
        if (!beDisposed)
        {
            beDisposed = true;   

            if (reference > 0 && luaState != null)
            {
                luaState.CollectRef(reference, name, !disposeManagedResources);
            }
           
            reference = -1;
            luaState = null;
            count = 0;
        }            
    }

  • 当调用Dispose时,会检查是否还有引用,count > 0,表示还有其他地方引用了这个对象。当没有引用了,则会调用luaState.CollectRef释放LuaTable,从LuaState.funcRefMap中移除,并调起lua侧的toluaL_unref移除引用。
  • lua侧的toluaL_unref方法,通过获取 LUA_REGISTRYINDEX 表的 reference对应的table,将 LUA_RIDX_FIXEDMAP 表key值为table的value设为nil,即移除此key-value,然后再移除 LUA_REGISTRYINDEX 表对此table的引用。
  • 然而,可以发现,当我们的LuaTable如果没有正确执行Dispose方法,则会出现count一直大于0的情况,则无法释放LuaTable,即便我们已经没有再使用它。因此,LuaBaseRef对此做了处理。
	// LuaBaseRef.cs

    ~LuaBaseRef()
    {
        IsAlive = false;
        Dispose(false);
    }

  • LuaBaseRef在析构函数中,也做了释放,将LuaTable(LuaBaseRef)加入LuaState.gcList中。在Update方法中,执行LuaState.Collect方法,释放CSharp侧的LuaTable和lua侧的对应table。
  • 由于LuaTable在CSharp侧,是以弱引用存在于LuaState.funcRefMap中,所以当CSharp侧触发GC(Unity切换场景或主动执行System.GC.Collect方法等)时,由于CSharp侧对LuaTable没有直接引用持有,所以无论当前的引用count是多少,LuaTable都会被回收,进而触发LuaBaseRef的析构函数,实现了释放。
  • 总的来说,LuaTable能释放的情况有以下情况
    • LuaTable为局部变量,方法执行完成后就触发析构函数(Unity中才会触发局部变量的析构函数)释放(立即释放)。
    • LuaTable为成员变量,手动执行了LuaTable.Dispose方法,LuaTable.count为0,luaState.CollectRef执行进行释放(立即释放)。
    • LuaTable为MonoBehavior的成员变量,对应的GameObject销毁后,Mono对象没有其他引用,无论LuaTable.count为多少,都会触发LuaTable析构函数释放(延迟释放)。
    • LuaTable为普通Class的成员变量,引用class的对象置为null后,无论LuaTable.count为多少,都会触发LuaTable析构函数释放(延迟释放)。
    • LuaTable没有直接引用时,触发CSharp的GC,触发析构函数释放(立即释放)。
  • 因此,为了保证能立即释放,有以下方式
    • 不直接引用LuaTable,即保持所有LuaTable都为局部变量。
    • 将LuaTable设为成员变量,则CSharp侧都通过此成员变量获取相同的table,而不再从lua侧重新检查获取,count只有1,只对此LuaTable执行Dispose。
    • 不同对象从lua侧获取同一个table,都作为成员变量,对所有LuaTable都执行Dispose,保证count减少为0。

时序图

  • Lua侧访问CSharp对象的时序图为 LuaCallCSharpObject.png
  • CSharp访问Lua对象的时序图为 CSharpCallLuaObject.png

调用方法

Lua调用CSharp方法

  • 初始化的时候,LuaBinder将CSharp的class对应创建了table存到了 LUA_REGISTRYINDEX 表中,而CSharp的class,对应的各种方法也已经进行了绑定,调用的时候,通过查找_G表,即可找到对应方法,以GameObject为例
	_G = {
		UnityEngine = {
			GameObject =  LUA_REGISTRYINDEX表中LUA_RIDX_LOADED表的UnityEngine.GameObject,
			.name = "UnityEngine"
			...
		}
		
		...
	}

	-10000(即 LUA_REGISTRYINDEX) = {

		...

		26(即 LUA_RIDX_LOADED) {
			UnityEngine.GameObject 
				table = {},
				metatable = ref_GameObject
				
			UnityEngine.Space 
				table = {},
				metatable = ref_SpaceEnum
		},	

		...

		ref_GameObject(分配的id,UnityEngine.GameObject)
			table = {
				...
				Find = CSharp的UnityEngine_GameObjectWrap.Find,

			},
			metatable = ref_UObject

		
		ref_UObject(分配的id,UnityEngine.Object)
			table = {
				...
			},
			metatable = {
				...
			}

		...
	}

  • 当想要调用GameObject.Find时,在lua侧需要执行 UnityEngine.GameObject.Find(“test”),则最终会调用CSharp侧的UnityEngine_GameObjectWrap.Find,读取栈上的参数string,并调起UnityEngine.GameObject.Find,最后再把GameObject传回lua。
	// UnityEngine_GameObjectWrap.cs

	[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
	static int Find(IntPtr L)
	{
		try
		{
			ToLua.CheckArgsCount(L, 1);
			string arg0 = ToLua.CheckString(L, 1);
			UnityEngine.GameObject o = UnityEngine.GameObject.Find(arg0);
			ToLua.PushSealed(L, o);
			return 1;
		}
		catch (Exception e)
		{
			return LuaDLL.toluaL_exception(L, e);
		}
	}

	...

CSharp调用Lua方法

  • 和Lua对象一样,有时候也需要调用Lua侧的方法,主要是通过table获取,以LuaFunction表示,如:
    • 直接通过方法名,找到对应表中的方法。
    • 找到某个LuaTable,查找表中的方法。
  • 当我们要获取一个Lua侧的function时,通常为
	// ToLua.cs
	...

	stackPos = LuaDLL.abs_index(L, stackPos);
	// 将栈上stackPos的lua function压到栈上
    LuaDLL.lua_pushvalue(L, stackPos);
	// 获取lua侧的function的引用
	int reference = LuaDLL.toluaL_ref(L);
	// CSharp侧获取对应的LuaFunction
	return LuaStatic.GetFunction(L, reference);

  • 和LuaTable一样,function也会对应存入 LUA_RIDX_FIXEDMAP 表和 LUA_REGISTRYINDEX 表。销毁机制也和LuaTable一致。

委托

  • Lua侧的业务,往往需要调用CSharp的逻辑,如加载某些资源。资源通过CSharp代码进行加载,加载完成后,就执行CSharp的回调。由于我们需要执行lua侧的后续逻辑,就需要设置lua侧的方法作为CSharp侧的回调,因此需要创建对应类型的委托。前面提到,DelegateFactory.Init 完成委托类型的初始化,其中 DelegateTraits 注册了各种类型的委托。通过调用 ToLua.CheckDelegate 即可创建对应委托方法,设置时的方法主要为:
	// ToLua.cs
	...
	// 获取栈上的lua function
	LuaFunction func = ToLua.ToLuaFunction(L, stackPos);
	// 创建对应类型的静态方法委托
    return DelegateTraits<T>.Create(func);

  • 注意,所有的lua侧委托方法都是静态方法,如果方法中引用了自身table(self),即产生了闭包,则table释放的时候,需要将CSharp侧对此方法的引用置为null,table才能被lua gc回收。否则即便table已经在CSharp侧Dispose了,但由于委托方法还引用了table,而CSharp还引用着方法,导致table还是不能释放。
  • lua方法对应委托创建时会存入 LuaState.delegateMap 中,同样以WeakReference(Delegate.Target)的形式存在。key值有两种情况
    • 静态方法:key值为对应的LuaFunction的reference。
    • 成员方法:key值为64位,高位32位为LuaFunction的reference,低位32位为LuaTable的reference。
  • 可见,LuaTable和LuaFunction都会被LuaDelegate引用(LuaDelegate的变量),所以如果LuaDelegate还有引用,则LuaTable和LuaFunction也不能释放。而由于lua侧的委托方法都是静态方法,所以CSharp不会引用LuaTable,也避免了LuaTable无法释放引起的其他资源卸载问题。而当lua侧的function触发gc时,由于是静态方法,所以直接通过reference就能移除 LuaState.delegateMap 中的引用。
  • 在场景加载完成后,由于CSharp侧的GC触发,LuaDelegate如果没有直接引用则会被释放,之后通过 LuaState.RefreshDelegateMap 方法,可以将delegateMap中已经释放的LuaDelegate移除,避免delegateMap频繁扩容。

重载

  • CSharp中存在重载函数的情况,而Lua是弱语言类型,当我们在Lua侧调用一个CSharp方法时,如果此方法是重载的,则我们不能确定应该要调起哪一个对应的方法。因此在生成Wrap文件时,会对重载方法进行处理,通过检查参数个数和类型,来确定应该调用哪个方法。生成重载方法的主要流程为,获取class中所有同名方法,按一定规则进行排序,并确定需要进行类型判断的参数位置。
  • 排序的规则如下:
    • 无可选参数的方法排在前面。
    • 成员方法非可选参数数量+1,参数数量不一样的,数量少的方法排在前面。参数数量一样的,最后一个非可选参数的类型不是object容器的方法排在前面。
    • 成员方法总参数数量+1,数量小的方法排在前面。
    • 第一个参数为基本类型(bool、byte、int等)的,排在前面,为object的排在后面。
    • 从第二个参数开始,基本类型 > long > ulong > object。
  • 确定需要进行类型判断的方法为:
    • 参数数量不一样,不用进行类型判断。
    • 参数数量一样的方法,逐一检查参数,相同的不用则进行类型判断,直到找到第一个不一样的,从这个参数起所有都需要进行类型判断检查。
  • 前面提到,TypeTraits.Init 初始化注册了检查类型的方法,通过 TypeTraits.Check 方法可以检查lua方法的参数类型,类型匹配则通过ToLua.ToObject读取对象,强转成对应的类型。
  • 参数可以传委托方法,当我们没有重载或重载的参数数量不一样时,对应的委托类型参数则使用 ToLua.CheckDelegate 来创建。而当出现参数数量一致,且委托类型参数是需要检查类型的,则会通过ToLua.ToObject来获取对象。以System.Action类型为例,检查类型的方法和获取对象为:
	// ToLua.cs
	...
	bool Check_System_Action(IntPtr L, int pos)
	{
		return TypeChecker.CheckDelegateType(typeof(System.Action), L, pos);
	}

	// TypeChecker.cs
	...

    static public bool CheckDelegateType(Type type, IntPtr L, int pos)
    {
        LuaTypes luaType = LuaDLL.lua_type(L, pos);

        switch (luaType)
        {
            case LuaTypes.LUA_TNIL:
                return true;
            case LuaTypes.LUA_TUSERDATA:                    
                int udata = LuaDLL.tolua_rawnetobj(L, pos);

                if (udata != -1)
                {
                    ObjectTranslator translator = ObjectTranslator.Get(L);
                    object obj = translator.GetObject(udata);
                    return obj == null ? true : type == obj.GetType();
                }
                return false;
            default:
                return false;
        }
    }

	// ToLua.cs
	...
    public static object ToObject(IntPtr L, int stackPos)
    {
        int udata = LuaDLL.tolua_rawnetobj(L, stackPos);

        if (udata != -1)
        {
            ObjectTranslator translator = ObjectTranslator.Get(L);
            return translator.GetObject(udata);
        }

        return null;
    }
  • 可以看到,检查类型的方法只检查了userdata类型和nil,也就是说,System.Action类型的参数,必须是userdata类型才能匹配,即必须是Lua侧持有的CSharp侧对象。如果传入的参数为lua侧的方法时,则由于类型检查不匹配(LuaTypes.LUA_TFUNCTION)而无法调起。同样,ToLua.ToObject 方法也是直接通过 udata 获取 ObjectTranslator.objects 的对应对象,所以当出现这种情况时,lua侧的方法是不能正常调起的,这也是目前重载会遇到的问题。
  • 总的来说,为了避免出现重载引起CSharp侧不能调起lua方法的问题,可以有以下方法:
    • 避免使用重载函数。
    • 增加新的重载函数,将所有原有的重载函数参数全组合到新的重载函数中,并将原来的重载函数逻辑整合到此新重载函数中,而原来旧的重载函数使用 [NoToLua] 特性,不生成Wrap,则在lua侧只有这个新的重载方法类型可以传入。

无GC传值

  • 前面说到,Lua侧获取CSharp对象是通过userdata的形式,即获取对应的id,而CSharp侧将id和对象存入ObjectTranslator.objects中,由于 ObjectTranslator.objects 中存的对象类型为object,对于值类型的对象,使用object就会需要装拆箱的操作,还会产生gc,因此对于此类对象,则需要有无gc的传值方法,来提升效率。
  • 对于大部分基本类型,如:bool、int,Lua和CSharp可以通过c的api进行交互,如:
    • lua_pushboolean/lua_toboolean
    • lua_pushinteger/lua_tointeger
  • 通过这些方法,将数据压到栈上,或从栈上读取,则实现了数据交互,而不会产生gc。而无gc传值也是基于这种方法,通过规定struct的数据类型和顺序,则可实现入栈和出栈的配套方法。尤其是对频繁调用的类型,如UnityEngine.Vector3,效率上会有很大的提升。
  • 以Vector3为例,CSharp侧对Vector3创建了Push(将Vector3数据压入栈中)、Check(检查并创建Vector3对象)、To(获取Vector3对象)三个方法:
	// ToLua.cs
	...
	public static void Push(IntPtr L, Vector3 v3)
    {
        LuaDLL.tolua_pushvec3(L, v3.x, v3.y, v3.z);
    }

    static public Vector3 CheckVector3(IntPtr L, int stackPos)
    {
        int type = LuaDLL.tolua_getvaluetype(L, stackPos);

        if (type != LuaValueType.Vector3)
        {
            LuaDLL.luaL_typerror(L, stackPos, "Vector3", LuaValueTypeName.Get(type));
            return Vector3.zero;
        }

        float x, y, z;
        LuaDLL.tolua_getvec3(L, stackPos, out x, out y, out z);
        return new Vector3(x, y, z);
    }	

    public static Vector3 ToVector3(IntPtr L, int stackPos)
    {
        float x = 0, y = 0, z = 0;
        LuaDLL.tolua_getvec3(L, stackPos, out x, out y, out z);
        return new Vector3(x, y, z);
    }	

  • LuaState.OpenBaseLuaLibs 方法,会调用lua侧的 tolua_openluavec3 方法,获取全局表里的 Vector3.New 方法,设置为 LUA_REGISTRYINDEX 表里的 LUA_RIDX_PACKVEC3,Vector3.Get 方法则设置为 LUA_REGISTRYINDEX 表里的 LUA_RIDX_UNPACKVEC3,即
	_G = {
		Vector3 = lua侧的Vector3.lua
		
		...
	}

	-10000(即 LUA_REGISTRYINDEX) = {

		...

		"_LOADED" = {
			UnityEngine.Vector3 = lua侧的Vector3.lua
		}

		7(即 LUA_RIDX_PACKVEC3) = Vector3.New
		8(即 LUA_RIDX_UNPACKVEC3)= Vector3.Get
		...
	}
  • lua侧的 tolua_getvec3 方法,会获取 LUA_RIDX_UNPACKVEC3 方法,调用 Vector3.Get 将lua侧的Vector3的x、y、z压入栈中。
  • lua侧的 tolua_pushvec3 方法,会获取 LUA_RIDX_PACKVEC3 方法,将参数x、y、z传入,调用 Vector3.New 创建lua侧的Vector3。
  • lua侧实现了Vector3对象,对UnityEngine.Vector3的一些常用计算方法在Vector3.lua中做,来避免频繁和CSharp交互。

泛型

  • CSharp侧调用Lua侧方法时,LuaFunction提供了两种方法,可简化CSharp侧的调用方式,且不会产生GC。
    • 无返回值:Call,支持9个参数以内(包括9个参数)的泛型调用。
    • 一个返回值(泛型R1):Invoke,支持9个参数以内(包括9个参数)的泛型调用。
  • Lua侧调用CSharp的泛型方法,此方法需要带泛型类型的参数,并且泛型类型需要限定为引用类型,否则工具方法不会生成Wrap代码,无法调用。
  • 前面提到,LuaState.OpenBaseLibs中,List、Dictionary注册了Wrap,即可以在Lua侧调用对应的操作,但是没有注册创建的方法,所以无法在Lua侧创建此类对象。
  • 如果没有提前生成Wrap代码,泛型方法可以通过反射方式,调用 Type.MakeGenericMethod 方法,创建泛型方法的泛型实例方法,再直接调起实例方法,即完成对泛型方法的调用。但是如果泛型类型为值类型,且CSharp侧没有调用过对应类型的泛型方法,则在 AOT 模式下,编译器不会提前编译出对应类型的方法,导致无法正常调用。可以通过增加强制编译方法,来保证对应类型代码会被编译。
    private void Compile()
    {
        var o = new TestGeneric();
        o.Test2<int>();
        o.Test2<float>();
        o.Test2<bool>();
        ...
        o.Test3<object, int>();
        o.Test3<object, float>();
        o.Test3<object, bool>();
        ...
        o.Test3<int, object>();
        o.Test3<float, object>();
        o.Test3<bool, object>();
    }

反射

  • 为了支持反射,CSharp侧提供了LuaReflection,通过 OpenLibs 方法注册反射相关方法。
    • tolua.findtype:查找类名获取对应类型。
    • tolua.loadassembly:加载指定名称的程序集。
    • tolua.getmethod:获取类型对应的方法,封装成 LuaMethod,再转成userdata给lua侧,方便lua侧执行方法。
    • tolua.getconstructor:获取类型对应的构造函数,封装成LuaConstructor,再转成userdata给lua侧。
    • tolua.gettypemethod:获取类型对应的方法,带多参数。
    • tolua.getfield:获取成员变量,封装成 LuaField,再转成userdata给lua侧,方便lua侧获取内容。
    • tolua.getproperty:获取属性,封装成 LuaProperty,再转成userdata给lua侧,方便lua侧获取内容。
    • tolua.createinstance:创建类型示例。
    • tolua.reflection:初始化LuaMethod、LuaProperty、LuaField、LuaConstructor的Wrap文件注册,需要初始化后才能正常反射。
  • 使用 tolua.findtype 方法,一般需要先添加 Assembly-CSharp 程序集。Assembly-CSharp 程序集没有在 LuaReflection 的构造函数中添加到list中,避免放在插件目录无法加载,需要可从lua代码 loadassembly。如果不清楚类型名,可在 CSharp 侧使用 Type.FullName 得到具体类名。
	tolua.loadassembly("Assembly-CSharp")
	tolua.findtype("XXXXX")
  • 对于枚举对象,枚举对象的值需要用 GetHashCode 方法获取。反射获取流程如下
	tolua.loadassembly("Assembly-CSharp")
	local enumType = tolua.findtype('XXXEnum')
	if enumType ~= nil and enumType.IsEnum then
		-- 获取枚举
		-- 方法一
		local valueArray = System.Enum.GetValues(enumType)
		for	i = 0,valueArray.Length - 1 do
			-- 枚举名
			local enumKey = valueArray:GetValue(i):ToString()
			-- 枚举值
			local enumValue = valueArray:GetValue(i):GetHashCode()
		end

		-- 方法二
		local nameArray = System.Enum.GetNames(enumType)
		for i = 0,nameArray.Length - 1 do
			-- 枚举名
			local enumKey = nameArray[i]
			-- 枚举值
			local enumValue = System.Enum.Parse(enumType, enumKey):GetHashCode()
		end
	end
  • 由于这里使用反射获取,会有装拆箱问题,所以最好调用一次后将结果缓存起来。
  • 对于反射CSharp侧的方法、属性、字段、构造函数,则流程如下:
	-- 初始化调用一次,注册反射相关Wrap(默认没有初始化,未调用则无法使用 LuaInterface 下的内容)
	package.preload['tolua.reflection']()

	local type = tolua.findtype("xxxx类名")
	-- 方法调用
	local method = tolua.gettypemethod(type, "xxxx方法名")
	method:Call(参数)

	-- 属性调用
	local property = tolua.getproperty(type, "xxx属性名")
    local instance = property:Get(实例对象, nil)

	-- 变量调用
	local field = tolua.getfield(type, "xxx变量名")
	local field:Get(实例对象)

64位整型

  • 前面提到,在CSharp的 OpenToLuaLibs 方法中,调用了lua侧的 tolua_openlibs 进行初始化库,其中对lua侧的int64和uint64进行了初始化。
    • tolua_openint64
    • tolua_openuint64
  • 初始化完后,在lua侧的结构大致如下:
	_G = {
		...

		int64 = LUA_REGISTRYINDEX 表的 LUA_RIDX_INT64 表
		uint64 = LUA_REGISTRYINDEX 表的 LUA_RIDX_UINT64 表
	}

	26(即 LUA_RIDX_LOADED) {
			...

			int64 = LUA_REGISTRYINDEX 表的 LUA_RIDX_UINT64 表
		},	

	-10000(即 LUA_REGISTRYINDEX) = {
		...

		20(即 LUA_RIDX_INT64) = {
			__add = int64.c的 _int64add 方法
			__sub = int64.c的 _int64sub 方法
			__mul = int64.c的 _int64mul 方法
			__div = int64.c的 _int64div 方法
			__mod = int64.c的 _int64mod 方法
			__unm = int64.c的 _int64unm 方法
			__pow = int64.c的 _int64pow 方法
			__tostring = int64.c的 _int64tostring 方法
			tostring = int64.c的 _int64tostring 方法
			__eq = int64.c的 _int64eq 方法
			__lt = int64.c的 _int64lt 方法
			__le = int64.c的 _int64le 方法
			.name = "int64"
			new = int64.c的 tolua_newint64 方法
			equals = int64.c的 _int64equals 方法
			tonum2 = int64.c的 _int64tonum 方法
			__index = LUA_REGISTRYINDEX 表的 LUA_RIDX_INT64 表
		}

		27(即 LUA_RIDX_UINT64 = {
			__add = uint64.c的 _uint64add 方法
			__sub = uint64.c的 _uint64sub 方法
			__mul = uint64.c的 _uint64mul 方法
			__div = uint64.c的 _uint64div 方法
			__mod = uint64.c的 _uint64mod 方法
			__unm = uint64.c的 _uint64unm 方法
			__pow = uint64.c的 _uint64pow 方法
			__tostring = uint64.c的 _uint64tostring 方法
			tostring = uint64.c的 _uint64tostring 方法
			__eq = uint64.c的 _uint64eq 方法
			__lt = uint64.c的 _uint64lt 方法
			__le = uint64.c的 _uint64le 方法
			.name = "uint64"
			new = uint64.c的 tolua_newuint64 方法
			equals = uint64.c的 _uint64equals 方法
			tonum2 = uint64.c的 _uint64tonum 方法
			__index = LUA_REGISTRYINDEX 表的 LUA_RIDX_UINT64 表
		}
	}
  • 以int64为例,从CSharp侧传到Lua侧时,调用 tolua_pushint64 方法,创建userdata,将int64的值设为userdata的值,将 LUA_RIDX_INT64 设为userdata的metatable,即
	userdata_int64 = {
				
		(头部 Udata*){
				...
				tt = LUA_TUSERDATA
				metatable = LUA_REGISTRYINDEX.LUA_RIDX_INT64
		}
		(用户自定义数据 user domain*){
			int64的值
		}
	}
  • 而从Lua侧传到CSharp侧时,则调用 tolua_toint64,lua侧的int64有几种格式,LUA_TNUMBER、LUA_TSTRING、LUA_TUSERDATA,将lua侧的数据解析成int64在传回CSharp使用。

协程

  • 推荐使用lua的协程。

接口(Interface)

  • 暂时不支持
    • ToLuaExport.Generate 工具生成方法中, 除了 System.Collections.IEnumerator ,其他 Interface 都直接返回不处理。

热修复(暖更新)

  • 尽管tolua的设计是为了让开发者能将绝大部分逻辑都在lua侧实现,但还是不可避免需要将部分逻辑放到CSharp侧编写,因此也就出现了需要线上修复CSharp代码的需求。

修复类型

  • LuaInterface.InjectType 中定义了修复的各种类型:
    • After(1 « 0):CSharp侧方法执行后执行Lua侧注册的方法。
    • Before(1 « 1):CSharp侧方法执行前执行Lua侧注册的方法。
    • Replace(1 « 2):CSharp侧方法不执行,替换为执行Lua侧注册的方法。
    • ReplaceWithPreInvokeBase(1 « 3):CSharp侧方法不执行,替换为执行Lua侧注册的方法,并在Lua方法执行前,执行一次CSharp侧的的Base同名函数。
    • ReplaceWithPostInvokeBase(1 « 4):CSharp侧方法不执行,替换为执行Lua侧注册的方法,并在Lua方法执行后,执行一次CSharp侧的的Base同名函数。

宏定义

  • 热修复的相关代码,主要通过宏 ENABLE_LUA_INJECTION 来控制开关。

代码注入

  • ToLuaInjection.Inject 方法,加载 “./Library/ScriptAssemblies/Assembly-CSharp.dll” 程序集,根据修复类型注入IL代码。
  • 以某个类为参考,注入前代码如下:
public class TestInject : MonoBehaviour
{
    private int m_Id = 1;

    private void Log()
    {
        Debug.Log(m_Id);
    }

    private int GetId()
    {
        return m_Id;
    }
}

  • 通过查看 Assembly-CSharp.dll ,注入完成后的代码如下:
// Token: 0x0200000C RID: 12
public class TestInject : MonoBehaviour
{
	// Token: 0x0600001C RID: 28 RVA: 0x00002D2C File Offset: 0x00000F2C
	private void Log()
	{
		byte injectFlag = LuaInjectionStation.GetInjectFlag(414);
		LuaFunction injectionFunction;
		if (injectFlag != 0)
		{
			injectionFunction = LuaInjectionStation.GetInjectionFunction(414);
			if (injectFlag > 1)
			{
				injectionFunction.Call<TestInject>(this);
				if (injectFlag > 2)
				{
					return;
				}
			}
		}
		Debug.Log(this.m_Id);
		if (injectFlag == 1)
		{
			injectionFunction.Call<TestInject>(this);
		}
	}

	// Token: 0x0600001D RID: 29 RVA: 0x00002D80 File Offset: 0x00000F80
	private int GetId()
	{
		byte injectFlag = LuaInjectionStation.GetInjectFlag(413);
		LuaFunction injectionFunction;
		if (injectFlag != 0)
		{
			injectionFunction = LuaInjectionStation.GetInjectionFunction(413);
			if (injectFlag > 1)
			{
				int result = injectionFunction.Invoke<TestInject, int>(this);
				if (injectFlag > 2)
				{
					return result;
				}
			}
		}
		int id = this.m_Id;
		int result2 = id;
		if (injectFlag == 1)
		{
			injectionFunction.Call<TestInject>(this);
		}
		return result2;
	}

	// Token: 0x04000015 RID: 21
	private int m_Id = 1;
}
  • 可以看到,经过Inject后,会对方法的逻辑进行修改,根据index获取修复类型,根据修复类型获取对应方法,再根据修复类型确定是替换还是插入执行。
  • 对于一些私有的变量、方法等,如果没有提前生成Wrap文件,就需要使用反射获取。

初始化

  • 当lua代码加载完成后,会回调执行 LuaState.Start 方法,如果开启了热修复,则会进行热修复的初始化流程:
    • 设置 LUA_GLOBALSINDEX 表的 tolua_tag 为 tolua.c 中的 tolua_tag 方法(lightuserdata类型)。
    • 执行 LuaState.DoFile 方法,加载并执行 “System/Injection/LuaInjectionStation.lua” 。
    • 设置 LuaState.bInjectionInited 为 true。

相关说明

  • LuaInjectionStation.lua
    • 热修复lua侧启动入口,加载热修复需要的脚本:
      • require “System.Injection.LuaInjectionBus”
      • local bridgeInfo = require “System.Injection.InjectionBridgeInfo”
    • 提供热修复lua侧主要方法:
      • InjectByName
        • 从 bridgeInfo 表中,查找是否有对应的 moduleName 表。
        • 遍历lua侧进行热修复对应的table,检查方法是否在对应的 moduleName 表中。
        • 调用CSharp侧的 LuaInjectStation.CacheInjectFunction 方法,将需要热修复的方法的index、修复类型injectFlag、修复方法func注册到CSharp侧。
          • LuaInjectionStation.injectFunctionCache[index] = func
          • LuaInjectionStation.injectionFlagCache[index] = injectFlag
      • InjectByModule
        • 获取对应module,检查 &tag 是否为1,为1表示CSharp类已经绑定到Lua侧。
        • 使用 module[".name"],调用 InjectByName 方法进行热修复注册。
        • 如果是替换类型(Replace、ReplaceWithPostInvokeBase、ReplaceWithPreInvokeBase),修改 module 的 __index 方法。
          • 如果 module 中有替换类型的方法,则返回对应方法。
          • 如果 module 中没有替换类型的方法,则调用原有的 __index 方法返回对应方法。
  • LuaInjectionBus.lua
    • 热修复脚本注册入口,所有需要注册热修复的脚本都需要在这里进行注册,如:
      • require “System.Injection.ToLuaInjectionTestInjector”
  • InjectionBridgeInfo.lua
    • 记录CSharp侧能进行热修复的方法和对应的索引index。

时序图

  • 热修复的时序图为 Inject.png

生成工具

  • (待补充)

总结

  • 总体来说,ToLua作为使用相对广泛的框架,对于以lua代码为主的项目,能很好地解决热更问题,并且复杂度比较低,比较容易上手。
  • 通过整体的介绍,将对象的交互流程比较形象地展现出来,可以更清晰地了解数据的流向,也能更好地管理。
  • ToLua主要是通过提前生成Wrap代码的方式,让lua侧能够调用CSharp侧的对象。因此如果没有生成Wrap,就需要通过反射来进行调用,效率会变低。
  • 对于重载变量为委托的问题,不容易被发现,所以需要谨慎使用重载。
  • 由于ToLua的设计理念是以lua为主体进行开发,因此在热修复上,还是有一些不足,例如:不能在lua侧创建List、Dictionary,没有注册int、float等类型不能直接使用,不支持接口等,需要开发者对框架有比较深入的了解,才能避免出现问题。