UserData改良 |
|
也許我應該先寫這個,就像幾個人指出的(請參閱下方的「動機」部分)。
一個UserData同時具有一個元資料表和一個環境表格。元資料表似乎包含userdatatype的一般資訊,而環境表格則包含特定於userdata實例的資訊。(請參閱底部的註腳。您如何在此Wiki中插入錨點?)
方法也有環境表格。方法通常是userdata類型的所有實例共有的;您會期望他們的環境表格參考userdata的元資料表(或所有類型的實例共有的另一個表格)。除非方法本身特定於實例,否則很難想像方法函數的環境表格會是userdata的環境表格。
所以我們通常會期望某種關係(可能是相等)
(請參閱下面的程式碼片段1。)
建立userData的目前API預設初始化userdata的元資料表為NULL
,其環境表格為呼叫者的環境表格。這似乎與一般情況相反,一般情況,根據以上的分析,應當初始化userdata的元資料表為呼叫者的環境表格,而其環境表格為NULL
。(userdata環境表格在目前實作中不能是NULL
,但請參閱下方文字。)(請參閱下面的程式碼片段2。)
現在,一個userdata類型可能通常需要一個環境表格(例如Mike Pall有用的佇列範例,儘管即使在這種情況下,空佇列也可能不需要環境表格),但也有一些使用情況中環境表格是可選的。例如,您可能使用環境表格來儲存屬於腳本環境而非C實作的實例屬性,甚至可以將其用於允許Lua程式碼覆寫特定實例的userdata方法。(這暗示著__index
後設方法的實作,它執行適當的查詢。)
目前的實作沒有簡單的方式表明「沒有環境表格」。可以建立一個空表格,但是,如果環境表格在userdata類型的實例中不常見,那麼將會造成極大的浪費。我選擇的解決方法是將userdata的環境表格設為元資料表(亦即建立userdata的函數的環境表格),並檢查該條件。(請參閱下面的程式碼片段3。)
因此,簡而言之,使用者資料環境表格是有用且可用的,但實作感覺很奇怪,因為它不符合(我認為)常見的狀況。
大約在一年之前,我提出了一個稍有不同(而且也有瑕疵)的實作 [1]。現在,我認為該提議有瑕疵,因為它嘗試重複使用 lua_raw*
函式,有點像我認為建議的 5.1 實作有瑕疵,因為它嘗試重複使用 lua_{g,s}etfenv
函式。事實上,使用者資料和實例表格之间的關聯與元資料表或環境表格不同,並且如果 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
。
此外,為了涵蓋在建立時附加元資料表的常見情況,我們擴充 lua_newuserdata()
以接受一個額外的引數,這是元資料表的索引或 0。一個常見的呼叫會是
self = lua_newuserdatameta(L, sizeof(*self), LUA_ENVIRONINDEX); // but read on
NULL
。這個變更也很簡單。
到目前為止,就 Udata
結構而言,我提議的只是一個重新命名的練習,還有一些不同的建立預設值。Udata
結構並沒有真正改變,因此它仍然會受到新增環境表格後產生的對齊問題的困擾。在 5.1 中,Udata
標頭現在實際上是五個指標/長整數:next
、旗標、metatable
、env
、size
。如果強制有效負載為雙指標對齊,則會在標頭中插入填充。如果不強制有效負載為雙指標對齊,幾乎可以保證它會未對齊雙指標。(因此,例如,在 x86 中,如果有效負載是雙倍向量,它們全部都會未對齊雙字。)因此,在 Udata
標頭中新增另一個指標的成本似乎很低。
在典型情況下,user_data 包含一個 boxed 指標,負載量僅為 void*
的大小;我們實際上可以將它放入 Udata
標頭並改善對齊方式(在一些平台上,甚至可以利用未使用的填充。)但實際上,我們一致將此指標設定為 user_data 負載量的位址,表示無條件查看負載量位址,不論 user_data 是否是 boxed。這與 UpVal
實作方式非常類似。
現在,任何僅需要知道對應於 user_data 之 C
結構位址的 CFunction
可以簡單以 lua_tocpeer()
取代 lua_touserdata()
,並與 boxed 或 unboxed 版本的 user_data 搭配使用。事實上,lua_touserdata()
應可為完整 user_data 傳回 cpeer
,而新的 API 函數應類似於 lua_topayload()
。
__gc
metamethod 真的很在乎 user_data 是否為 boxed,如果它存在的話。幸運的是,僅在 CommonHeader
中使用兩個標記,因此可以在不進一步擴增 Udata
的情況下,插入一個 isboxed
標記位元組。因此我們只需在 newuserdata 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
metamethod 幾個可選擇的優點。考量上述,我們可能會預期 __gc
metamethod 看起來像這樣
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()
。然後只需在 boxed user_data 上執行 __gc
方法。為方便此目的,我們可以在 isboxed
位元組中放入兩個標記:LUA_ISBOXED
和 LUA_NEEDSGC
。如果後一個標記為關閉,則 GC 只會刪除物件,甚至不會嘗試尋找 __gc
metamethod。
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); }
在使用者資料的方法中建立封裝使用者資料。
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); }
index2adr()
。以下數字是 api 呼叫
/index2adr 呼叫
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 使用者資料並匯出到 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
是一個有用的功能。