tolua源码之小白剖析(三)

接下来我们就沿着tolua官方提供的例子,逐个进行研究吧。02_ScriptsFromFile我们略过不看,无非是通过文件的方式进行lua加载,我们直接来看03_CallLuaFunction,04_AccessingLuaVariables,在C#中调用lua函数,和在C#中访问lua变量。


我们先看怎么在C#层访问lua方法,先贴上demo源码(我去掉一些无关紧要的代码部分):

using UnityEngine;
using LuaInterface;
using System;

public class CallLuaFunction : MonoBehaviour
{
    private LuaState lua;
    private LuaFunction luaFunc;
    
    private string script =
        @"  function luaFunc(num)                        
                return num + 1
            end

            test = {}
            test.luaFunc = luaFunc
        ";
	
    void Start () 
    {
        lua = new LuaState();
        lua.Start();
        DelegateFactory.Init();        
        lua.DoString(script);

        //Get the function object
        luaFunc = lua.GetFunction("test.luaFunc");

        if (luaFunc != null)
        {
            int num = luaFunc.Invoke<int, int>(123456);
            Debugger.Log("generic call return: {0}", num);

            num = CallFunc();
            Debugger.Log("expansion call return: {0}", num);

            Func<int, int> Func = luaFunc.ToDelegate<Func<int, int>>();
            num = Func(123456);
            Debugger.Log("Delegate call return: {0}", num);
            
            num = lua.Invoke<int, int>("test.luaFunc", 123456, true);
            Debugger.Log("luastate call return: {0}", num);
        }

        lua.CheckTop();
    }

    void OnDestroy()
    {
        if (luaFunc != null)
        {
            luaFunc.Dispose();
            luaFunc = null;
        }

        lua.Dispose();
        lua = null;
    }

    int CallFunc()
    {        
        luaFunc.BeginPCall();                
        luaFunc.Push(123456);
        luaFunc.PCall();        
        int num = (int)luaFunc.CheckNumber();
        luaFunc.EndPCall();
        return num;                
    }
}

输出:
image


        lua = new LuaState();
        lua.Start();

初始化Lua虚拟机,这部分上一节已经分析过了,这里就不再重复赘述了。

        DelegateFactory.Init();
	public delegate Delegate DelegateCreate(LuaFunction func, LuaTable self, bool flag);
	public static Dictionary<Type, DelegateCreate> dict = new Dictionary<Type, DelegateCreate>();
	static DelegateFactory factory = new DelegateFactory();

	public static void Init()
	{
		Register();
	}
	
	public static void Register()
	{
		dict.Clear();
		dict.Add(typeof(System.Action), factory.System_Action);
		......

		DelegateTraits<System.Action>.Init(factory.System_Action);
		......

		TypeTraits<System.Action>.Init(factory.Check_System_Action);
		......

		StackTraits<System.Action>.Push = factory.Push_System_Action;
		......
	}

dict绑定:key-委托类型,val-DelegateCreate(自定义委托类型,接收lua的方法,lua的table,一个bool标识,标记该方法调用时,是否用了self语法语法糖,为true才会用到table参数。最终返回一个委托)。
DelegateTraits绑定:与dict类似,不过DelegateTraits运用起来更方便点,通过泛型就能拿到注册进去的自定义委托类型。
TypeTraits绑定:前一节有提到,用来检查栈上指定位置的元素,是否是注册进去的委托类型。
StackTraits绑定:前一节有提到,用来将某个委托类型的object压入到lua栈上。
总结:通过这一系列的注册,C#层就能将Lua层的方法转换为一个委托进行调用,并添加一系列压入、检查等操作。


        lua.DoString(script);

        //Get the function object
        luaFunc = lua.GetFunction("test.luaFunc");

