UserData改良

lua-users home
wiki

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 沒有嘗試強制套用不存在的類比,就會更容易理解。

元資料有同層項目,而非環境

讓我們將使用者資料環境表格重新命名為其「同層表格」,並使其成為可選項目。這只需要一個非常輕微的調整:我們需要兩個新的 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。這個變更也很簡單。

它們也有 C 同層項目

現在,讓我們考慮使用者資料的另一個面向。在許多情況下,使用者資料只是封裝的指標,但有時同時具有相同結構繫結的兩個版本會很有用:一個是封裝的指標;另一個是非封裝的結構。在不複製大量程式碼的情況下處理這種二元性並不容易,依我淺見這是沒必要的。因此,以下提案也嘗試解決這個問題。

到目前為止,就 Udata 結構而言,我提議的只是一個重新命名的練習,還有一些不同的建立預設值。Udata 結構並沒有真正改變,因此它仍然會受到新增環境表格後產生的對齊問題的困擾。在 5.1 中,Udata 標頭現在實際上是五個指標/長整數:next、旗標、metatableenvsize。如果強制有效負載為雙指標對齊,則會在標頭中插入填充。如果不強制有效負載為雙指標對齊,幾乎可以保證它會未對齊雙指標。(因此,例如,在 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;
  }
這裡是程式碼變更草案:(請見以下的 Pseudo Patch)我改寫了一些程式碼片段來說明這些建議變更的影響。(請見以下的比較程式碼片段。)

最後,提供 __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_ISBOXEDLUA_NEEDSGC。如果後一個標記為關閉,則 GC 只會刪除物件,甚至不會嘗試尋找 __gc metamethod。


腳註

1. user_data 本身包含特定於執行個體的資訊,但環境表格新增將 Lua 物件與 user_data 執行個體關聯的可能性。

2. 資料類型共通資訊通常包括方法函數,而這些函數實際上會在元資料表格中由 __index 鍵所指的表格中。在此,我假設常見慣例將元資料表格 __index 鍵指向元資料表格本身(可能由實際的 __index 函數調停)。

-- RiciLake


程式碼片段 1

如果已知道 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;
  }

程式碼片段 2

建立套件和使用者資料本身的近似程式碼。此程式碼未曾測試過。我使用的實際繫結系統稍微有些不同。

A) 設定模組。

請注意,這可以抽象成一個具有多幾個參數的單一函數。
  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;
  }

B) 在使用者資料的方法中建立使用者資料的新執行個體

  newobj = lua_newuserdata(L, sizeof(*newobj));
  lua_pushvalue(L, LUA_ENVIRONINDEX);
  lua_setmetatable(L, -2);

C) 在任意方法中建立使用者資料的新執行個體

  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);

程式碼片段 3

A) 取得欄位

在實際生活中,我們可能審慎地檢查鍵,然後再從元資料表中檢索它,還有可能從環境表中檢索。在這裡我們僅從環境表(若有)或從 CFunction 的環境表中檢索,我們假設它與使用者的資料表的元資料表相同(或至少可以找得到類型方法的地方)。技巧在於,使用者的資料表的環境表設定為元資料表,用以指出沒有特定的環境表。這使我們可以省略一個查詢。但是,如下方(在比較程式碼片段中)所示,我們執行小小的 API 變更,這樣可以做得更好。
  // 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);
  }

B) 設定欄位

  // 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);
  }

比較的程式碼片段

建立一個封裝使用者資料

根據程式碼片段 2,範例 B 和 C

在使用者資料的方法中建立封裝使用者資料。

  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);
  }

取得和設定欄位

從程式碼片段 3 複製
  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);
  }

比較

在缺乏實際基準(無論如何,可能會出現偏頗)的情況下,我能做到的最好方法是計算 API 呼叫。API 呼叫數量看起來可能不是一個很重要的指標,但是分析結果似乎顯示大部分時間都花在 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())
如果我想允許 Lua 腳本來這麼做,就需要有一個地方來儲存已覆寫的 doKeyPress 成員函數。我無法將它儲存在標準的元表中,因為這會套用於所有實例。照邏輯來說,我應該將它儲存在小工具的環境變數表中,因為這對於小工具的實例而言是本地的。

當然,在常見的情況中,沒有任何方法會被覆寫。所以我不想要任何環境變數表。我希望方法查詢可以直接進入元表中。如果無法將環境變數表設定為 nil,就必須將它設定為哨兵,並在每次查詢時進行測試。所以我要尋找一些

a) 與(我的)環境變數表的預期用途相應的語意。

b) 在常見作業中會涉及較少 API 呼叫。

因此,目標並不深奧。它只是反映了我的想法,那就是將使用者資料的 env 變數表設定為目前執行的函數的 env 變數表是一個極不可能的預設值,而且能夠將它設定為 nil 是一個有用的功能。

歡迎留言


最近變更 · 偏好設定
編輯 · 歷史記錄
最後編輯於 2008 年 11 月 29 日下午 12:15 GMT (差異)