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代码不能改变userdatum的metatable,所以他不会伪造我们的代码。
我们还需要一个地方来保存这个新的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检查在栈中指定位置的对象是否为带有给定名字的metatable的usertatum。如果对象不存在正确的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。
最后一步,setarray、getarray和getsize检查他们的第一个参数是否是一个有效的数组。因为我们打算在参数错误的情况下抛出一个错误信息,我们定义了下面的辅助函数:
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;
}
由于setarray和getarray检查第二个参数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,函数setarray和getarray更加直观易懂:
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)