lua.DoString(script)会执行我们定义的lua脚本,然后通过GetFunction取出lua方法,我们重点关注一下这个是怎么实现的。

        public LuaFunction GetFunction(string name, bool beLogMiss = true)
        {
            WeakReference weak = null;

            if (funcMap.TryGetValue(name, out weak))
            {
                if (weak.IsAlive)
                {
                    LuaFunction func = weak.Target as LuaFunction;
                    CheckNull(func, "{0} not a lua function", name);

                    if (func.IsAlive)
                    {
                        func.AddRef();
                        RemoveFromGCList(func.GetReference());
                        return func;
                    }
                }

                funcMap.Remove(name);
            }

            if (PushLuaFunction(name, false))
            {
                int reference = ToLuaRef();

                if (funcRefMap.TryGetValue(reference, out weak))
                {
                    if (weak.IsAlive)
                    {
                        LuaFunction func = weak.Target as LuaFunction;
                        CheckNull(func, "{0} not a lua function", name);

                        if (func.IsAlive)
                        {
                            funcMap.Add(name, weak);
                            func.AddRef();
                            RemoveFromGCList(reference);
                            return func;
                        }
                    }

                    funcRefMap.Remove(reference);
                    delegateMap.Remove(reference);
                }
                
                LuaFunction fun = new LuaFunction(reference, this);
                fun.name = name;
                funcMap.Add(name, new WeakReference(fun));
                funcRefMap.Add(reference, new WeakReference(fun));
                RemoveFromGCList(reference);
                if (LogGC) Debugger.Log("Alloc LuaFunction name {0}, id {1}", name, reference);                
                return fun;
            }

            if (beLogMiss)
            {
                Debugger.Log("Lua function {0} not exists", name);                
            }

            return null;
        }

因为是第一次执行,我们从 if (PushLuaFunction(name, false))处开始阅读:

        bool PushLuaFunction(string fullPath, bool checkMap = true)
        {
            if (checkMap)
            {
                WeakReference weak = null;

                if (funcMap.TryGetValue(fullPath, out weak))
                {
                    if (weak.IsAlive)
                    {
                        LuaFunction func = weak.Target as LuaFunction;
                        CheckNull(func, "{0} not a lua function", fullPath);

                        if (func.IsAlive)
                        {
                            func.AddRef();
                            return true;
                        }
                    }

                    funcMap.Remove(fullPath);
                }
            }

            int oldTop = LuaDLL.lua_gettop(L);
            int pos = fullPath.LastIndexOf('.');

            if (pos > 0)
            {
                string tableName = fullPath.Substring(0, pos);

                if (PushLuaTable(tableName, checkMap))
                {
                    string funcName = fullPath.Substring(pos + 1);
                    LuaDLL.lua_pushstring(L, funcName);
                    LuaDLL.lua_rawget(L, -2);

                    LuaTypes type = LuaDLL.lua_type(L, -1);

                    if (type == LuaTypes.LUA_TFUNCTION)
                    {
                        LuaDLL.lua_insert(L, oldTop + 1);
                        LuaDLL.lua_settop(L, oldTop + 1);
                        return true;
                    }
                }

                LuaDLL.lua_settop(L, oldTop);
                return false;
            }
            else
            {
                LuaDLL.lua_getglobal(L, fullPath);
                LuaTypes type = LuaDLL.lua_type(L, -1);

                if (type != LuaTypes.LUA_TFUNCTION)
                {
                    LuaDLL.lua_settop(L, oldTop);
                    return false;
                }
            }

            return true;
        }

checkMap参数表示我们是否还要从funcMap检查,因为之前已经检查过一次没有,所以这里直接传false,不做二次检查。我们从int oldTop = LuaDLL.lua_gettop(L);处开始阅读。

  1. 这里首先做了一个fullPath.LastIndexOf('.');操作,判断一下我们要取的方法是全局变量,还是某个表中的某个方法,我们这里明显是某表中的方法。
  2. 将test表压入栈中,再压入'luaFunc'字符串,取出该lua对象,并再判断一次该对象类型是不是luaFunc类型。

取出lua函数后,我们回到GetFunction方法来,紧接着会调用int reference = ToLuaRef();这个方法的作用是在Lua注册表中为栈顶元素创建或获取一个唯一引用标识符(整数),然后才会实例化LuaFuntion,每个LuaFunction都会存储着唯一的reference值。接着将该LuaFunction存进funcMap和funcRefMap中去,避免下次调用还要重复生成。
image


LuaFunction我们知道怎么获取后,我们接下来看看它是怎么调用的。首先看第一个方法:

            int num = luaFunc.Invoke<int, int>(123456);
            Debugger.Log("generic call return: {0}", num);

image
为 Lua 函数的调用初始化堆栈环境
image
将参数压入栈中
image
执行函数
image

image
取出栈顶元素,并调用我们之前注册过的类型检查,判断类型是否正确
image
结束函数调用。
然后我们再看看另一个函数执行方法:

    num = CallFunc();
    Debugger.Log("expansion call return: {0}", num);
			
    int CallFunc()
    {        
        luaFunc.BeginPCall();                
        luaFunc.Push(123456);
        luaFunc.PCall();        
        int num = (int)luaFunc.CheckNumber();
        luaFunc.EndPCall();
        return num;                
    }

