找回密码
 立即注册
查看: 102|回复: 0

tolua源码分析(二) C#调用lua函数的机制实现

[复制链接]
发表于 2023-3-8 05:54 | 显示全部楼层 |阅读模式
上一节我们主要关注了tolua自身的初始化流程。本节我们来深入理解tolua是如何实现C#调用lua函数的。先看一个具体的例子,来自tolua自带的工程Examples 03,核心代码如下:
public class CallLuaFunction : MonoBehaviour
{
    private string script =
        @"  function luaFunc(num)                        
                return num + 1
            end

            test = {}
            test.luaFunc = luaFunc
        ";

    LuaFunction luaFunc = null;
    LuaState lua = null;
    string tips = null;

    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();
    }

    int CallFunc()
    {        
        luaFunc.BeginPCall();               
        luaFunc.Push(123456);
        luaFunc.PCall();        
        int num = (int)luaFunc.CheckNumber();
        luaFunc.EndPCall();
        return num;               
    }
}
字符串script就是一段简单的lua代码,lua虚拟机启动之后,调用DoString执行这段代码,此时lua虚拟机中就包含了名为test的table,它的luaFunc成员是一个lua函数。显然,第24行就是在C#层获取到这个lua函数,第28-39行展示了C#调用lua函数的多种方式。运行结果如下:



tolua源码分析(二) 运行结果

