|
异次元的归来:tolua源码分析(二) C#调用lua函数的机制实现上一节我们讨论了C#是如何获取并调用到lua定义的函数,这一节我们更进一步,来看看如何让C#可以访问lua定义的变量的。依旧从一个例子看起,这次是tolua自带的example04,主要代码如下:
new LuaResLoader();
LuaState lua = new LuaState();
lua.Start();
lua["Objs2Spawn"] = 5;
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
";
lua.DoString(script);
Debugger.Log("Read var from lua: {0}", lua["var2read"]);
Debugger.Log("Read table var from lua: {0}", lua["varTable.default"]);
LuaFunction func = lua["TestFunc"] as LuaFunction;
func.Call();
func.Dispose();
LuaTable table = lua.GetTable("varTable");
Debugger.Log("Read varTable from lua, default: {0} name: {1}", table["default"], table["map.name"]);
table["map.name"] = "new";
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(&#34;varTable[{0}], is {1}&#34;, i, list);
}
table.Dispose();
lua.CheckTop();
lua.Dispose();
第1-3行对lua虚拟机进行了初始化,这块内容我们在第一节里已经详细展开过,这里就不再赘述。通过第4行,可以发现C#的LuaState类重载了下标访问的操作符,使其扮演了lua中_G的角色,可以直接往lua层写入一个名为Objs2Spawn的变量,它的值为5。这一点在接下来执行的lua代码可以得到验证:
tolua源码分析(三) C#写入lua变量
下面就来看看下标访问操作符的具体实现,这里用到的是set,那么就先忽略掉get相关的实现:
public object this[string fullPath]
{
get
{
...
}
set
{
int oldTop = LuaGetTop();
int pos = fullPath.LastIndexOf(&#39;.&#39;);
if (pos > 0)
{
string tableName = fullPath.Substring(0, pos);
IntPtr p = LuaFindTable(LuaIndexes.LUA_GLOBALSINDEX, tableName);
if (p == IntPtr.Zero)
{
string name = fullPath.Substring(pos + 1);
LuaPushString(name);
PushVariant(value);
LuaSetTable(-3);
}
else
{
LuaSetTop(oldTop);
int len = LuaDLL.tolua_strlen(p);
string str = LuaDLL.lua_ptrtostring(p, len);
throw new LuaException(string.Format(&#34;{0} not a Lua table&#34;, str));
}
}
else
{
PushVariant(value);
LuaSetGlobal(fullPath);
}
LuaSetTop(oldTop);
}
}
第10-11行就是判断是直接往lua的_G里赋值,还是往_G中的某个table里赋值。如果是往某个table中赋值,那么首先需要在lua层找到这个table,也就是这里的LuaFindTable,它的实现就是简单地调用lua的API函数luaL_findtable,值得一提的是如果查找的table不存在,则该函数会去创建对应name的table,这样大大简化了在C#层创建table的操作。如果函数执行成功,返回的是NULL。对应到第20-23行,此时lua栈顶是获取或创建的table,我们只需将key和value都压入栈,调用lua_settable设置即可。当然,如果是直接往_G里赋值,那直接调用lua_setglobal就行了。
这里我们稍微看一下PushVariant,它会调用到ToLua.Push上,这个方法就是根据C# object的类型,调用不同的push将object压入lua栈中。由于我们的参数是5,直接当作lua的number类型处理:
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];
}
if (t.IsPrimitive)
{
double d = LuaMisc.ToDouble(obj);
LuaDLL.lua_pushnumber(L, d);
}
...
}
else
{
...
}
}
看完了set,我们回到例子中,第25-30行就用到了下标访问操作符的get了,相关输出如下:
tolua源码分析(三) C#访问lua变量
get的实现与set非常相似:
public object this[string fullPath]
{
get
{
int oldTop = LuaGetTop();
int pos = fullPath.LastIndexOf(&#39;.&#39;);
object obj = null;
if (pos > 0)
{
string tableName = fullPath.Substring(0, pos);
if (PushLuaTable(tableName))
{
string name = fullPath.Substring(pos + 1);
LuaPushString(name);
LuaRawGet(-2);
obj = ToVariant(-1);
}
else
{
LuaSetTop(oldTop);
return null;
}
}
else
{
LuaGetGlobal(fullPath);
obj = ToVariant(-1);
}
LuaSetTop(oldTop);
return obj;
}
set
{
...
}
}
一个区别就是这里使用的是PushLuaTable,该函数的作用就是将C#指定的table压入到lua栈中,但是如果该table不存在,并不会新建,而是会返回false。
bool PushLuaTable(string fullPath, bool checkMap = true)
{
if (checkMap)
{
WeakReference weak = null;
if (funcMap.TryGetValue(fullPath, out weak))
{
if (weak.IsAlive)
{
LuaTable table = weak.Target as LuaTable;
CheckNull(table, &#34;{0} not a lua table&#34;, fullPath);
Push(table);
return true;
}
else
{
funcMap.Remove(fullPath);
}
}
}
if (!LuaDLL.tolua_pushluatable(L, fullPath))
{
return false;
}
return true;
}
这个函数和上一节里说的PushLuaFunction类似,会先去查找C#缓存,如果缓存中存在,就直接通过缓存中记录的reference,调用lua_getref拿到对应的table,从而避免了每次要去解析字符串,递归查找table的过程。拿到table之后,再将key压入栈,就能得到value了。
同样地,这里有一个ToVariant,会根据lua栈上object的类型,调用不同的转换函数得到C#的object。例子中value的类型为number和function,对于number来说,直接调用lua_tonumber即可,而对于function来说,则稍微复杂一些:
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_TFUNCTION:
return ToLuaFunction(L, stackPos);
...
default:
return null;
}
}
public static LuaFunction ToLuaFunction(IntPtr L, int stackPos)
{
LuaTypes type = LuaDLL.lua_type(L, stackPos);
if (type == LuaTypes.LUA_TNIL)
{
return null;
}
stackPos = LuaDLL.abs_index(L, stackPos);
LuaDLL.lua_pushvalue(L, stackPos);
int reference = LuaDLL.toluaL_ref(L);
return LuaStatic.GetFunction(L, reference);
}
通过这种方式,从lua栈上获取lua函数时,需要在lua层和C#层都进行缓存,因为这个函数相当于同时被lua层和C#层引用了。上一节中我们提到,lua层缓存的方式走的是toluaL_ref,得到reference后返回给C#层。然后,C#层会尝试先从缓存中查找reference,如果找到会增加其引用计数,找不到就将reference绑定到一个新的LuaFunction对象上:
LuaBaseRef TryGetLuaRef(int reference)
{
WeakReference weak = null;
if (funcRefMap.TryGetValue(reference, out weak))
{
if (weak.IsAlive)
{
LuaBaseRef luaRef = (LuaBaseRef)weak.Target;
if (luaRef.IsAlive)
{
luaRef.AddRef();
return luaRef;
}
}
funcRefMap.Remove(reference);
}
return null;
}
public LuaFunction GetFunction(int reference)
{
LuaFunction func = TryGetLuaRef(reference) as LuaFunction;
if (func == null)
{
func = new LuaFunction(reference, this);
funcRefMap.Add(reference, new WeakReference(func));
if (LogGC) Debugger.Log(&#34;Alloc LuaFunction name , id {0}&#34;, reference);
}
RemoveFromGCList(reference);
return func;
}
下面一种方式,lua.GetTable是将lua层的table取出,C#层进行缓存的方式。注意它跟前面调用的PushLuaTable的区别。PushLuaTable这个函数不会增加缓存,也不会增加引用,这是因为它对应的table只是个临时产生的变量;而这里的GetTable并不同,因此获取的LuaTable会在lua层和C#层都进行一遍缓存,缓存的机制和前面类似,就不展开了。
public LuaTable GetTable(string fullPath, bool beLogMiss = true)
{
WeakReference weak = null;
if (funcMap.TryGetValue(fullPath, out weak))
{
if (weak.IsAlive)
{
LuaTable table = weak.Target as LuaTable;
CheckNull(table, &#34;{0} not a lua table&#34;, fullPath);
if (table.IsAlive)
{
table.AddRef();
RemoveFromGCList(table.GetReference());
return table;
}
}
funcMap.Remove(fullPath);
}
if (PushLuaTable(fullPath, false))
{
int reference = ToLuaRef();
LuaTable table = null;
if (funcRefMap.TryGetValue(reference, out weak))
{
if (weak.IsAlive)
{
table = weak.Target as LuaTable;
CheckNull(table, &#34;{0} not a lua table&#34;, fullPath);
if (table.IsAlive)
{
funcMap.Add(fullPath, weak);
table.AddRef();
RemoveFromGCList(reference);
return table;
}
}
funcRefMap.Remove(reference);
}
table = new LuaTable(reference, this);
table.name = fullPath;
funcMap.Add(fullPath, new WeakReference(table));
funcRefMap.Add(reference, new WeakReference(table));
if (LogGC) Debugger.Log(&#34;Alloc LuaTable name {0}, id {1}&#34;, fullPath, reference);
RemoveFromGCList(reference);
return table;
}
if (beLogMiss)
{
Debugger.LogWarning(&#34;Lua table {0} not exists&#34;, fullPath);
}
return null;
}
C#层的LuaTable,具有和lua层的table类似的操作,第33-35行展示了利用下标访问,对lua层的table读取和写入:
tolua源码分析(三) C#的LuaTable
LuaTable类的下标访问操作符key的类型只支持string和int,不过对于绝大部分情况已经完全够用。string类型和int类型的实现大同小异,这里以例子中用到的string为例,来看看具体的实现:
public object this[string key]
{
get
{
int top = luaState.LuaGetTop();
try
{
luaState.Push(this);
luaState.Push(key);
luaState.LuaGetTable(top + 1);
object ret = luaState.ToVariant(top + 2);
luaState.LuaSetTop(top);
return ret;
}
catch (Exception e)
{
luaState.LuaSetTop(top);
throw e;
}
}
set
{
int top = luaState.LuaGetTop();
try
{
luaState.Push(this);
luaState.Push(key);
luaState.PushVariant(value);
luaState.LuaSetTable(top + 1);
luaState.LuaSetTop(top);
}
catch (Exception e)
{
luaState.LuaSetTop(top);
throw e;
}
}
}
这个实现就是在C#层模拟了一遍对lua栈的操作,值得一提的是对LuaTable进行push,实际上等价于将lua层对应的table,push到lua栈顶,而不是真的push了一个C#对象到lua栈顶。因此,这里需要把LuaTable记录的reference取出,然后调用lua_getref到lua层取出真正的table:
public void Push(LuaBaseRef lbr)
{
if (lbr == null)
{
LuaPushNil();
}
else
{
LuaGetRef(lbr.GetReference());
}
}
接着看例子的第37-41行,它展示了如何在C#层新建一个table。
tolua源码分析(三) C#层新建table
重点就在这个AddTable上,这个函数做的事情,其实也就是在C#层,模拟在lua栈上创建table的操作:
public void AddTable(string name)
{
int oldTop = luaState.LuaGetTop();
try
{
luaState.Push(this);
luaState.Push(name);
luaState.LuaCreateTable();
luaState.LuaRawSet(oldTop + 1);
luaState.LuaSetTop(oldTop);
}
catch (Exception e)
{
luaState.LuaSetTop(oldTop);
throw e;
}
}
由于我们还需要把lua层新建的table转换成C#对应的LuaTable返回,因此这里还会用到之前提到的ToVarObject函数,在lua层生成reference,然后返回给C#层,查找对应的C#缓存,如果不存在就新建一个LuaTable对象。
public static object ToVarObject(IntPtr L, int stackPos)
{
LuaTypes type = LuaDLL.lua_type(L, stackPos);
switch (type)
{
case LuaTypes.LUA_TTABLE:
return ToVarTable(L, stackPos);
...
default:
return null;
}
}
接着看例子,下一个是在C#层去访问lua层设置的metatable。实现方式与前面所说的类似,还是C#层直接调用lua的API,模拟栈操作。
tolua源码分析(三) C#层访问metatable
例子的最后,演示了LuaTable.ToArray方法。通过这个方法,可以方便地将lua层的table转换为C#的数组。
tolua源码分析(三) LuaTable.ToArray
类似地,该方法的实现,就是在C#层,调用lua的lua_objlen函数,得到当前table的长度,然后在C#层对该table进行遍历,把结果塞到C#的数组中:
public object[] ToArray()
{
int oldTop = luaState.LuaGetTop();
try
{
luaState.Push(this);
int len = luaState.LuaObjLen(-1);
List<object> list = new List<object>(len + 1);
int index = 1;
object obj = null;
while(index <= len)
{
luaState.LuaRawGetI(-1, index++);
obj = luaState.ToVariant(-1);
luaState.LuaPop(1);
list.Add(obj);
}
luaState.LuaSetTop(oldTop);
return list.ToArray();
}
catch (Exception e)
{
luaState.LuaSetTop(oldTop);
throw e;
}
}
下一节我们将揭开lua层如何调用C#方法的面纱,先从C#的数组开始。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号我是真的想做游戏啊
Reference
[1] Learning Lua: 5 - Document for luaL_findtable() function |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|