28.2 Metatables

我们上面的实现有一个很大的安全漏洞。假如使用者写了如下类似的代码:array.set(io.stdin, 1, 0)io.stdin 中的值是一个带有指向流(FILE*)的指针的userdatum。因为它是一个userdatum,所以array.set很乐意接受它作为参数,程序运行的结果可能导致内存core dump(如果你够幸运的话,你可能得到一个访问越界(index-out-of-range)错误)。这样的错误对于任何一个Lua库来说都是不能忍受的。不论你如何使用一个C库,都不应该破坏C数据或者从Lua产生core dump

为了区分数组和其他的userdata,我们单独为数组创建了一个metatable(记住userdata也可以拥有metatables)。下面,我们每次创建一个新的数组的时候,我们将这个单独的metatable标记为数组的metatable。每次我们访问数组的时候,我们都要检查他是否有一个正确的metatable。因为Lua代码不能改变userdatummetatable,所以他不会伪造我们的代码。

我们还需要一个地方来保存这个新的metatable,这样我们才能够当创建新数组和检查一个给定的userdatum是否是一个数组的时候,可以访问这个metatable。正如我们前面介绍过的,有两种方法可以保存metatable:在registry中,或者在库中作为函数的upvalue。在Lua中一般习惯于在registry中注册新的C类型,使用类型名作为索引,metatable作为值。和其他的registry中的索引一样,我们必须选择一个唯一的类型名,避免冲突。我们将这个新的类型称为 "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函数创建一个新表(将用作metatable),将新表放到栈顶并建立表和registry中类型名的联系。这个关联是双向的:使用类型名作为表的key;同时使用表作为类型名的key(这种双向的关联,使得其他的两个函数的实现效率更高)。luaL_getmetatable函数获取registry中的tname对应的metatable。最后,luaL_checkudata检查在栈中指定位置的对象是否为带有给定名字的metatableusertatum。如果对象不存在正确的metatable,返回NULL(或者它不是一个userdata);否则,返回userdata的地址。

下面来看具体的实现。第一步修改打开库的函数,新版本必须创建一个用作数组metatable的表:

int luaopen_array (lua_State *L) {

    luaL_newmetatable(L, "LuaBook.array");

    luaL_openlib(L, "array", arraylib, 0);

    return 1;

}

第二步,修改newarray,使得在创建数组的时候设置数组的metatable

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函数将表出栈,并将其设置为给定位置的对象的metatable。在我们的例子中,这个对象就是新的userdatum

最后一步,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;

}

使用checkarray,新定义的getsize是更直观、更清楚:

static int getsize (lua_State *L) {

    NumArray *a = checkarray(L);

    lua_pushnumber(L, a->size);

    return 1;

}

由于setarraygetarray检查第二个参数index的代码相同,我们抽象出他们的共同部分,在一个单独的函数中完成:

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)


相关链接:
lua程序设计目录 - 中国lua开发者 - lua论坛