第一版是针对 Lua 5.0 编写的。虽然在很大程度上仍然适用于后续版本,但有一些差异。
第四版针对 Lua 5.3,可在 Amazon 和其他书店购买。
购买这本书,您还可以帮助支持 Lua 项目


28.2 – 元表

我们当前的实现存在一个重大的安全漏洞。假设用户编写类似于 array.set(io.stdin, 1, 0) 的内容。io.stdin 中的值是一个用户数据,其中包含指向流(FILE*)的指针。因为它是一个用户数据,所以 array.set 会很乐意接受它作为有效参数;可能的结果是内存损坏(如果幸运的话,您可能会收到索引超出范围错误)。对于任何 Lua 库来说,这种行为都是不可接受的。无论您如何使用 C 库,它都不应损坏 C 数据或从 Lua 产生核心转储。

为了将数组与其他用户数据区分开来,我们为其创建了一个唯一的元表。(请记住,用户数据也可以有元表。)然后,每次创建数组时,我们都会用此元表标记它;每次获取数组时,我们都会检查它是否具有正确的元表。由于 Lua 代码无法更改用户数据的元表,因此它无法伪造我们的代码。

我们还需要一个地方来存储此新元表,以便我们可以访问它来创建新数组并检查给定的用户数据是否为数组。正如我们之前看到的,有两种常见的存储元表的方法:在注册表中或作为库中函数的上值。在 Lua 中,通常的做法是使用类型名称作为索引,将任何新 C 类型注册到注册表中,并将元表作为值。与任何其他注册表索引一样,我们必须谨慎选择类型名称,以避免冲突。我们将把这种新类型称为 "LuaBook.array"

与往常一样,辅助库提供了一些函数来帮助我们。我们将使用的新的辅助函数是

    int   luaL_newmetatable (lua_State *L, const char *tname);
    void  luaL_getmetatable (lua_State *L, const char *tname);
    void *luaL_checkudata (lua_State *L, int index,
                                         const char *tname);
luaL_newmetatable 函数创建一个新表(用作元表),将新表保留在堆栈顶部,并将表和注册表中的给定名称关联起来。它执行双重关联:它使用名称作为表的键,使用表作为名称的键。(这种双重关联允许为其他两个函数实现更快的实现。)luaL_getmetatable 函数从注册表中检索与 tname 关联的元表。最后,luaL_checkudata 检查给定堆栈位置的对象是否是具有与给定名称匹配的元表的 userdatum。如果对象没有正确的元表(或不是用户数据),则返回 NULL;否则,返回用户数据地址。

现在我们可以开始实现。第一步是更改打开库的函数。新版本必须创建一个表,用作数组的元表

    int luaopen_array (lua_State *L) {
      luaL_newmetatable(L, "LuaBook.array");
      luaL_openlib(L, "array", arraylib, 0);
      return 1;
    }

下一步是更改 newarray,以便它在创建的所有数组中设置此元表

    static int newarray (lua_State *L) {
      int n = luaL_checkint(L, 1);
      size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
      NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
    
      luaL_getmetatable(L, "LuaBook.array");
      lua_setmetatable(L, -2);
    
      a->size = n;
      return 1;  /* new userdatum is already on the stack */
    }
lua_setmetatable 函数从堆栈中弹出表,并将其设置为给定索引处对象的元表。在我们的示例中,此对象是新的用户数据。

最后,setarraygetarraygetsize 必须检查它们是否将有效数组作为其第一个参数。由于我们希望在参数错误的情况下引发错误,因此我们定义以下辅助函数

    static NumArray *checkarray (lua_State *L) {
      void *ud = luaL_checkudata(L, 1, "LuaBook.array");
      luaL_argcheck(L, ud != NULL, 1, "`array' expected");
      return (NumArray *)ud;
    }
使用 checkarraygetsize 的新定义非常简单
    static int getsize (lua_State *L) {
      NumArray *a = checkarray(L);
      lua_pushnumber(L, a->size);
      return 1;
    }

由于 setarraygetarray 还共享代码以检查其第二个参数作为其索引,因此我们在以下函数中分解了它们的公共部分

    static double *getelem (lua_State *L) {
      NumArray *a = checkarray(L);
      int index = luaL_checkint(L, 2);
    
      luaL_argcheck(L, 1 <= index && index <= a->size, 2,
                       "index out of range");
    
      /* return element address */
      return &a->values[index - 1];
    }
在定义 getelem 之后,setarraygetarray 非常简单
    static int setarray (lua_State *L) {
      double newvalue = luaL_checknumber(L, 3);
      *getelem(L) = newvalue;
      return 0;
    }
    
    static int getarray (lua_State *L) {
      lua_pushnumber(L, *getelem(L));
      return 1;
    }
现在,如果您尝试类似于 array.get(io.stdin, 10) 的操作,您将收到适当的错误消息
    error: bad argument #1 to `getarray' (`array' expected)