BlaXuan 发表于 2023-3-8 05:54

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

上一节我们主要关注了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变量机制的实现。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊
页: [1]
查看完整版本: tolua源码分析(二) C#调用lua函数的机制实现