xLua添加Peer机制
peer这个概念来自tolua++,是对等、同等的意思。其主要目标是为了解决一个lua和c++(或者c#)交互的问题:
在lua下继承一个c#的类时,我们希望能给这个派生类添加一些新的成员变量,同时,希望在别的地方获取到这个派生类的对象时,也能正常访问到这些新的成员变量。
首先要理解lua下是怎样访问c#的对象的。一个c#的对象传给lua时,会生成一个userdata(相同的对象会用同一个userdata,xLua内部管理),同时设置这个userdata的元表,其元表可以理解为就是其对应的wrap类。
这样在lua下访问这个userdata时,就可以通过其元表访问到c#的成员变量和成员函数。
由于userdata是不能直接添加新成员的,所以我们需要创建一个额外的table,新成员会写入这个table,同时把这个table和userdata绑定,并修改userdata的__index
和__newindex
元方法,令其支持访问这个table即可。
在lua5.1下有个很简单高效的方法将一个table和userdata绑定,即通过environment。参考lua5.1的官方文档,在2.9 - Environment一节有:
Environments associated with userdata have no meaning for Lua. It is only a convenience feature for programmers to associate a table to a userdata.
这样我们可以通过lua_setfenv
把userdata和table绑定。
在xlua.c中添加getpeer
和setpeer
方法,并导出给lua:
#if LUA_VERSION_NUM == 501
LUA_API int setpeer(lua_State* L) {
/* stack: userdata, table */
if (!lua_isuserdata(L, -2)) {
lua_pushstring(L, "Invalid argument #1 to setpeer: userdata expected.");
lua_error(L);
};
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushvalue(L, XLUA_NOPEER);
};
lua_setfenv(L, -2);
return 0;
};
LUA_API int getpeer(lua_State* L) {
/* stack: userdata */
lua_getfenv(L, -1);
if (lua_rawequal(L, -1, XLUA_NOPEER)) {
lua_pop(L, 1);
lua_pushnil(L);
};
return 1;
};
#endif
static const luaL_Reg xlualib[] = {
{"sethook", profiler_set_hook},
{"genaccessor", gen_css_access},
{"structclone", css_clone},
{"setpeer", setpeer},
{"getpeer", getpeer},
{NULL, NULL}
};
其中的XLUA_NOPEER
会在后面讲到。
然后在lua文件中,添加生成lua派生类对象的方法:
-- 给userdata绑定peer
local bind_peer
bind_peer = function(ud, cls)
if type(ud) == "userdata" then
local peer = xlua.getpeer(ud)
if not peer then
peer = {}
xlua.setpeer(ud, peer)
end
setmetatable(peer, cls)
end
end
-- ...
cls.new_with_cs_instance = function(cs_instance)
local obj
-- 递归调用构造函数
do
local create
create = function(c, cs_instance)
if c.super then
create(c.super, cs_instance)
end
if not obj then
obj = cs_instance
bind_peer(obj, cls)
obj['.is_cs_instance'] = true
obj.class = cls
end
if c.ctor then
c.ctor(obj)
end
end
create(cls, cs_instance)
end
return obj
end
这样c#层通过特殊函数new_with_cs_instance
,可以创建指定的lua派生类对象,这些对象在创建时就会绑定一个lua table。
这里有个坑,看下面的代码:
local peer = xlua.getpeer(t)
if not peer then
peer = {}
xlua.setpeer(t, peer)
end
setmetatable(peer, index)
这代码的意思就是,如果这个userdata没有绑定的table,就给它绑定一个新的table。由于底层是通过绑定userdata的环境来实现,但在lua5.1下,这个环境默认不是nil
,而且不是一个固定值。可以参考这篇文章how-to-detect-if-a-userdata-has-environment-table。
基本思路就是在userdata创建时,我们会给它设置一个默认的环境,后面再通过判断这个userdata的环境是不是默认环境来确定其是否有绑定table。
这里我们采用lua的注册表来当成默认环境。在xlua.c里添加如下代码:
#define XLUA_NOPEER LUA_REGISTRYINDEX
// ...
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
int* pointer = (int*)lua_newuserdata(L, sizeof(int));
*pointer = key;
if (need_cache) cacheud(L, key, cache_ref);
lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
lua_setmetatable(L, -2);
#if LUA_VERSION_NUM == 501
lua_pushvalue(L, XLUA_NOPEER);
lua_setfenv(L, -2);
#endif
}
最后,我们需要修改xLua的通用__index
和__newindex
函数来支持peer查询。修改后的代码如下:
//upvalue --- [1]: methods, [2]:getters, [3]:csindexer, [4]:base, [5]:indexfuncs, [6]:arrayindexer, [7]:baseindex
//param --- [1]: obj, [2]: key
LUA_API int obj_indexer(lua_State *L) {
int t = lua_type(L,1);
if (t == LUA_TUSERDATA) {
/* Access alternative table */
#ifdef LUA_VERSION_NUM == 501
lua_getfenv(L,1);
if (!lua_rawequal(L, -1, XLUA_NOPEER)) {
lua_pushvalue(L, 2); /* key */
lua_gettable(L, -2); /* on lua 5.1, we trade the "xlua_peers" lookup for a gettable call */
if (!lua_isnil(L, -1))
return 1;
};
#endif
}
if (!lua_isnil(L, lua_upvalueindex(1))) {
lua_pushvalue(L, 2);
lua_gettable(L, lua_upvalueindex(1));
if (!lua_isnil(L, -1)) {//has method
return 1;
}
lua_pop(L, 1);
}
if (!lua_isnil(L, lua_upvalueindex(2))) {
lua_pushvalue(L, 2);
lua_gettable(L, lua_upvalueindex(2));
if (!lua_isnil(L, -1)) {//has getter
lua_pushvalue(L, 1);
lua_call(L, 1, 1);
return 1;
}
lua_pop(L, 1);
}
if (!lua_isnil(L, lua_upvalueindex(6)) && lua_type(L, 2) == LUA_TNUMBER) {
lua_pushvalue(L, lua_upvalueindex(6));
lua_pushvalue(L, 1);
lua_pushvalue(L, 2);
lua_call(L, 2, 1);
return 1;
}
if (!lua_isnil(L, lua_upvalueindex(3))) {
lua_pushvalue(L, lua_upvalueindex(3));
lua_pushvalue(L, 1);
lua_pushvalue(L, 2);
lua_call(L, 2, 2);
if (lua_toboolean(L, -2)) {
return 1;
}
lua_pop(L, 2);
}
if (!lua_isnil(L, lua_upvalueindex(4))) {
lua_pushvalue(L, lua_upvalueindex(4));
while(!lua_isnil(L, -1)) {
lua_pushvalue(L, -1);
lua_gettable(L, lua_upvalueindex(5));
if (!lua_isnil(L, -1)) // found
{
lua_replace(L, lua_upvalueindex(7)); //baseindex = indexfuncs[base]
lua_pop(L, 1);
break;
}
lua_pop(L, 1);
lua_getfield(L, -1, "BaseType");
lua_remove(L, -2);
}
lua_pushnil(L);
lua_replace(L, lua_upvalueindex(4));//base = nil
}
if (!lua_isnil(L, lua_upvalueindex(7))) {
lua_settop(L, 2);
lua_pushvalue(L, lua_upvalueindex(7));
lua_insert(L, 1);
lua_call(L, 2, 1);
return 1;
} else {
return 0;
}
}
static void storeatubox (lua_State* L, int lo) {
// 参考tolua++里的实现,暂时去掉了非5.1版本的支持
// 如需支持其他lua版本,请自觉根据tolua++的实现,采用额外的一张表存储userdata和其绑定的table
#if LUA_VERSION_NUM == 501
lua_getfenv(L, lo);
if (lua_rawequal(L, -1, XLUA_NOPEER)) {
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfenv(L, lo); /* stack: k,v,table */
};
lua_insert(L, -3);
lua_settable(L, -3); /* on lua 5.1, we trade the "xlua_peers" lookup for a settable call */
lua_pop(L, 1);
#endif
}
//upvalue --- [1]:setters, [2]:csnewindexer, [3]:base, [4]:newindexfuncs, [5]:arrayindexer, [6]:basenewindex
//param --- [1]: obj, [2]: key, [3]: value
LUA_API int obj_newindexer(lua_State *L) {
int t = lua_type(L,1);
if (!lua_isnil(L, lua_upvalueindex(1))) {
lua_pushvalue(L, 2);
lua_gettable(L, lua_upvalueindex(1));
if (!lua_isnil(L, -1)) {//has setter
lua_pushvalue(L, 1);
lua_pushvalue(L, 3);
lua_call(L, 2, 0);
return 0;
}
lua_pop(L, 1);
}
if (!lua_isnil(L, lua_upvalueindex(2))) {
lua_pushvalue(L, lua_upvalueindex(2));
lua_pushvalue(L, 1);
lua_pushvalue(L, 2);
lua_pushvalue(L, 3);
lua_call(L, 3, 1);
if (lua_toboolean(L, -1)) {
return 0;
}
}
if (!lua_isnil(L, lua_upvalueindex(5)) && lua_type(L, 2) == LUA_TNUMBER) {
lua_pushvalue(L, lua_upvalueindex(5));
lua_pushvalue(L, 1);
lua_pushvalue(L, 2);
lua_pushvalue(L, 3);
lua_call(L, 3, 0);
return 0;
}
if (!lua_isnil(L, lua_upvalueindex(3))) {
lua_pushvalue(L, lua_upvalueindex(3));
while(!lua_isnil(L, -1)) {
lua_pushvalue(L, -1);
lua_gettable(L, lua_upvalueindex(4));
if (!lua_isnil(L, -1)) // found
{
lua_replace(L, lua_upvalueindex(6)); //basenewindex = newindexfuncs[base]
lua_pop(L, 1);
break;
}
lua_pop(L, 1);
lua_getfield(L, -1, "BaseType");
lua_remove(L, -2);
}
lua_pushnil(L);
lua_replace(L, lua_upvalueindex(3));//base = nil
}
if (!lua_isnil(L, lua_upvalueindex(6))) {
lua_settop(L, 3);
lua_pushvalue(L, lua_upvalueindex(6));
lua_insert(L, 1);
lua_call(L, 3, 0);
return 0;
} else if (t == LUA_TUSERDATA) {
storeatubox(L, 1);
return 0;
} else {
return luaL_error(L, "cannot set %s, no such field", lua_tostring(L, 2));
}
}
PS:
- 以上的实现基本参考自tolua++,有兴趣的可以看下tolua++的实现,主要参考文件tolua_map.c以及tolua_event.c。
- 这里只实现了lua5.1版本的支持,由于lua5.3已经取消了
lua_setfenv
函数,也修改了environment概念。要支持lua5.3,需要额外创建一个xlua_peers的table,记录全局的userdata和table的绑定关系。由于我们采用luajit,也就是用的是lua5.1,有需要支持5.3的可以参考tolua++的实现。 - 这种实现方法有一个小坑,如果在C#层有一个成员是可访问不可修改的,也就是类似
public int a { get; protected set; }
这样的情况。这时如果在lua派生类中,复写这个变量,也就是类似self.a = 5
,这样以后在lua就只能访问到lua下变量a,访问不到c#的变量a。但在c#层还是只能访问c#的变量a。所以尽量不要复写c#的成员变量!