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;
}
}
输出:

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);处开始阅读。
- 这里首先做了一个fullPath.LastIndexOf('.');操作,判断一下我们要取的方法是全局变量,还是某个表中的某个方法,我们这里明显是某表中的方法。
- 将test表压入栈中,再压入'luaFunc'字符串,取出该lua对象,并再判断一次该对象类型是不是luaFunc类型。
取出lua函数后,我们回到GetFunction方法来,紧接着会调用int reference = ToLuaRef();这个方法的作用是在Lua注册表中为栈顶元素创建或获取一个唯一引用标识符(整数),然后才会实例化LuaFuntion,每个LuaFunction都会存储着唯一的reference值。接着将该LuaFunction存进funcMap和funcRefMap中去,避免下次调用还要重复生成。

LuaFunction我们知道怎么获取后,我们接下来看看它是怎么调用的。首先看第一个方法:
int num = luaFunc.Invoke<int, int>(123456);
Debugger.Log("generic call return: {0}", num);

为 Lua 函数的调用初始化堆栈环境

将参数压入栈中

执行函数


取出栈顶元素,并调用我们之前注册过的类型检查,判断类型是否正确

结束函数调用。
然后我们再看看另一个函数执行方法:
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);

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



因为我们调用的是test.luaFunc(),没有用到Lua语法糖,所以执行这个方法,最终返回的肯定是System_Func_int_int_Event.Call方法啦。可以看到整个call方法就封装了那些我们之前分析的方法执行流程。

那我们这个Create最终也是返回了这个方法,并将它缓存起来,第二次可以迅速获取。
然后我们再来看看第四个执行方法:
num = lua.Invoke<int, int>("test.luaFunc", 123456, true);
Debugger.Log("luastate call return: {0}", num);

与前三者类似,就不细细分析了。
最后,做个C#执行lua层方法的总结吧。
- 首先找到lua层中对应的函数,并给该函数存储一个全局唯一的reference值,返回到C#层。
- 在C#层实例化一个LuaFunction类,并将refernce值存入该对象,作为C#层和C层的函数关系映射。
- 在C#层执行lua方法时,首先会用到之前缓存起来的reference在c层准备lua堆栈好。--- luaState.BeginPCall
- 如果该lua方法需要参数,C#层压入对应的参数。 --- luaState.PushGeneric;
- 最后执行这个方法 --- luaState.PCall
- 方法如果有返回值会留在lua栈顶,我们可以取出并检查。--- luaState.CheckValue
- 最后结束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();
}
}
输出结果:

LuaState lua = new LuaState();
lua.Start();
lua["Objs2Spawn"] = 5;
启动lua虚拟机,前面已经讨论过了,这里不再重复赘述,关注的是第三句,luaState定义了一个有参属性,可以将luaState视作_G来使用:

代码分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方法:

可以看到,同样分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");

与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的有参属性:

可以看到因为缓存了reference的原因,我们可以直接将lua层对应的表压到lua栈头,并读取相关的数据。
最后同样对属性读取做个总结吧:
- 如果读取的是全局属性,则直接将对应的名称压入lua栈,然后读取该值。
- 如果读取的是某个表的属性,首先要找到lua表,并将它压到lua栈头,并给它生成一个reference。在C#层缓存一个LuaTable对象,LuaTable会存储这个reference值,用作和lua层对应table的关联。
- lua层找到对应table,并压入栈后,再将读取的属性名称也压入栈,然后调用函数进行读取,函数执行完毕后,返回的值会留在栈顶,这时我们可以取出。

浙公网安备 33010602011771号