tolua源码分析(二) C#调用lua函数的机制实现 您所在的位置:网站首页 lua源码解析 tolua源码分析(二) C#调用lua函数的机制实现

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

2023-03-09 06:01| 来源: 网络整理| 查看: 265

上一节我们主要关注了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(123456); Debugger.Log("generic call return: {0}", num); num = CallFunc(); Debugger.Log("expansion call return: {0}", num); Func Func = luaFunc.ToDelegate(); num = Func(123456); Debugger.Log("Delegate call return: {0}", num); num = lua.Invoke("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.Init(factory.System_Action); ... TypeTraits.Init(factory.Check_System_Action); ... StackTraits.Push = factory.Push_System_Action; ... }

这个函数对C#和Unity中常用的委托类型进行了注册,dict是DelegateFactory类的静态成员,它是一个key为委托类型,value为委托创建函数的字典:

public delegate Delegate DelegateCreate(LuaFunction func, LuaTable self, bool flag); public static Dictionary dict = new Dictionary();

委托创建函数接收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 funcMap = new Dictionary(); Dictionary funcRefMap = new Dictionary();

可以注意到它们的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 arg1) { BeginPCall(); PushGeneric(arg1); PCall(); R1 ret1 = CheckValue(); 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) { try { luaState.PushGeneric(t); ++argCount; } catch (Exception e) { EndPCall(); throw e; } }

这里会借助到上一节我们提到过的StackTraits,根据不同的类型选择不同的push函数压栈:

public void PushGeneric(T o) { StackTraits.Push(L, o); }

这里要压入的参数类型为int,int类型在初始化过程中已经注册过了:

void InitStackTraits() { LuaStackOp op = new LuaStackOp(); ... StackTraits.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() { try { return luaState.CheckValue(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的委托,这里就要用到最开始我们提到的DelegateTraits了:

public T ToDelegate() where T : class { return DelegateTraits.Create(this) as T; }

在前面DelegateFactory.Init函数中,我们已经注册了Func了:

DelegateTraits.Init(factory.System_Func_int_int);

因此,这里就会使用System_Func_int_int函数来创建委托。有关委托创建的具体细节,我们后面再说。

那么剩下的最后一种,直接通过当前的LuaState调用lua函数,当然内部实现其实也和第一种差不多,相当于再封装了一层,只需传入函数名称就能调用,连LuaFunction对象都不需要,是很便捷的方法。

public R1 Invoke(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(top + 2); LuaDLL.lua_settop(L, top); return ret1; } return default(R1); } catch (Exception e) { LuaDLL.lua_settop(L, top); throw e; } }

下一节我们将关注C#访问lua变量机制的实现。

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有