下面我们来对这段代码进行分析。首先我们来看一下第20行的DelegateFactory.Init函数:
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;
    ...
}
这个函数对C#和Unity中常用的委托类型进行了注册,dict是DelegateFactory类的静态成员,它是一个key为委托类型,value为委托创建函数的字典:
public delegate Delegate DelegateCreate(LuaFunction func, LuaTable self, bool flag);
public static Dictionary<Type, DelegateCreate> dict = new Dictionary<Type, DelegateCreate>();
委托创建函数接收2个来自lua的参数,一个是lua的函数,一个是lua的table,用来表示self,最后bool类型的flag参数是用来区分是否需要self的标志。DelegateTraits与这个dict的用处类似,主要区别在一个是使用type,而另一个是使用泛型来索引到具体的创建函数。TypeTraits和StackTraits我们在上一节已经提过了,一个是用来判断当前lua栈某个位置上的数据是否为某个委托类型,另一个是用来将某个委托类型的object压入到lua栈上。总的来说,通过这一系列的注册,C#层可以将一个lua的函数(不论是否带有self语法糖)转换为C#的委托,也可以检查lua栈上的数据是否为委托类型,还可以将C#的委托压入到lua栈上。
那么为什么要先做这件事情呢?等一下我们就知道了。不如现在来看下最核心的GetFunction函数:
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;
}
函数大致分为两块内容,第5-21行判断,如果当前的函数对象已经在C#层缓存住,就直接将其取出就好。由于我们是第一次在C#层获取test.luaFunc,显然这里是取不到的。直接来到第23行,这里PushLuaFunction将lua函数取出压到当前lua栈上,并为之生成一个reference,这个reference是唯一的,可以与lua函数一一映射。这里会再去检查一遍reference对应的lua函数是否在C#层有缓存,确认没有才会真正新建一个LuaFunction对象,并将其缓存。缓存的数据结构有两种,一个是key为函数名称的funcMap,一个是key为reference的funcRefMap:
Dictionary<string, WeakReference> funcMap = new Dictionary<string, WeakReference>();
Dictionary<int, WeakReference> funcRefMap = new Dictionary<int, WeakReference>();
可以注意到它们的value是对LuaFunction的弱引用,意味着当指向的LuaFunction对象被释放时,这两个map对应的key和value也会自动释放,从而保证缓存的可靠性。
通过分析,可以得知这里最重要的函数就是这个PushLuaFunction了,深入其中一探究竟:
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参数,用来判断是否要在C#的缓存中检查lua函数是否已经存在。显然在这里我们是没有必要判断的,因此直接传入的false。下一步,第25-26行是用来判断lua函数是全局函数,还是某个table下的函数。如果是全局的,那很简单,直接从_G中取出即可;如果不是,则稍微麻烦一点,需要先把这个table压到lua栈上,然后再从table中取出lua函数。最后不要忘记恢复lua栈,只让lua函数保持在栈顶,其他产生的临时数据都需要清理掉。
那么还有一点,怎么让lua知道,C#层获取了哪些lua函数缓存呢?这里就要借助ToLuaRef函数了,函数的具体实现是在tolua_runtime的toluaL_ref中:
LUALIB_API int toluaL_ref(lua_State *L)
{
    int stackPos = abs_index(L, -1);   
    lua_getref(L, LUA_RIDX_FIXEDMAP);
    lua_pushvalue(L, stackPos);
    lua_rawget(L, -2);

    if (!lua_isnil(L, -1))
    {
        int ref = (int)lua_tointeger(L, -1);
        lua_pop(L, 3);
        return ref;
    }
    else
    {
        lua_pushvalue(L, stackPos);
        int ref = luaL_ref(L, LUA_REGISTRYINDEX);
        lua_pushvalue(L, stackPos);
        lua_pushinteger(L, ref);
        lua_rawset(L, -4);
        lua_pop(L, 3);
        return ref;
    }
}该函数大概做了这样一件事情:首先去LUA_RIDX_FIXEDMAP这个table中检查,如果key为我们传入的lua函数的value存在,就直接返回reference;否则,去LUA_REGISTRYINDEX中申请一个reference,lua提供的原生APIluaL_ref可以保证申请到的reference不会重复。得到reference之后,将其缓存到LUA_RIDX_FIXEDMAP中去。这个table我们在上一节的时候也提到过,它就是lua层专门用来缓存C#访问的lua对象用的。
好了,C#层成功获得LuaFunction对象之后,我们来看一下例子里给出的若干种不同的调用方式吧。第一种,LuaFunction类提供了泛型方法Invoke,最后一个泛型参数表示的是方法的返回类型,比如我们这个例子,实际调用到的是这里:
public R1 Invoke<T1, R1>(T1 arg1)
{
    BeginPCall();
    PushGeneric(arg1);
    PCall();
    R1 ret1 = CheckValue<R1>();
    EndPCall();
    return ret1;
}
BeginPCall主要是做一些调用前的准备工作,保存当前的oldTop和stackPos,这两个值分别表示当前函数在lua栈中的起始位置,和函数的返回值在lua栈中的位置。
public virtual int BeginPCall()
{
    if (luaState == null)
    {
        throw new LuaException("LuaFunction has been disposed");
    }

    stack.Push(new FuncData(oldTop, stackPos));
    oldTop = luaState.BeginPCall(reference);
    stackPos = -1;
    argCount = 0;
    return oldTop;
}
PushGeneric就是将函数所需要到参数压入栈中:
public void PushGeneric<T>(T t)
{
    try
    {
        luaState.PushGeneric(t);
        ++argCount;
    }
    catch (Exception e)
    {
        EndPCall();
        throw e;
    }
}
这里会借助到上一节我们提到过的StackTraits,根据不同的类型选择不同的push函数压栈:
public void PushGeneric<T>(T o)
{
    StackTraits<T>.Push(L, o);
}
这里要压入的参数类型为int,int类型在初始化过程中已经注册过了:
void InitStackTraits()
{
    LuaStackOp op = new LuaStackOp();
    ...
    StackTraits<int>.Init(op.Push, op.CheckInt32, op.ToInt32);
    ...
}
要把一个int类型参数压入lua栈很简单,就是调一下lua_pushnumber:
public void Push(IntPtr L, int n)
{
    LuaDLL.lua_pushnumber(L, n);
}
然后是PCall函数,基本上就是对lua原生的pcall函数进行了封装,考虑了异常的处理:
public void PCall()
{
    stackPos = oldTop + 1;

    try
    {
        luaState.PCall(argCount, oldTop);
    }
    catch (Exception e)
    {
        EndPCall();
        throw e;
    }
}
CheckValue就是对函数返回值做类型检查,转换为指定类型返回:
public T CheckValue<T>()
{
    try
    {
        return luaState.CheckValue<T>(stackPos++);
    }
    catch (Exception e)
    {
        EndPCall();
        throw e;
    }
}
同样需要借助StackTraits,这次用到的是int类型的CheckInt32:
public int CheckInt32(IntPtr L, int stackPos)
{
    double ret = LuaDLL.luaL_checknumber(L, stackPos);
    return Convert.ToInt32(ret);
}
lua只有一个number类型,所以要先以double取出,再转换为int。
EndPCall主要是做一些调用后的清理工作,恢复堆栈,以及oldTop和stackPos:
public void EndPCall()
{
    if (oldTop != -1)
    {
        luaState.EndPCall(oldTop);
        argCount = 0;
        FuncData data = stack.Pop();
        oldTop = data.oldTop;
        stackPos = data.stackPos;
    }
}
再看看第二种调用方式,其实就是不借助泛型方法,而是显式地调用它们,本质上是一样的:
int CallFunc()
{        
    luaFunc.BeginPCall();               
    luaFunc.Push(123456);
    luaFunc.PCall();        
    int num = (int)luaFunc.CheckNumber();
    luaFunc.EndPCall();
    return num;               
}
第三种方式,就是将函数转换成了一个Func<int, int>的委托,这里就要用到最开始我们提到的DelegateTraits了:
public T ToDelegate<T>() where T : class
{
    return DelegateTraits<T>.Create(this) as T;
}
在前面DelegateFactory.Init函数中,我们已经注册了Func<int, int>了:
DelegateTraits<System.Func<int,int>>.Init(factory.System_Func_int_int);
因此,这里就会使用System_Func_int_int函数来创建委托。有关委托创建的具体细节,我们后面再说。
那么剩下的最后一种,直接通过当前的LuaState调用lua函数,当然内部实现其实也和第一种差不多,相当于再封装了一层,只需传入函数名称就能调用,连LuaFunction对象都不需要,是很便捷的方法。
public R1 Invoke<T1, R1>(string name, T1 arg1, bool beLogMiss)
{
    int top = LuaDLL.lua_gettop(L);

    try
    {
        if (BeginCall(name, top, beLogMiss))
        {
            PushGeneric(arg1);
            Call(1, top + 1, top);
            R1 ret1 = CheckValue<R1>(top + 2);
            LuaDLL.lua_settop(L, top);
            return ret1;
        }

        return default(R1);
    }
    catch (Exception e)
    {
        LuaDLL.lua_settop(L, top);
        throw e;
    }
}
下一节我们将关注C#访问lua变量机制的实现。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-4 13:02 , Processed in 0.103030 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表