tolua源码分析(四)lua调用C#函数机制的实现
上一节我们讨论了C#是如何访谒lua变量的,此次我们将研究lua是如何访谒到C#函数的。同样也是来看下官方的例子,example 08:string script =
@”
function TestArray(array)
local len = array.Length
for i = 0, len - 1 do
print('Array: '..tostring(array))
end
local iter = array:GetEnumerator()
while iter:MoveNext() do
print('iter: '..iter.Current)
end
local t = array:ToTable()
for i = 1, #t do
print('table: '.. tostring(t))
end
local pos = array:BinarySearch(3)
print('array BinarySearch: pos: '..pos..' value: '..array)
pos = array:IndexOf(4)
print('array indexof bbb pos is: '..pos)
return 1, '123', true
end
”;
LuaState lua = null;
LuaFunction func = null;
new LuaResLoader();
lua = new LuaState();
lua.Start();
lua.DoString(script, ”AccessingArray.cs”);
int[] array = { 1, 2, 3, 4, 5 };
func = lua.GetFunction(”TestArray”);
func.BeginPCall();
func.Push(array);
func.PCall();
func.EndPCall();这个例子中,lua代码的TestArray函数接收一个array参数,它是来自C#的数组。我们仿佛就在C#层一样,可以直接调用array的方式,例如GetEnumerator;还可以直接使用下标访谒,array就能取出对应下标的元素;还可以直接使用array的get/set属性,例如Length。运行成果如下图:
tolua源码分析(四)例子运行成果
那么,这一切是如何完成的呢?让我们回忆一下tolua的初始化流程,在C#的LuaState类的构造函数中,有一个OpenBaseLibs的调用,它包含了一些最基本C#类的注册,此中就有我们这里要用到的System.Array。这里只截取了与System.Array相关的代码:
void OpenBaseLibs()
{
BeginModule(null);
BeginModule(”System”);
System_ArrayWrap.Register(this);
EndModule();//end System
EndModule(); //end global
ArrayMetatable = metaMap;
}BeginModule和EndModule是一组配对函数,用来将C#的namespace注册给lua。BeginModule会调到C层的tolua_beginmodule函数,一开始我们的参数为null,暗示我们即将向全局的namespace中注册各种东西,也就是筹备往lua层的_G中塞东西,那么对应的tolua_beginmodule实现也非常简单,就是将lua层的_G筹备好,此时的lua栈如图所示:
LUALIB_API bool tolua_beginmodule(lua_State *L, const char *name)
{
if (name != NULL)
{
...
}
else
{
lua_pushvalue(L, LUA_GLOBALSINDEX);
return true;
}
}
tolua源码分析(四)调用BeginModule(null)时的lua栈
接下来,调用的是BeginModule(”System”),此时对应的tolua_beginmodule实现会稍微复杂一些,我们将一步步地模拟栈操作,对当前的lua栈进行可视化:
LUALIB_API bool tolua_beginmodule(lua_State *L, const char *name)
{
if (name != NULL)
{
lua_pushstring(L, name); //stack key
lua_rawget(L, -2); //stack value
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
lua_newtable(L); //stack table
lua_pushstring(L, ”__index”);
lua_pushcfunction(L, module_index_event);
lua_rawset(L, -3);
lua_pushstring(L, name); //stack table name
lua_pushstring(L, ”.name”); //stack table name ”.name”
pushmodule(L, name); //stack table name ”.name” module
lua_rawset(L, -4); //stack table name
lua_pushvalue(L, -2); //stack table name table
lua_rawset(L, -4); //stack table
lua_pushvalue(L, -1);
lua_setmetatable(L, -2);
return true;
}
else if (lua_istable(L, -1))
{
...
}
}
else
{
...
}
}第5-6行判断_G中是否已经存在名为System的table,如果有就直接取出,不用再新建了,当然此时我们是没有的,所以会走到第8行,新建一个table出来,此时的lua栈如下图所示:
tolua源码分析(四)调用BeginModule("System")时的lua栈
第13-15行设置了这个table的__index域,显然这个table会被拿来用作metatable使用,这里的module_index_event函数我们等用到的时候再展开说。接下来17-18行持续两个pushstring,显然是为了记录这个table的name,此时的lua栈如图所示:
tolua源码分析(四)当前lua栈
第19行来了个pushmodule函数,这个函数使用了一个buffer来缓存当前注册过程中已经注册过的namespace,这样就能够通过拼接得到当前namespace的完整名称,这里namespace System就已经是完整名称了,因此push进lua栈的还是System,此时的lua栈如图所示:
tolua源码分析(四)当前lua栈
第20行设置了这个table的.name域为System,它暗示对应C# namespace的完整名称。紧接着第21行又把table复制了一份,压到了栈顶:
tolua源码分析(四)当前lua栈
第22行就是把namespace信息保留到_G了,简单来说就是_G[”System”] = new table,最后第24-25行,就是把该table的metatable设置为它自身,函数执行结束时,会在栈上留下一份该table,这样做的目的是让该namespace下的class及namespace都能够关联到table上。
tolua源码分析(四)tolua_beginmodule结束时的lua栈
总的来说,tolua_beginmodule做的事情,就是创建了一个table,设置table的__index,.name还有metatable,此中.name是table对应C# namespace的全称,metatable为它自身。
接下来就是重头戏System_ArrayWrap.Register,System_ArrayWrap是tolua自动生成用来将C#的System.Array注册到lua层的类,Register是它的静态方式,里面包含了需要注册到lua层的方式,属性,以及下标访谒等操作:
public static void Register(LuaState L)
{
L.BeginClass(typeof(Array), typeof(System.Object));
L.RegFunction(”.geti”, get_Item);
L.RegFunction(”.seti”, set_Item);
L.RegFunction(”ToTable”, ToTable);
L.RegFunction(”GetLength”, GetLength);
L.RegFunction(”GetLongLength”, GetLongLength);
L.RegFunction(”GetLowerBound”, GetLowerBound);
L.RegFunction(”GetValue”, GetValue);
L.RegFunction(”SetValue”, SetValue);
L.RegFunction(”GetEnumerator”, GetEnumerator);
L.RegFunction(”GetUpperBound”, GetUpperBound);
L.RegFunction(”CreateInstance”, CreateInstance);
L.RegFunction(”BinarySearch”, BinarySearch);
L.RegFunction(”Clear”, Clear);
L.RegFunction(”Clone”, Clone);
L.RegFunction(”Copy”, Copy);
L.RegFunction(”IndexOf”, IndexOf);
L.RegFunction(”Initialize”, Initialize);
L.RegFunction(”LastIndexOf”, LastIndexOf);
L.RegFunction(”Reverse”, Reverse);
L.RegFunction(”Sort”, Sort);
L.RegFunction(”CopyTo”, CopyTo);
L.RegFunction(”ConstrainedCopy”, ConstrainedCopy);
L.RegFunction(”__tostring”, ToLua.op_ToString);
L.RegVar(”Length”, get_Length, null);
L.RegVar(”LongLength”, get_LongLength, null);
L.RegVar(”Rank”, get_Rank, null);
L.RegVar(”IsSynchronized”, get_IsSynchronized, null);
L.RegVar(”SyncRoot”, get_SyncRoot, null);
L.RegVar(”IsFixedSize”, get_IsFixedSize, null);
L.RegVar(”IsReadOnly”, get_IsReadOnly, null);
L.EndClass();
}与namespace类似,class的注册也需要一组配对方式BeginClass和EndClass,此中属性相关的注册是使用RegVar方式,方式和索引操作的注册是使用RegFunction方式。那么首先从BeginClass看起,它接受两个type参数,分袂暗示当前注册的类型与其基类,这里System.Object是Array的基类:
public int BeginClass(Type t, Type baseType, string name = null)
{
if (beginCount == 0)
{
throw new LuaException(”must call BeginModule first”);
}
int baseMetaRef = 0;
int reference = 0;
if (name == null)
{
name = GetToLuaTypeName(t);
}
if (baseType != null && !metaMap.TryGetValue(baseType, out baseMetaRef))
{
LuaCreateTable();
baseMetaRef = LuaRef(LuaIndexes.LUA_REGISTRYINDEX);
BindTypeRef(baseMetaRef, baseType);
}
if (metaMap.TryGetValue(t, out reference))
{
LuaDLL.tolua_beginclass(L, name, baseMetaRef, reference);
RegFunction(”__gc”, Collect);
}
else
{
reference = LuaDLL.tolua_beginclass(L, name, baseMetaRef);
RegFunction(”__gc”, Collect);
BindTypeRef(reference, t);
}
return reference;
}函数首先会进行一次查抄,即第3-6行,BeginModule必需要在BeginClass之前调用,否则生成的class table没法子绑定到namespace table上;第11-14行,如果函数传参时没有指定注册的class的name,那这里会使用GetToLuaTypeName进行生成,这里生成的name就是Array;第16-21行,如果函数传参时指定了基类baseType,那么需要查抄baseType是否已经注册过了。这里的metaMap是一个key为Type,value为int的dictionary,此中key暗示当前已经注册过的class类型,value暗示该类型在lua层的reference。如果baseType已经注册过,那直接将其reference取出即可;如果尚未注册,就先创建一个空的table,用这个空table向lua层获取reference,得到的reference就作为baseType在lua层的reference,记录到metaMap中;这里在注册Array时,已经在之前注册过System.Object了,因此直接就能拿到System.Object的reference了;第23-33行,就是判断当前注册的类是否已经有reference了,由我们方才讨论可知,如果先注册子类,再注册基类,那么在注册基类时reference就会存在,不管怎样,最后函数城市走到tolua_beginclass上,无非就是传不传reference的区别。
tolua_beginclass在C层实现,相对也斗劲复杂:
LUALIB_API int tolua_beginclass(lua_State *L, const char *name, int baseType, int ref)
{
int reference = ref;
lua_pushstring(L, name);
lua_newtable(L);
_addtoloaded(L);
if (ref == LUA_REFNIL)
{
lua_newtable(L);
lua_pushvalue(L, -1);
reference = luaL_ref(L, LUA_REGISTRYINDEX);
}
else
{
lua_getref(L, reference);
}
if (baseType != 0)
{
lua_getref(L, baseType);
lua_setmetatable(L, -2);
}
lua_pushlightuserdata(L, &tag);
lua_pushnumber(L, 1);
lua_rawset(L, -3);
lua_pushstring(L, ”.name”);
_pushfullname(L, -4);
lua_rawset(L, -3);
lua_pushstring(L, ”.ref”);
lua_pushinteger(L, reference);
lua_rawset(L, -3);
lua_pushstring(L, ”__call”);
lua_pushcfunction(L, class_new_event);
lua_rawset(L, -3);
tolua_setindex(L);
tolua_setnewindex(L);
return reference;
}第3-5行把class的name与新建的class table压入了lua栈:
tolua源码分析(四)调用tolua_beginclass时的lua栈
第6行的_addtoloaded函数,顾名思义,就是将class table保留到package.loaded里,用lua代码描述就是:package.loaded[”System.Array”] = table。第8-17行,按照当前类型的reference是否存在,如果存在直接调用lua_getref取出对应的table,否则就再新建一个table,作为该类型的reference table,同时得到reference,此时lua栈如图所示:
tolua源码分析(四)当前lua栈
第19-23行判断当前类型是否有基类,如果有基类的话,要把基类的reference table取出,作为当前类型reference table的metatable,这样做的目的是为了在lua层实现对C#基类的访谒,比如lua层的System.Array对象可以直接访谒Sytem.Object类中注册过的方式和属性。
第25-27行为reference table打上了特殊的tag,table中插入了一个特殊的lightuserdata类型的key,拥有这个key的table即为reference table;第29-31行在table中设置class的全称,这里就是System.Array;第33-35行在table中记录了reference的值;第37-39行设置了__call对应的方式,有了这个我们在lua层便利地使用System.Array()来创建一个Array对象了。从这里也能猜出,这个reference table是作为metatable使用的。函数的最后,tolua_setindex和tolua_setnewindex设置了__index和__newindex,分袂用来从C#读取类对象的信息到lua层,以及从lua层写入类对象的信息到C#层。
到这,BeginClass差不多结束了,总的来说就是往栈上新增了class的名称,以及两个table,此中一个table中设置了class的全称,reference,以及用作metatable的__call,__index,__newindex方式。那此刻回过头来看看方式和索引操作的注册RegFunction的实现:
public void RegFunction(string name, LuaCSFunction func)
{
IntPtr fn = Marshal.GetFunctionPointerForDelegate(func);
LuaDLL.tolua_function(L, name, fn);
}函数实现异常简单,主要逻辑在C层的tolua_function中:
LUALIB_API void tolua_function(lua_State *L, const char *name, lua_CFunction fn)
{
lua_pushstring(L, name);
tolua_pushcfunction(L, fn);
lua_rawset(L, -3);
}也很简单,就是往栈顶里的table塞数据,而由前面的图可知,此时lua栈顶的table为该类型的reference table,用lua代码描述Register中的RegFunction操作,也就是:
ref[”.geti”] = get_Item
ref[”.seti”] = set_Item
ref[”ToTable”] = ToTable
...下面再看下注册属性的RegVar实现:
public void RegVar(string name, LuaCSFunction get, LuaCSFunction set)
{
IntPtr fget = IntPtr.Zero;
IntPtr fset = IntPtr.Zero;
if (get != null)
{
fget = Marshal.GetFunctionPointerForDelegate(get);
}
if (set != null)
{
fset = Marshal.GetFunctionPointerForDelegate(set);
}
LuaDLL.tolua_variable(L, name, fget, fset);
}可以看到get和set是分隔的,与C#层保持一致,只有C#层的get/set属性存在,才会注册到lua层。来看下C层的tolua_variable:
LUALIB_API void tolua_variable(lua_State *L, const char *name, lua_CFunction get, lua_CFunction set)
{
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2);
if (!lua_istable(L, -1))
{
/* create .get table, leaving it at the top */
lua_pop(L, 1);
lua_newtable(L);
lua_pushlightuserdata(L, &gettag);
lua_pushvalue(L, -2);
lua_rawset(L, -4);
}
lua_pushstring(L, name);
tolua_pushcfunction(L, get);
lua_rawset(L, -3); /* store variable */
lua_pop(L, 1); /* pop .get table */
/* set func */
if (set != NULL)
{
lua_pushlightuserdata(L, &settag);
lua_rawget(L, -2);
if (!lua_istable(L, -1))
{
/* create .set table, leaving it at the top */
lua_pop(L, 1);
lua_newtable(L);
lua_pushlightuserdata(L, &settag);
lua_pushvalue(L, -2);
lua_rawset(L, -4);
}
lua_pushstring(L, name);
tolua_pushcfunction(L, set);
lua_rawset(L, -3); /* store variable */
lua_pop(L, 1); /* pop .set table */
}
}类似地,为了保证table的key的独一性,以及这个key不成能被lua层访谒到,这里再次使用lightuserdata作为reference table的key,第4-14行测验考试从reference table中取出用来存储get属性的get table,如果存在就压入栈顶,如果不存在,则新建一个,以lightuserdata为key插入到reference table中;第16-18行,把get属性对应的方式绑定到get table中,即get = get_function。完成这一步后,第19行将get table从栈顶弹出,get属性的注册算是结束了。set属性的操作与之类似,这里就不再反复描述了。
自此,reference table中包含了注册的C#函数信息,以及get和set这两个table,分袂包含了注册的C# get/set属性信息。最后就剩下tolua_endclass了:
LUALIB_API void tolua_endclass(lua_State *L)
{
lua_setmetatable(L, -2);
lua_rawset(L, -3);
}函数做了两件事,一是把reference table设置为class table的metatable:
tolua源码分析(四)当前lua栈
二是把class table塞到namespace中,这里就是_G.System[”Array”] = class。自此class的注册过程彻底结束,lua栈也恢复到了注册前的模样:
tolua源码分析(四)tolua_beginclass结束时的lua栈
我们之前提到过,在注册namespace时,我们把注册的table的metatable设置为它自身,为什么在注册class的时候,不这样做,而是此外用了一个reference table来作为class table的metatable呢?这是因为,我们在lua层除了直接访谒class之外,更多可能是通过C# object访谒到class,也就是说,存在两种分歧的方式访谒到注册的C#方式。因此,为了区分这两种情况,需要把保留注册C#方式的table给单独抽出来,class直接访谒就通过class table的形式,例如System.Array(),object访谒通过userdata的形式, 例如array.Length。
我们花了这么长的篇幅做了铺垫,此刻回到例子中的lua代码,它有个array的参数,这个参数是从C#层push进去的,我们先看看C#的array是怎么变成lua的userdata的:
public void Push(Array array)
{
if (array == null)
{
LuaPushNil();
}
else
{
PushUserData(array, ArrayMetatable);
}
}这里的ArrayMetatable就是lua层reference table的reference,在前面BeginClass时已经缓存在C#了。
void PushUserData(object o, int reference)
{
int index;
if (translator.Getudata(o, out index))
{
if (LuaDLL.tolua_pushudata(L, index))
{
return;
}
translator.Destroyudata(index);
}
index = translator.AddObject(o);
LuaDLL.tolua_pushnewudata(L, reference, index);
}第5-13行,首先查找C#缓存objectsBackMap,这个缓存记录了push到lua层的C# object与lua userdata存放index的映射关系。如果查找到了,会拿查到的index到lua层进行查验:
LUALIB_API bool tolua_pushudata(lua_State *L, int index)
{
lua_getref(L, LUA_RIDX_UBOX); // stack: ubox
lua_rawgeti(L, -1, index); // stack: ubox, obj
if (!lua_isnil(L, -1))
{
lua_remove(L, -2); // stack: obj
return true;
}
lua_pop(L, 2);
return false;
}LUA_RIDX_UBOX保留了C#层push进来的userdata,我们按照传进来的index查找对应的userdata,如果查找到了,就将其压入栈顶,返回true;如果查找掉败则返回false。
回到C#层,如果lua层的查验成功,那么什么都不用做,因为userdata已经在lua栈顶了,直接返回即可;如果掉败了,说明这个userdata在lua层并不存在,C#缓存已经掉效,需要将其断根。第15行就是在C#层为object生成一个新的index,使用这个新的index在lua层生成一个新的userdata:
LUALIB_API void tolua_pushnewudata(lua_State *L, int metaRef, int index)
{
lua_getref(L, LUA_RIDX_UBOX);
tolua_newudata(L, index);
lua_getref(L, metaRef);
lua_setmetatable(L, -2);
lua_pushvalue(L, -1);
lua_rawseti(L, -3, index);
lua_remove(L, -2);
}lua层先是创建了一个userdata,并设置它的值为index,然后设置userdata的metatable为我们之前注册class生成的reference table,最后把这个userdata塞到LUA_RIDX_UBOX缓存,对应的key就是index。同样地,lua栈顶保留了一份userdata。
那么,此刻lua代码中的array已经是一个userdata了,往下看首先会调用到array.Length,这会触发userdta的metatable,也就是reference table,它的__index元方式,这个是在之前tolua_beginclass的tolua_setindex函数中设置的:
LUALIB_API void tolua_setindex(lua_State *L)
{
lua_pushstring(L, ”__index”);
lua_pushcfunction(L, class_index_event);
lua_rawset(L, -3);
}那么此时就会触发调用这个class_index_event函数了,这个函数按照调用方是userdata还是table,以及要访谒的key是哪种类型,分袂做了分歧措置,这里我们只看相关的部门:
static int class_index_event(lua_State *L)
{
int t = lua_type(L, 1);
if (t == LUA_TUSERDATA)
{
lua_getfenv(L,1);
if (!lua_rawequal(L, -1, TOLUA_NOPEER)) // stack: t k env
{
...
}
lua_settop(L,2);
lua_pushvalue(L, 1); // stack: obj key obj
while (lua_getmetatable(L, -1) != 0)
{
lua_remove(L, -2); // stack: obj key mt
if (lua_isnumber(L,2)) // check if key is a numeric value
{
...
}
else
{
lua_pushvalue(L, 2); // stack: obj key mt key
lua_rawget(L, -2); // stack: obj key mt value
if (!lua_isnil(L, -1))
{
return 1;
}
lua_pop(L, 1);
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2); //stack: obj key mt tget
if (lua_istable(L, -1))
{
lua_pushvalue(L, 2); //stack: obj key mt tget key
lua_rawget(L, -2); //stack: obj key mt tget value
if (lua_isfunction(L, -1))
{
lua_pushvalue(L, 1);
lua_call(L, 1, 1);
return 1;
}
}
}
lua_settop(L, 3);
}
lua_settop(L, 2);
int *udata = (int*)lua_touserdata(L, 1);
if (*udata == LUA_NULL_USERDATA)
{
return luaL_error(L, ”attemp to index %s on a nil value”, lua_tostring(L, 2));
}
if (toluaflags & FLAG_INDEX_ERROR)
{
return luaL_error(L, ”field or property %s does not exist”, lua_tostring(L, 2));
}
}
else if(t == LUA_TTABLE)
{
...
}
lua_pushnil(L);
return 1;
}第7-12行获取了绑定在userdata上的env table,它用来实现lua层担任扩展C#对象的机制,这个我们后面再说;第17行开始是一个循环,不竭地获取metatable,其含义就是如果当前类的reference table没有找到对应的元素,还会往基类的reference table中查找。第21行判断索引的key是否为number,我们这里的key是Length,是一个字符串,因此实际的正片要从第27行开始。首先查找当前reference table是否包含索引的key,如果包含直接取出,这里对应的就是先前注册过的各种方式;而Length是一个get属性,因此还要继续在reference table的get table中查找,找到对应的返回get属性的函数,直接调用得到成果。如果循环结束时还未查找成功,则说明要么userdata不合法,要么索引的key压根没注册,需要按照分歧的情况进行报错。
既然找到了Length对应的get函数,我们回到C#层看看它是怎么实现的:
static int GetLength(IntPtr L)
{
try
{
ToLua.CheckArgsCount(L, 2);
System.Array obj = (System.Array)ToLua.CheckObject<Array>(L, 1);
int arg0 = (int)LuaDLL.luaL_checknumber(L, 2);
int o = obj.GetLength(arg0);
LuaDLL.lua_pushinteger(L, o);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}函数的重点在第6-7行,就是要把lua栈上的数据转换成正确的C#类型,如何把userdata转换成本来的C# object呢?这点到此刻其实已经很明了了,lua层的userdata记录了它对应C#层缓存的index,我们只要通过这个index,反查C#缓存,就能取出缓存的object。
与array.Length类似,下标访谒array时,索引的key变成了number:
if (lua_isnumber(L,2)) // check if key is a numeric value
{
lua_pushstring(L,”.geti”);
lua_rawget(L,-2); // stack: obj key mt func
if (lua_isfunction(L,-1))
{
lua_pushvalue(L,1);
lua_pushvalue(L,2);
lua_call(L,2,1);
return 1;
}
}可以看到这里有个trick,在当时注册下标操作时,注册的其实是一个.geti对应的函数,这个函数负责接受object和index参数,返回object,将其压入lua栈中。
static int get_Item(IntPtr L)
{
try
{
Array obj = ToLua.ToObject(L, 1) as Array;
if (obj == null)
{
throw new LuaException(”trying to index an invalid object reference”);
}
int index = (int)LuaDLL.lua_tointeger(L, 2);
if (index >= obj.Length)
{
throw new LuaException(”array index out of bounds: ” + index + ” ” + obj.Length);
}
Type t = obj.GetType().GetElementType();
if (t.IsValueType)
{
...
}
object val = obj.GetValue(index);
ToLua.Push(L, val);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}总结一下,要想在lua层访谒某个C#类的方式或属性,需要提前对这个类进行注册,注册就是在lua层生成一个class table和reference table,class table用于lua层直接通过类而不是对象来访谒C#方式或属性,reference table中包含了各种key对应的C#函数,以及get/set table,分袂包含对应的C#属性,C#的下标访谒操作则是通过特殊的.geti/.seti函数封装的。reference table是class table的metatable,如果该类有基类,还会把基类的reference table设置为当前reference table的metatable,用于实现基类查找。每一个push到lua层的C#对象,在lua层是以userdata的形式存在,userdata的值是其C#对象在C#缓存中的index,每个userdata的metatable都是各自对应C#类型的reference table。
下一节我们将存眷一些特殊C#语法在lua层的实现,例如枚举Enum。
如果你感觉我的文章有辅佐,欢迎存眷我的微信公众号我是真的想做游戏啊
页:
[1]