上一节我们阐述了lua调用带out参数的C#函数机制,本节我们来看下lua层是如何扩展C#类的。这次的例子在example 17,主要都是lua代码:
LuaTransform =
{
}
function LuaTransform.Extend(u)
local t = {}
local _position = u.position
tolua.setpeer(u, t)
t.__index = t
local get = tolua.initget(t)
local set = tolua.initset(t)
local _base = u.base
--重写同名属性获取
get.position = function(self)
return _position
end
--重写同名属性设置
set.position = function(self, v)
if _position ~= v then
_position = v
_base.position = v
end
end
--重写同名函数
function t:Translate(...)
print('child Translate')
_base:Translate(...)
end
return u
end
--既保证支持继承函数,又支持go.transform == transform 这样的比较
function Test(node)
local v = Vector3.one
local transform = LuaTransform.Extend(node)
local t = os.clock()
for i = 1, 200000 do
transform.position = transform.position
end
print('LuaTransform get set cost', os.clock() - t)
transform:Translate(1,1,1)
local child = transform:Find('child')
print('child is: ', tostring(child))
if child.parent == transform then
print('LuaTransform compare to userdata transform is ok')
end
transform.xyz = 123
transform.xyz = 456
print('extern field xyz is: '.. transform.xyz)
end
C#层负责传一个transform对象给Test方法调用,运行的结果如下:
可以发现,lua层在Extend中重写的同名属性和方法都被调用到了,甚至最后给transform新增的字段xyz也生效了。这是怎么做到的呢?我们来细细研究下函数LuaTransform.Extend
。
首先,可以看到第8行有一句:tolua.setpeer(u, t)
,这句话会调用到tolua_bnd_setpeer
:
static int tolua_bnd_setpeer(lua_State *L)
{
// stack: userdata, table
if (!lua_isuserdata(L, -2))
{
return luaL_error(L, "Invalid argument #1 to setpeer: userdata expected.");
}
if (lua_isnil(L, 2))
{
lua_pop(L, 1);
lua_pushvalue(L, TOLUA_NOPEER);
lua_setfenv(L, -2);
}
else
{
lua_pushvalue(L, 2); //stack: u p p
lua_setfenv(L, -3); //stack: u p
lua_newtable(L); //stack: u p vt
lua_pushlightuserdata(L, &vptr);
lua_pushvalue(L, 1);
lua_rawset(L, -3);
//lua_pushvalue(L, 1);
//lua_rawseti(L, -2, 1);
lua_getref(L, LUA_RIDX_VPTR); //stack: u p vt mt
lua_setmetatable(L, -2); //stack: u p vt
lua_pushstring(L, "base"); //stack: u p vt "base"
lua_pushvalue(L, -2); //stack: u p vt "base" vt
lua_rawset(L, 2); //stack: u p vt
lua_pop(L, 1);
}
return 0;
};
这个函数接收两个参数,第一个是userdata,表示C#类的对象,第二个是table,表示用来扩展该对象的lua数据结构。如果table为空,则userdata没有扩展table,那么就为它设置一个空的env table,这里TOLUA_NOPEER
就是registry table。其实到这,我们应该就已经猜到,tolua是使用env table的方式,把lua的table绑定到userdata上,从而实现对userdata的扩展。
由于扩展后的userdata它的类型依旧是userdata,我们希望能够通过它灵活访问到扩展前和扩展后的内容,所以这里又引入了一个vptr的table。它用一个light userdata的key,绑定了对应的userdata,以及名为LUA_RIDX_VPTR
的metatable。最后,我们设置env table的base
域为这个vptr。
第11-12行就是获取tolua里绑定C# get/set属性的table,将其绑定到新的env table上。通过修改get/set table,就可以改变C# get/set属性的行为了。听上去似乎云里雾里,我们不如直接来看下执行过这些步骤后,访问C# userdata的逻辑会发生哪些改变。在例子中,我们重写了transform的postion属性,和translate方法,那么在lua层实际调用时,会触发到class_index_event
函数中的如下逻辑:
lua_getfenv(L,1);
if (!lua_rawequal(L, -1, TOLUA_NOPEER)) // stack: t k env
{
while (lua_istable(L, -1)) // stack: t k v mt
{
lua_pushvalue(L, 2);
lua_rawget(L, -2);
if (!lua_isnil(L, -1))
{
return 1;
}
lua_pop(L, 1);
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2); //stack: obj key env tget
if (lua_istable(L, -1))
{
lua_pushvalue(L, 2); //stack: obj key env tget key
lua_rawget(L, -2); //stack: obj key env tget func
if (lua_isfunction(L, -1))
{
lua_pushvalue(L, 1);
lua_call(L, 1, 1);
return 1;
}
lua_pop(L, 1);
}
lua_pop(L, 1);
if (lua_getmetatable(L, -1) == 0) // stack: t k v mt mt
{
lua_pushnil(L);
}
lua_remove(L, -2); // stack: t k v mt
}
}
如果userdata上绑定的env table是有效table,那么优先访问该table,这很好理解,毕竟子类如果重写了父类的方法,调用时应当优先调用子类的实现。对于translate方法,就是直接去env table中获取key为translate的value;对于position属性,就是走get属性的table,获取到对应的value,它应该是一个function,调用之后返回的值为get返回的属性。当然,如果访问的是子类没有重写过的父类方法,则这里的判断都不会命中,会进而获取userdata的metatable,也就是前面我们所说过的class table,进行递归查找。
在子类的实现中,我们往往需要直接调用父类的方法。tolua这里类似,可以通过访问userdata的base域,由上文可知,这里返回的会是一个vptr的table。对它访问会触发它的metatable的__index
元方法,也就是vptr_index_event
函数:
static int vptr_index_event(lua_State *L)
{
lua_pushlightuserdata(L, &vptr);
lua_rawget(L, 1); // stack: t key u
lua_replace(L, 1); // stack: u key
lua_pushvalue(L, 1); // stack: u key u
while (lua_getmetatable(L, -1) != 0)
{
lua_remove(L, -2); // stack: u key mt
lua_pushvalue(L, 2); // stack: u key mt key
lua_rawget(L, -2); // stack: u key mt value
if (!lua_isnil(L, -1))
{
return 1;
}
lua_pop(L, 1);
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2); //stack: u 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);
}
}
访问vptr table的效果其实应当于直接访问扩展之前的userdata效果一样。所以这里有一个比较trick的实现,就是将vptr table中绑定过的userdata取出,再对userdata进行访问,即对它的metatable进行递归查找。之所以对所有的vptr table都设置同样一个这样的metatable,其原因是它们的逻辑是完全相同的,唯一不同就是各自绑定的userdata不同,因此只需要在metatable的元方法中,把vptr table绑定的userdata传进去就行了,没必要为每个vptr table都设置一个单独的metatable。这里的实现是比较巧妙的。
在例子中,我们还看到对扩展后的userdata进行判等的操作。由于扩展后的userdata依旧是userdata,它自身的value其实是不变的,所以这里判等一定是成立的。例子的最后展示了对userdata设置各种value的操作,如果是扩展前的C#类userdata,这里就会报错了,因为事先注册的set table中没有这些key。而扩展后就不一样了,这里的各种set,最终会往env table中进行set,而env table只是一个普通的lua table,当然就没有问题了。可以看到,对userdata进行扩展,能够极大地提高它使用的灵活性。
下一节我们将讨论lua层如何使用反射调用没有wrap过的C#函数。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