用户数据细化

lua-users home
wiki

用户数据“环境”表

动机

也许我应该先写这个,因为有几个人指出了这一点(参见下面的动机部分)。

一个小建议

用户数据既有元表,也有环境表。似乎很合乎逻辑的是,元表包含与用户数据数据类型相关的通用信息,而环境表包含与用户数据实例相关的特定信息。(参见底部的脚注。如何在维基中插入锚点?

方法也有环境表。方法通常对用户数据类型的所有实例都是通用的;人们会期望它们的环境表引用用户数据的元表(或所有类型实例共享的另一个表)。除非方法本身特定于实例,否则很难想象方法函数的环境表会是用户数据环境表的情况。

因此,我们通常会期望某种关系(可能是相等)存在于

(参见下面的代码片段 1。)

当前创建用户数据的 API 默认将用户数据的元表初始化为NULL,并将它的环境表初始化为调用者的环境表。但这似乎与常见情况相反,根据上述分析,常见情况应该是将用户数据的元表初始化为调用者的环境表,并将它的环境表初始化为NULL。(在当前实现中,用户数据环境表不能为NULL,但请参见下文。)(参见下面的代码片段 2。)

现在,可能给定的用户数据类型总是需要环境表(例如 Mike Pall 有用的队列示例,尽管即使是空队列也可能不需要环境表),但也有一些用例,其中环境表是可选的。例如,可以使用环境表来存储属于脚本环境而不是 C 实现的实例属性,甚至可以使用它来允许用户数据方法被 Lua 代码覆盖特定实例。(这意味着__index 元方法的实现会进行适当的查找。)

当前实现没有简单的方法来指定“没有环境表”。可以创建一个空表,但是如果在用户数据类型实例中很少使用环境表,这将非常浪费。我选择的解决方法是将用户数据环境表设置为元表(即创建用户数据的函数的环境表),并检查该条件。(参见下面的代码片段 3。)

因此,简而言之,用户数据环境表很有用且可用,但实现感觉很笨拙,因为它不符合(我所看到的)常见情况。

大约一年前,我提出了一种略有不同(也存在缺陷)的实现方案 [1]。我现在认为该方案存在缺陷,因为它试图以某种方式重用 lua_raw* 函数,就像我认为提出的 5.1 实现方案存在缺陷,因为它试图重用 lua_{g,s}etfenv 函数一样。事实是,userdata 和实例表之间的关联既不属于元表也不属于环境表,如果 API 不试图强加一个不存在的类比,它会更容易理解。

元数据有同伴,而不是环境

让我们将 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。此更改也很简单。

它们也有 C 同伴

现在,让我们考虑 userdata 的另一个方面。在许多情况下,userdata 只是封装的指针,但有时使用相同结构的两个版本会很有用:一个是封装的指针;另一个是未封装的结构。在不复制大量代码的情况下很难处理这种二元性,而且我认为这是不必要的。因此,以下提案试图解决这个问题。

到目前为止,关于Udata结构,我提出的只是重命名和一些不同的创建默认值。Udata结构本身并没有真正改变,因此它仍然受到添加环境表带来的对齐问题的影响。在 5.1 中,Udata 头部现在实际上是五个指针/长整型:next、flags、metatableenvsize。如果强制有效载荷进行双指针对齐,则会在头部引入填充。如果有效载荷没有强制进行双指针对齐,则几乎可以肯定它会是双指针未对齐的。(例如,在 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_ISBOXEDLUA_NEEDSGC。如果后一个标志关闭,那么垃圾回收将简单地删除对象,而不会尝试查找__gc元方法。


脚注

1. 用户数据本身包含特定于实例的信息,但环境表增加了将 Lua 对象与用户数据实例关联的可能性。

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) 在 userdata 的方法内部创建 userdata 的新实例

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

C) 从任意方法创建 userdata 的新实例

  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` 的环境表中查找,我们假设它与 `userdata` 的元表相同(或至少是类型方法可能存在的地方)。诀窍是 `userdata` 的环境表被设置为元表,以表明没有特定的环境表;这使我们能够节省查找。但是,正如在比较代码片段(如下)中看到的,我们可以通过对 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);
  }

比较代码片段

创建带盒子的 userdata

基于代码片段 2,示例 B 和 C

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

获取和设置字段

从代码片段 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 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())
如果我想允许 Lua 脚本执行此操作,那么我需要一个地方来存储重载的 doKeyPress 成员函数。我不能将其存储在标准元表中;那样会应用于所有实例。从逻辑上讲,我应该将其存储在小部件的环境表中,因为它是小部件实例的本地表。

当然,在常见情况下,不会覆盖任何方法。因此,我根本不需要环境表;我希望方法查找直接转到元表。如果我不能将环境表设置为 nil,那么我必须将其设置为某个哨兵,并在每次查找时对其进行测试。因此,我一直在寻找一些

a) 在语义上对应于(我)对环境表的预期使用。

b) 在常见操作中涉及更少的 API 调用。

因此,目标并不深刻。它只是反映了我的想法,即将用户数据的 env 表设置为当前正在运行函数的 env 表是一个极不可能的默认值,并且能够将其设置为 nil 是一个有用的功能。

欢迎评论


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2008 年 11 月 29 日下午 12:15 GMT (差异)