这个跟前一个执行方法类似,为 Lua 函数的调用初始化堆栈环境,将参数压入栈中,执行函数,取出栈顶元素并检查。
然后我们再来看看第三个执行方法:

            Func<int, int> Func = luaFunc.ToDelegate<Func<int, int>>();
            num = Func(123456);
            Debugger.Log("Delegate call return: {0}", num);

image
这里调用了我们最开始绑定的方法:
image

image

image
因为我们调用的是test.luaFunc(),没有用到Lua语法糖,所以执行这个方法,最终返回的肯定是System_Func_int_int_Event.Call方法啦。可以看到整个call方法就封装了那些我们之前分析的方法执行流程。
image
那我们这个Create最终也是返回了这个方法,并将它缓存起来,第二次可以迅速获取。
然后我们再来看看第四个执行方法:

            num = lua.Invoke<int, int>("test.luaFunc", 123456, true);
            Debugger.Log("luastate call return: {0}", num);

image
与前三者类似,就不细细分析了。


最后,做个C#执行lua层方法的总结吧。

  1. 首先找到lua层中对应的函数,并给该函数存储一个全局唯一的reference值,返回到C#层。
  2. 在C#层实例化一个LuaFunction类,并将refernce值存入该对象,作为C#层和C层的函数关系映射。
  3. 在C#层执行lua方法时,首先会用到之前缓存起来的reference在c层准备lua堆栈好。--- luaState.BeginPCall
  4. 如果该lua方法需要参数,C#层压入对应的参数。 --- luaState.PushGeneric;
  5. 最后执行这个方法 --- luaState.PCall
  6. 方法如果有返回值会留在lua栈顶,我们可以取出并检查。--- luaState.CheckValue
  7. 最后结束lua函数运算,结束lua堆栈。--- luaState.EndPCall

然后我们再来看看04_AccessingLuaVariables实例,看看C#层是怎么访问lua层变量的。还是一样,先贴上源码:

using UnityEngine;
using LuaInterface;

public class AccessingLuaVariables : MonoBehaviour 
{
    private string script =
        @"
            print('Objs2Spawn is: '..Objs2Spawn)
            var2read = 42
            varTable = {1,2,3,4,5}
            varTable.default = 1
            varTable.map = {}
            varTable.map.name = 'map'
            
            meta = {name = 'meta'}
            setmetatable(varTable, meta)
            
            function TestFunc(strs)
                print('get func by variable')
            end
        ";

	void Start () 
    {
        LuaState lua = new LuaState();
        lua.Start();
        lua["Objs2Spawn"] = 5;
        lua.DoString(script);

        //通过LuaState访问
        Debugger.Log("Read var from lua: {0}", lua["var2read"]);
        Debugger.Log("Read table var from lua: {0}", lua["varTable.default"]);  //LuaState 拆串式table

        LuaFunction func = lua["TestFunc"] as LuaFunction;
        func.Call();
        func.Dispose();

        //cache成LuaTable进行访问
        LuaTable table = lua.GetTable("varTable");
        Debugger.Log("Read varTable from lua, default: {0} name: {1}", table["default"], table["map.name"]);
        table["map.name"] = "new";  //table 字符串只能是key
        Debugger.Log("Modify varTable name: {0}", table["map.name"]);

        table.AddTable("newmap");
        LuaTable table1 = (LuaTable)table["newmap"];
        table1["name"] = "table1";
        Debugger.Log("varTable.newmap name: {0}", table1["name"]);
        table1.Dispose();

        table1 = table.GetMetaTable();

        if (table1 != null)
        {
            Debugger.Log("varTable metatable name: {0}", table1["name"]);
        }

        object[] list = table.ToArray();

        for (int i = 0; i < list.Length; i++)
        {
            Debugger.Log("varTable[{0}], is {1}", i, list[i]);
        }

        table.Dispose();                        
        lua.CheckTop();
        lua.Dispose();
	}
}

输出结果:
image


        LuaState lua = new LuaState();
        lua.Start();
        lua["Objs2Spawn"] = 5;

