用户数据细化 |
|
也许我应该先写这个,因为有几个人指出了这一点(参见下面的动机部分)。
用户数据既有元表,也有环境表。似乎很合乎逻辑的是,元表包含与用户数据数据类型相关的通用信息,而环境表包含与用户数据实例相关的特定信息。(参见底部的脚注。如何在维基中插入锚点?)
方法也有环境表。方法通常对用户数据类型的所有实例都是通用的;人们会期望它们的环境表引用用户数据的元表(或所有类型实例共享的另一个表)。除非方法本身特定于实例,否则很难想象方法函数的环境表会是用户数据环境表的情况。
因此,我们通常会期望某种关系(可能是相等)存在于
(参见下面的代码片段 1。)
当前创建用户数据的 API 默认将用户数据的元表初始化为NULL,并将它的环境表初始化为调用者的环境表。但这似乎与常见情况相反,根据上述分析,常见情况应该是将用户数据的元表初始化为调用者的环境表,并将它的环境表初始化为NULL。(在当前实现中,用户数据环境表不能为NULL,但请参见下文。)(参见下面的代码片段 2。)
现在,可能给定的用户数据类型总是需要环境表(例如 Mike Pall 有用的队列示例,尽管即使是空队列也可能不需要环境表),但也有一些用例,其中环境表是可选的。例如,可以使用环境表来存储属于脚本环境而不是 C 实现的实例属性,甚至可以使用它来允许用户数据方法被 Lua 代码覆盖特定实例。(这意味着__index 元方法的实现会进行适当的查找。)
当前实现没有简单的方法来指定“没有环境表”。可以创建一个空表,但是如果在用户数据类型实例中很少使用环境表,这将非常浪费。我选择的解决方法是将用户数据环境表设置为元表(即创建用户数据的函数的环境表),并检查该条件。(参见下面的代码片段 3。)
因此,简而言之,用户数据环境表很有用且可用,但实现感觉很笨拙,因为它不符合(我所看到的)常见情况。
大约一年前,我提出了一种略有不同(也存在缺陷)的实现方案 [1]。我现在认为该方案存在缺陷,因为它试图以某种方式重用 lua_raw* 函数,就像我认为提出的 5.1 实现方案存在缺陷,因为它试图重用 lua_{g,s}etfenv 函数一样。事实是,userdata 和实例表之间的关联既不属于元表也不属于环境表,如果 API 不试图强加一个不存在的类比,它会更容易理解。
lua_{g,s}etmetatable() 而不是 lua_{g,s}etfenv(),但它们基本上具有相同的效果。
/** * If the indexed object is a metatable and has a peer table, push it onto * the stack and return 1. Otherwise, leave the stack unaltered and return 0 */ int lua_getpeer (lua_State *L, int index); /** * If the indexed object is a metatable, set its peer table to the table * on the top of the stack, or to NULL if the top of the stack is nil, * and return 1, Otherwise return 0. Pop the stack in either case. */ int lua_setpeer (lua_State *L, int index);
执行此操作的实际代码基本上只是从 lua_getfenv() 和 lua_setfenv() API 中移动,并且不会使 lapi.o 的大小增加超过几个字节。唯一需要修改的另一个地方是 lgc.c,其中必须检查 peer 是否为 NULL,类似于检查 metatable 是否为 NULL。
此外,为了涵盖在创建时将元表附加到 userdata 的常见情况,我们增强了 lua_newuserdata() 以接受一个额外的参数,该参数是元表的索引或 0。一个常见的调用将是
self = lua_newuserdatameta(L, sizeof(*self), LUA_ENVIRONINDEX); // but read on
NULL。此更改也很简单。
到目前为止,关于Udata结构,我提出的只是重命名和一些不同的创建默认值。Udata结构本身并没有真正改变,因此它仍然受到添加环境表带来的对齐问题的影响。在 5.1 中,Udata 头部现在实际上是五个指针/长整型:next、flags、metatable、env、size。如果强制有效载荷进行双指针对齐,则会在头部引入填充。如果有效载荷没有强制进行双指针对齐,则几乎可以肯定它会是双指针未对齐的。(例如,在 x86 中,如果有效载荷是双精度浮点数的向量,则它们都将是双字未对齐的。)因此,似乎在Udata头部添加另一个指针的成本相当小。
在包含一个装箱指针的用户数据的典型情况下,有效载荷的大小只是一个void*;我们实际上可以将它放在Udata头部,并改善对齐(在某些平台上,甚至可以利用未使用的填充)。但在这种情况下,我们可以始终将此指针设置为用户数据有效载荷的地址,这意味着无论用户数据是否装箱,都可以通过一个条件语句查找有效载荷地址。这与UpVal的实现方式非常相似。
现在,任何只需要知道与用户数据对应的C结构地址的CFunction都可以简单地用lua_tocpeer()替换lua_touserdata(),并与装箱或未装箱的用户数据版本一起使用。实际上,lua_touserdata()可能应该返回完整用户数据的cpeer,而新的 API 函数应该类似于lua_topayload()。
真正关心用户数据是否装箱的元方法是__gc元方法(如果存在)。幸运的是,CommonHeader中只使用了两个标志,因此有空间插入一个isboxed标志字节,而不会进一步膨胀Udata。所以我们只需要在新的用户数据 API 中添加(另一个!)参数。
Udata *luaS_newudata (lua_State *L, size_t s, Table *e, void *cpeer) {
// ...
u->uv.isboxed = (cpeer != NULL);
u->uv.metatable = e;
u->uv.peer = NULL;
u->uv.cpeer = cpeer ? cpeer : rawuvalue(o) + 1;
// ...
/* One new api function; the other one queries isboxed. */
void *lua_tocpeer (lua_State *L, int index) {
StkId o = index2adr(L, idx);
api_checkvalidindex(L, o);
api_check(L, ttisuserdata(L, o));
return uvalue(o)->cpeer;
}
最后,一些关于__gc元方法的可选细节。鉴于上述内容,我们可能期望__gc元方法看起来像这样
int foo_gc (lua_State *L) {
Foo *self = lua_tocpeer(L, 1);
foo_destruct(self); // delete self's references
if (lua_isboxed(L, 1)) foo_free(self); // free self's storage
return 0;
}
Foo对象本身是原子的;也就是说,没有foo_destruct()。然后,只需要在封装的用户数据上运行__gc方法。为了方便这一点,我们可以在isboxed字节中设置两个标志:LUA_ISBOXED和LUA_NEEDSGC。如果后一个标志关闭,那么垃圾回收将简单地删除对象,而不会尝试查找__gc元方法。
2. 数据类型通用信息通常包括方法函数,这些函数实际上位于元表中__index键所引用的表中。在这里,我假设将元表__index键指向元表本身的常见约定(可能由实际的__index函数进行中介)。
-- RiciLake
如果已知CFunction环境和userdata元表是同一个,我们可以使用以下代码代替luaL_checkudata()
void *luaL_checkself (lua_State *L) {
lua_getmetatable(L, 1);
if (!lua_rawequal(L, -1, LUA_ENVIRONINDEX))
luaL_error(L, "Method called without self or with incorrect self");
lua_pop(L, 1);
return lua_touserdata(L, 1);
}
luaL_checkudata()相比,这节省了表查找和字符串比较;鉴于此函数必须由每个方法调用(为了安全起见),时间节省可能是显著的。
上面的代码可以扩展到涵盖元表标识不足以识别用户数据类型的情况,也许是因为存在多个适用的元表。例如,以下操作是可能的(请注意,它故意将元表留在堆栈上),并将其留给调用者来生成错误消息)
void *luaL_getselfmeta (lua_State *L) {
lua_getmetatable(L, 1);
if (!lua_isnil(L, -1)) {
lua_pushvalue(L, LUA_ENVIRONINDEX);
lua_gettable(L, -2); // Are we one of the metatable's peers?
if (!lua_isnil(L, -1)) {
lua_pop(L, 1); // Ditch the sentinel. Could have been pop 2
return lua_touserdata(L, 1);
}
}
return NULL;
}
创建包和用户数据本身的近似代码。此代码未经测试;我使用的实际绑定系统略有不同。
int luaopen_foo (lua_State *L) {
// Check that the typename has not been used
lua_getfield(L, LUA_REGISTRYINDEX, FOO_TYPENAME);
if (!lua_isnil(L, -1))
// Instead of throwing an error, we could just use the returned table
luaL_error(L, LUA_QS "is already in use.", FOO_TYPENAME);
// Make the metatable
lua_newtable(L);
// Register it in the Registry
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, FOO_TYPENAME);
// Arrange for methods to inherit the metatable as env table
lua_pushvalue(L, -1);
lua_replace(L, LUA_ENVIRONINDEX);
// Fill in the metatable
luaL_openlib(L, NULL, mytypemethod_reg, 0);
// Make the actual package
luaL_openlib(L, MYTYPE_PACKAGE, mytypepkg_reg, 0);
return 1;
}
newobj = lua_newuserdata(L, sizeof(*newobj)); lua_pushvalue(L, LUA_ENVIRONINDEX); lua_setmetatable(L, -2);
newobj = lua_newuserdata(L, sizeof(*newobj));
lua_getfield(L, LUA_REGISTRYINDEX, FOO_TYPENAME);
if (lua_isnil(L, -1))
luaL_error(L, "Userdata type " LUA_QS " has not been registered",
FOO_TYPENAME);
// Set both the metatable and the environment table
lua_pushvalue(L, -1);
lua_setfenv(L, -3);
lua_setmetatable(L, -2);
// Push the value of the indicated field either from the environment
// table of the indexed userdata or from the environment table of the
// calling function.
void getenvfield (lua_State *L, int index, const char *fieldname) {
lua_getfenv(L, index);
lua_getfield(L, -1, fieldname);
if (lua_isnil(L, -1)
&& !lua_rawequal(L, -2, LUA_ENVIRONINDEX)) {
lua_pop(L, 2);
lua_getfield(L, LUA_ENVIRONINDEX, fieldname);
}
else
lua_replace(L, -2);
}
// Put the value on the top of the stack in the environment of the
// indexed userdata with the specified fieldname
void setenvfield (lua_State *L, int index, const char *fieldname) {
lua_getfenv(L, index);
if (lua_rawequal(L, -1, LUA_ENVIRONINDEX)) {
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfenv(L, index); // Only works if index > 0
}
lua_insert(L, -2);
lua_setfield(L, -2, fieldname);
}
在 userdata 的方法内部创建带盒子的 userdata。
void newboxed_self (lua_State *L, void *obj) {
void **newbox = lua_newuserdata(L, sizeof(*newbox));
lua_pushvalue(L, LUA_ENVIRONINDEX);
lua_setmetatable(L, -2);
*newbox = obj;
}
void newboxed_type (lua_State *L, const char *typename, void *obj) {
void *newobj = lua_newuserdata(L, sizeof(*newobj));
lua_getfield(L, LUA_REGISTRYINDEX, FOO_TYPENAME);
if (lua_isnil(L, -1))
luaL_error(L, "Userdata type " LUA_QS " has not been registered",
FOO_TYPENAME);
// Set both the metatable and the environment table
lua_pushvalue(L, -1);
lua_setfenv(L, -3);
lua_setmetatable(L, -2);
}
使用对等表
void newboxed_self (lua_State *L, void *obj) {
lua_newuserdata_ex(L, 0, LUA_ENVIRONINDEX, obj);
}
void newboxed_type (lua_State *L, const char *typename, void *obj) {
lua_getfield(L, LUA_REGISTRYINDEX, typename);
if (lua_isnil(L, -1))
luaL_error(L, "Userdata type " LUA_QS " has not been registered",
typename);
lua_newuserdata_ex(L, 0, -1, obj);
lua_replace(L, -2);
}
void getenvfield (lua_State *L, int index, const char *fieldname) {
lua_getfenv(L, index);
lua_getfield(L, -1, fieldname);
if (lua_isnil(L, -1)
&& !lua_rawequal(L, -2, LUA_ENVIRONINDEX)) {
lua_pop(L, 2);
lua_getfield(L, LUA_ENVIRONINDEX, fieldname);
}
else
lua_replace(L, -2);
}
void setenvfield (lua_State *L, int index, const char *fieldname) {
lua_getfenv(L, index);
if (lua_rawequal(L, -1, LUA_ENVIRONINDEX)) {
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setfenv(L, index); // Only works if index > 0
}
lua_insert(L, -2);
lua_setfield(L, -2, fieldname);
}
void getpeerfield (lua_State *L, int index, const char *fieldname) {
if (lua_getpeer(L, index)) {
lua_getfield(L, -1, fieldname);
if (!lua_isnil(L, -1)) {
lua_replace(L, -2);
return;
}
}
lua_getfield(L, LUA_ENVIRONINDEX, fieldname);
}
void setpeerfield (lua_State *L, int index, const char *fieldname) {
if (!lua_getpeer(L, index)) {
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setpeer(L, index); // Still only works if index > 0
}
lua_insert(L, -2);
lua_setfield(L, -2, fieldname);
}
current proposed
newself: 3/2 1/1
newtype: 6/5 4/4
getfield (* common case):
peer, found in peer: 4/4 4/4
peer, found in fn env; 6/7 5/5
peer, not found: 6/7 5/5
*No peer, found in fn env: 4/4 2/2
No peer, not found: 5/6 2/2
setfield (* common case):
*peer 4/5 3/3
no peer 8/8 6/5
以下是伪补丁格式的大部分更改(!表示更改,+ 表示添加,- 表示删除)。这些代码都没有实际尝试过 :)
/* In lobject.h */
typedef union Udata {
L_Umaxalign dummy; /* ensures maximum alignment for `local' udata */
struct {
CommonHeader;
+ lu_byte isboxed;
struct Table *metatable;
! struct Table *peer;
+ void *cpeer;
size_t len;
} uv;
} Udata;
/* In lstring.c; the header needs to be changed as well */
! Udata *luaS_newudata (lua_State *L, size_t s, Table *e, void *cpeer) {
Udata *u;
if (s > MAX_SIZET - sizeof(Udata))
luaM_toobig(L);
u = cast(Udata *, luaM_malloc(L, s + sizeof(Udata)));
u->uv.marked = luaC_white(G(L)); /* is not finalized */
u->uv.tt = LUA_TUSERDATA;
+ u->uv.isboxed = (cpeer != NULL);
u->uv.len = s;
! u->uv.metatable = e;
! u->uv.peer = NULL;
+ u->uv.cpeer = cpeer ? cpeer : rawuvalue(o) + 1;
/* chain it on udata list (after main thread) */
u->uv.next = G(L)->mainthread->next;
G(L)->mainthread->next = obj2gco(u);
return u;
}
/* in lapi.c */
+ LUA_API void *lua_tocpeer (lua_State *L, int idx) {
+ StkId o = index2adr(L, idx);
+ api_checkvalidindex(L, o);
+ api_check(L, ttisuserdata(L, o));
+ return uvalue(o)->cpeer;
+ }
+ LUA_API int lua_isboxed (lua_State *L, int idx) {
+ StkId o = index2apr(L, idx);
+ api_checkvalidindex(L, o);
+ api_check(L, ttisuserdata(L, o));
+ return uvalue(o)->isboxed;
+ }
! LUA_API void *lua_newuserdata_ex (lua_State *L, size_t size,
! int idx, void *cpeer) {
Udata *u;
+ Table *h = NULL;
lua_lock(L);
luaC_checkGC(L);
+ if (idx) {
+ api_check(L, ttistable(index2adr(L, idx)));
+ h = hvalue(index2adr(L, idx));
+ }
! u = luaS_newudata(L, size, h, cpeer);
setuvalue(L, L->top, u);
api_incr_top(L);
lua_unlock(L);
return u + 1;
}
LUA_API void lua_getfenv (lua_State *L, int idx) {
StkId o;
lua_lock(L);
o = index2adr(L, idx);
api_checkvalidindex(L, o);
! if (ttype(o) == LUA_TFUNCTION) {
- case LUA_TFUNCTION:
sethvalue(L, L->top, clvalue(o)->c.env);
+ }
+ else {
- break;
- case LUA_TUSERDATA:
- sethvalue(L, L->top, uvalue(o)->env);
- break;
- default:
setnilvalue(L->top);
break;
}
api_incr_top(L);
lua_unlock(L);
}
+ LUA_API int lua_getpeer (lua_State *L, int idx) {
+ const TValue *o;
+ Table *peer = NULL;
+ int res;
+ lua_lock(L);
+ o = index2adr(L, idx);
+ api_checkvalidindex(L, o);
+ if (ttype(o) == LUA_TUSERDATA)
+ peer = uvalue(o)->peer;
+ if (peer == NULL)
+ res = 0;
+ else {
+ sethvalue(L, L->top, h);
+ api_incr_top(L);
+ res = 1;
+ }
+ lua_unlock(L);
+ return res;
+ }
LUA_API int lua_setfenv (lua_State *L, int idx) {
StkId o;
int res = 1;
lua_lock(L);
api_checknelems(L, 1);
o = index2adr(L, idx);
api_checkvalidindex(L, o);
api_check(L, ttistable(L->top - 1));
- switch (ttype(o)) {
- case LUA_TFUNCTION:
+ if (ttype(o) == LUA_TFUNCTION) {
clvalue(o)->c.env = hvalue(L->top - 1);
- break;
- case LUA_TUSERDATA:
- uvalue(o)->env = hvalue(L->top - 1);
- break;
- default:
- res = 0;
- break;
- }
luaC_objbarrier(L, gcvalue(o), hvalue(L->top - 1));
+ }
+ else
+ res = 0;
L->top--;
lua_unlock(L);
return res;
}
+ LUA_API int lua_setpeer (lua_State *L, int idx) {
+ TValue *o;
+ Table *peer;
+ int res;
+ lua_lock(L);
+ api_checknelems(L, 1);
+ o = index2adr(L, idx);
+ api_checkvalidindex(L, o);
+ if (ttisnil(L->top - 1))
+ peer = NULL;
+ else {
+ api_check(L, ttistable(L->top - 1));
+ peer = hvalue(L->top - 1);
+ }
+ if (ttype(obj) == LUA_TUSERDATA) {
+ uvalue(obj)->peer = peer;
+ if (peer != NULL)
+ luaC_objbarriert(L, rawuvalue(obj), peer);
+ res = 1;
+ }
+ else
+ res = 0;
+ L->top--;
+ lua_unlock(L);
+ return res;
+ }
/* In lua.h */
+ LUA_API void *lua_tocpeer (lua_State *L, int index);
+ LUA_API int lua_isboxed (lua_State *L, int idx);
+ LUA_API void *lua_newuserdata_ex (lua_State *L, size_t size,
+ int idx, void *cpeer);
! #define lua_newuserdata(L,sz) lua_newuserdata_ex(L, sz, 0, NULL)
+ LUA_API int lua_getpeer (lua_State *L, int idx);
+ LUA_API int lua_setpeer (lua_State *L, int idx);
/* in lgc.c, reallymarkobject */
case LUA_TUSERDATA: {
Table *mt = gco2u(o)->metatable;
+ Table *peer = gco2u(o)->peer;
gray2black(o); /* udata are never gray */
if (mt) markobject(g, mt);
! if (peer) markobject(g, peer);
return;
}
然后可以将任何表设置为环境,该环境可以替换对象的现有环境。对吗?
是的,确实如此。但是你不能将“无表”设置为对象的现有环境。
考虑以下情况:我将 `myFancyWidget` 绑定到 Lua userdata 并将其导出到 Lua 环境中。
Lua 脚本可能希望覆盖 `MyFancyWidget` 的特定实例中的某些方法(使其更花哨,也许 :)。现在,它可以创建一个全新的对象来做到这一点,但这将比这样做简单得多
function overrideCtlY(widget)
local oldDoKeyPress = myFancyWidget.doKeyPress
function widget:doKeyPress(key)
if key == "ctl-y" then
-- handle control y the way I want to
else
return oldDoKeyPress(widget, key)
end
end
return widget
end
local widget = overrideCtlY(MyFancyWidget.new())
doKeyPress 成员函数。我不能将其存储在标准元表中;那样会应用于所有实例。从逻辑上讲,我应该将其存储在小部件的环境表中,因为它是小部件实例的本地表。
当然,在常见情况下,不会覆盖任何方法。因此,我根本不需要环境表;我希望方法查找直接转到元表。如果我不能将环境表设置为 nil,那么我必须将其设置为某个哨兵,并在每次查找时对其进行测试。因此,我一直在寻找一些
a) 在语义上对应于(我)对环境表的预期使用。
b) 在常见操作中涉及更少的 API 调用。
因此,目标并不深刻。它只是反映了我的想法,即将用户数据的 env 表设置为当前正在运行函数的 env 表是一个极不可能的默认值,并且能够将其设置为 nil 是一个有用的功能。