启动lua虚拟机,前面已经讨论过了,这里不再重复赘述,关注的是第三句,luaState定义了一个有参属性,可以将luaState视作_G来使用:
image
代码分2部分,上面是给_G.xxx.xx赋值,会优先调用LuaFindTable找到xxx表,下面是给_G.x赋值。PushVariant就是将值压入栈中,第二句就是将值赋值给我们指定的对象。

        public void PushVariant(object obj)
        {
            ToLua.Push(L, obj);
        }
		
        public static void Push(IntPtr L, object obj)
        {
            if (obj == null || obj.Equals(null))
            {
                LuaDLL.lua_pushnil(L);
                return;
            }

            Type t = obj.GetType();

            if (t.IsValueType)
            {
                if (TypeChecker.IsNullable(t))
                {
                    Type[] ts = t.GetGenericArguments();
                    t = ts[0];
                }

				......
                else if (t.IsPrimitive)
                {
                    double d = LuaMisc.ToDouble(obj);
                    LuaDLL.lua_pushnumber(L, d);
                }
				......
                else
                {
                    LuaPushVarObject _Push = null;

                    if (VarPushMap.TryGetValue(t, out _Push))
                    {
                        _Push(L, obj);
                    }
                    else
                    {
                        PushStruct(L, obj);
                    }
                }
            }
            else
            {
                if (t.IsArray)
                {
                    Push(L, (Array)obj);
                }
				......
                else
                {
                    PushObject(L, obj);                    
                }
            }
        }

tolua会根据压入栈的不同对象类型,选择不同的压入栈的API。


        Debugger.Log("Read var from lua: {0}", lua["var2read"]);
        Debugger.Log("Read table var from lua: {0}", lua["varTable.default"]);  //LuaState 拆串式table

        LuaFunction func = lua["TestFunc"] as LuaFunction;
        func.Call();
        func.Dispose();

这里同样,用到了有参属性,不过是get方法:
image
可以看到,同样分2部分,上面是取_G.xxx.xx的值,会优先将xxx表压入栈中,再将要取的对象名称压入栈中,调用取值方法,此时栈中就留下的值,将它取出;下面直接取_G.xx。

        public static object ToVarObject(IntPtr L, int stackPos)
        {
            LuaTypes type = LuaDLL.lua_type(L, stackPos);

            switch (type)
            {
                case LuaTypes.LUA_TNUMBER:                    
                    return LuaDLL.lua_tonumber(L, stackPos);
                case LuaTypes.LUA_TSTRING:
                    return LuaDLL.lua_tostring(L, stackPos);
                case LuaTypes.LUA_TUSERDATA:                    
                    switch(LuaDLL.tolua_getvaluetype(L, stackPos))
                    {                                                    
                        case LuaValueType.Int64:
                            return LuaDLL.tolua_toint64(L, stackPos);
                        case LuaValueType.UInt64:
                            return LuaDLL.tolua_touint64(L, stackPos);
                        default:
                            return ToObject(L, stackPos);
                    }
                case LuaTypes.LUA_TBOOLEAN:
                    return LuaDLL.lua_toboolean(L, stackPos);
                case LuaTypes.LUA_TFUNCTION:
                    return ToLuaFunction(L, stackPos);
                case LuaTypes.LUA_TTABLE:
                    return ToVarTable(L, stackPos);
                case LuaTypes.LUA_TNIL:
                    return null;
                case LuaTypes.LUA_TLIGHTUSERDATA:
                    return LuaDLL.lua_touserdata(L, stackPos);
                case LuaTypes.LUA_TTHREAD:
                    return ToLuaThread(L, stackPos);
                default:
                    return null;
            }
        }

ToVarObject则是先判断指定栈上索引的对象类型,调用的方法来取值。


        LuaTable table = lua.GetTable("varTable");

image
与LuaFunction类似,先是判断下有没有缓存起来的索引,有的话直接返回,没有的话,会调用PushLuaTable将lua中对应的表压到栈头,然后再用ToLuaRef生成与该表相关的refreence值,保存到c#层中的LuaTable对象上。然后将LuaTable缓存起来。


        Debugger.Log("Read varTable from lua, default: {0} name: {1}", table["default"], table["map.name"]);
        table["map.name"] = "new";  //table 字符串只能是key
        Debugger.Log("Modify varTable name: {0}", table["map.name"]);

那这里则是调用到了LuaTable的有参属性:
image
可以看到因为缓存了reference的原因,我们可以直接将lua层对应的表压到lua栈头,并读取相关的数据。


最后同样对属性读取做个总结吧:

  1. 如果读取的是全局属性,则直接将对应的名称压入lua栈,然后读取该值。
  2. 如果读取的是某个表的属性,首先要找到lua表,并将它压到lua栈头,并给它生成一个reference。在C#层缓存一个LuaTable对象,LuaTable会存储这个reference值,用作和lua层对应table的关联。
  3. lua层找到对应table,并压入栈后,再将读取的属性名称也压入栈,然后调用函数进行读取,函数执行完毕后,返回的值会留在栈顶,这时我们可以取出。
posted @ 2025-04-19 11:55  陈侠云  阅读(315)  评论(0)    收藏  举报
//雪花飘落效果