第 28 章。这章主要是讲怎样在 lua 中使用 c 语言中自定义的数据结构。
先看一个简单的例子:boolean 数组。在 lua 中可以使用 table 来存储,在 c 语言中由于每个变量只占一个 bit,使用的内存只占用 table 实现的 3%。
程序使用如下的数据结构和相关的宏定义:
#include <limits.h>
#define BITS_PER_WORD (CHAR_BIT*sizeof(unsigned int))
#define I_WORD(i) ((unsigned int)(i) / BITS_PER_WORD)
#define I_BIT(i) (1 << ((unsigned int)(i) % BITS_PER_WORD))
typedef struct NumArray {
int size;
unsigned int values[1]; /* variable part */
} NumArray;
第 0 个元素是 values[0] 的最高位。I_WORD 表示对于给出的 boolean 数组索引,对应于该索引的 bit 位于 values 数组的哪个整数中,I_BIT 计算该索引位于所在整数位置的掩码。我们为数组按需分配连续的空间:
NumArray* na;
na = malloc(sizeof(NumArray) + I_WORD(n - 1) * sizeof(unsigned int));
这样就可以访问 values[n] 而不会造成数组越界。
在 lua 中表示自定义结构
lua 为用户自定义的数据结构提供了一个基本的类型:userdata。这个类型在 lua 中只是一段供用户使用的内存,没有定义任何相关的操作。函数
void *lua_newuserdata (lua_State *L, size_t size);
分配一段长为 size 的内存并且返回起始地址,起始地址已经被压入栈。例如我们可以用下面的函数分配一个 boolean 数组:
static int l_newarray(lua_State* l)
{
int i, n;
NumArray* a;
n = luaL_checkint(l, 1);
luaL_argcheck(l, n >= 1, 1, "invalid size");
a = lua_newuserdata(l, sizeof(NumArray) + I_WORD(n - 1) * sizeof(unsigned int));
a->size = n;
for (i = 0; i <= I_WORD(n - 1); ++i)
a->values[i] = 0; /* initialize array */
return 1; /* new userdatum is already on the stack */
}
这个函数接收一个参数,表示数组中 32 bit 整数的个数。把这个函数注册到 lua 环境后就可以使用了,例如绑定到变量 new:
arr = new(1000)
来分配一个有 1000 个整数的数组了。为了给数组元素赋值,我们还需要另外一个函数调用 set(arr, index, value):
static int l_setarray(lua_State* l)
{
NumArray* a = (NumArray*)lua_touserdata(l, 1);
int index = luaL_checkint(l, 2) - 1;
luaL_argcheck(l, a != NULL, 1, "'array' expected");
luaL_argcheck(l, 0 <= index && index < a->size, 2, "index out of range");
luaL_checkany(l, 3);
if (lua_toboolean(l, 3))
a->values[I_WORD(index)] |= I_BIT(index); /* set bit */
else
a->values[I_WORD(index)] &= ~I_BIT(index); /* reset bit */
return 0;
}
因为 lua 可以使用任何值作为 bool 类型,所以使用 luaL_checkany() 来强制要求第三个参数。如果传递了错误的参数会得到错误提示。
还有一个获取某个索引元素值的函数 get(arr, index):
static int l_getarray(lua_State* l)
{
NumArray* a = (NumArray*)lua_touserdata(l, 1);
int index = luaL_checkint(l, 2) - 1;
luaL_argcheck(l, a != NULL, 1, "'array' expected");
luaL_argcheck(l, 0 <= index && index < a->size, 2, "index out of range");
lua_pushboolean(l, a->values[I_WORD(index)] & I_BIT(index));
return 1;
}
最后把这些函数注册到全局变量 array 中:
static const struct luaL_Reg arraylib[] = {
{"new", l_newarray},
{"set", l_setarray},
{"get", l_getarray},
{NULL, NULL}
};
int lua_openarraylib(lua_State* l)
{
lua_newtable(l);
luaL_setfuncs(l, arraylib, 0);
lua_setglobal(l, "array");
return 1;
}
int main(void)
{
lua_State* l;
l = luaL_newstate();
luaL_openlibs(l);
lua_openarraylib(l);
if (luaL_loadfile(l, "./newarray.lua") != 0) {
fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l, -1));
goto end;
}
if (lua_pcall(l, 0, 0, 0) != 0)
fprintf(stderr, "lua_pcall err: %s.\n", lua_tostring(l, -1));
end:
lua_close(l);
return 0;
}
在 lua 脚本中就可以使用这些函数了:
arr = array.new(1000)
array.set(arr, 10, nil)
print (array.get(arr, 10))
metatables
上面的实现存在安全漏洞,例如我们调用 array.set(io.stdin, 1, false) 的语法是合法的(因为 ip.stdin 是一个 FILE* 类型的指针),但是可能会造成程序崩溃,因为这个指针指向的类型并不是 set() 函数需要的类型,而 set() 函数并没有对传递的类型进行检查。
解决问题的一种方法是为每个类型创建一个 metatable。每当创建一个新的类型时建立对应的 metatable;传递参数的时候检查一下该参数的类型是否有对应的 metatable,如果没有就是非法的类型。由于在 lua 脚本中不能更改 metatable 和类型的对应关系,因此这个检测能够检测出不匹配的类型。
lua 提供了以下 c 函数来操作 metatable:
int luaL_newmetatable (lua_State *L, const char *tname);
void luaL_getmetatable (lua_State *L, const char *tname);
void luaL_setmetatable (lua_State *L, const char *tname);
void *luaL_checkudata (lua_State *L, int index, const char *tname);
metatable 保存在 registry 中,通过字符串名字作为索引,因此在创建 metatable 时要注意名字之间的冲突。luaL_getmetatable() 创建一个新的 metatable 并且关联到 tname,新创建的 metatable 位于栈顶;如果已经存在相同的名字则返回 0,否则返回 1。luaL_getmetatable() 根据给定的名字找到对应的 metatable 并且把结果放到栈顶。luaL_setmetatable() 把栈顶的元素设为和 tname 关联的 metatable。lua_setluaL_checkudata() 检查给定的 index 位置的 userdata 是否是 tname 关联的类型,如果结果匹配函数返回 userdata 的地址,否则产生一个 error 并终止程序。
在这章中我们使用“LuaBook.array”作为 array 对应的 metatable 的名称。下面使用 metatable 代替上面例子中的普通 table:
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <limits.h>
#define BITS_PER_WORD (CHAR_BIT*sizeof(unsigned int))
#define I_WORD(i) ((unsigned int)(i) / BITS_PER_WORD)
#define I_BIT(i) (1 << ((unsigned int)(i) % BITS_PER_WORD))
typedef struct NumArray {
int size;
unsigned int values[1]; /* variable part */
} NumArray;
static int l_newarray(lua_State* l)
{
int i, n;
NumArray* a;
n = luaL_checkint(l, 1);
luaL_argcheck(l, n >= 1, 1, "invalid size");
a = lua_newuserdata(l, sizeof(NumArray) + I_WORD(n - 1) * sizeof(unsigned int));
a->size = n;
for (i = 0; i <= I_WORD(n - 1); ++i)
a->values[i] = 0; /* initialize array */
luaL_setmetatable(l, "LuaBook.array");
return 1; /* new userdatum is already on the stack */
}
static int l_setarray(lua_State* l)
{
NumArray* a = (NumArray*)luaL_checkudata(l, 1, "LuaBook.array");
int index = luaL_checkint(l, 2) - 1;
luaL_argcheck(l, a != NULL, 1, "'array' expected");
luaL_argcheck(l, 0 <= index && index < a->size, 2, "index out of range");
luaL_checkany(l, 3);
if (lua_toboolean(l, 3))
a->values[I_WORD(index)] |= I_BIT(index); /* set bit */
else
a->values[I_WORD(index)] &= ~I_BIT(index); /* reset bit */
return 0;
}
static int l_getarray(lua_State* l)
{
NumArray* a = (NumArray*)luaL_checkudata(l, 1, "LuaBook.array");
int index = luaL_checkint(l, 2) - 1;
luaL_argcheck(l, a != NULL, 1, "'array' expected");
luaL_argcheck(l, 0 <= index && index < a->size, 2, "index out of range");
lua_pushboolean(l, a->values[I_WORD(index)] & I_BIT(index));
return 1;
}
static int l_getsize(lua_State* l)
{
NumArray* a = (NumArray*)luaL_checkudata(l, 1, "LuaBook.array");
lua_pushinteger(l, a->size);
return 1;
}
static const struct luaL_Reg arraylib[] = {
{"new", l_newarray},
{"set", l_setarray},
{"get", l_getarray},
{"size", l_getsize},
{NULL, NULL}
};
int lua_openarraylib(lua_State* l)
{
luaL_newmetatable(l, "LuaBook.array");
luaL_setfuncs(l, arraylib, 0);
lua_setglobal(l, "array");
return 1;
}
int main(void)
{
lua_State* l;
l = luaL_newstate();
luaL_openlibs(l);
lua_openarraylib(l);
if (luaL_loadfile(l, "./metatable.lua") != 0) {
fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l, -1));
goto end;
}
if (lua_pcall(l, 0, 0, 0) != 0)
fprintf(stderr, "lua_pcall err: %s.\n", lua_tostring(l, -1));
end:
lua_close(l);
return 0;
}
程序有几处改动的地方:在 lua_openarraylib() 中为自定义的 array 类型创建了一个对应的 metatable,名字为“LuaBook.array”;l_newarray() 中初始化新的 array 变量后把它和“LuaBook.array”关联起来(第 30 行);l_setarray() 和 l_getarray() 获取第一个参数的时候使用了 luaL_checkudata() 来检查参数类型是否正确。
面向对象式的行为
下一步就是把类型实例化,以便使用面向对象的语法操作对象,像下面这样:
a = array.new(1000)
print(a:size()) --> 1000
a:set(10, true)
print(a:get(10)) --> true
前面说过,userdata 只是一段内存,lua 并没有预定义任何操作,因此我们要为自定义的类型注册相应的操作函数,这个步骤可以通过 lua 代码实现:
local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size
这里的重点是 __index
这个方法。对于 table 类型(metatable 也是 table 的一种)来说,当找不到需要调用的函数或要访问的域时,__index
就会被调用。第一行通过 getmetatable() 获取 userdata 对应的 metatable,然后把 metatable 的 __index
指向本身,并且把 array 的几个操作分别赋值给 metatable 对应的域。这样,当尝试调用“a:size()”的时候,由于找不到 size 这个域,a 对应的 metatable 中的 __index
就会被调用(这个 metatable 的 __index
指向本身),并且从 metatable 中找到 size 这个域(已经指向了 array.size),因此能正确返回数组的大小,这个调用等效于array.size(a)
。
绑定方法的操作可以在 c 语言中进行。可以看到 array 中除了 new 操作需要保留之外,其它操作都不需要,因此我们修改一下注册 userdata 的方法:
static const struct luaL_Reg arraylib_f[] = {
{"new", l_newarray},
{NULL, NULL}
};
static const struct luaL_Reg arraylib_m[] = {
{"set", l_setarray},
{"get", l_getarray},
{"size", l_getsize},
{NULL, NULL}
};
int lua_openarraylib(lua_State* l)
{
luaL_newmetatable(l, "LuaBook.array");
/* metatable.__index = metatable */
lua_pushvalue(l, -1); /* duplicates the metatable */
lua_setfield(l, -2, "__index");
luaL_setfuncs(l, arraylib_m, 0);
lua_newtable(l);
luaL_setfuncs(l, arraylib_f, 0);
lua_setglobal(l, "array");
return 1;
}
在 lua_openarraylib() 中先把 metatable 的 __index
指向本身,然后把 set,get 和 size 操作注册到 metatable 中。另外新建一个 table,只把 new 操作注册给 array 类型。
数组访问操作
除了使用“arr.get(i)”来访问数组元素以外,还可以使用“a[i]”这样的语法,只是修改一下注册函数的方式(metatable 的各个域在第 13 章有详细介绍):
local metaarray = getmetatable(array.new(1))
metaarray.__index = array.get
metaarray.__newindex = array.set
metaarray.__len = array.size
这些步骤也可以在 c 语言中进行。然后就可以使用下面的写法来操作自定义的 array 类:
a = array.new(1000)
a[10] = true -- setarray
print(a[10]) -- getarray --> true
print(#a) -- getsize --> 1000
light userdata
前面介绍的 userdata 属于 full userdata,除此之外还有另外一种 userdata 叫 light userdata。一个 light userdata 仅仅是一个 c 指针(void*)。因为它是一个值而不是类型,因此没有实例化操作(就像数值常量一样)。我们使用
void lua_pushlightuserdata (lua_State *L, void *p);
把一个 light userdata 放到栈里。
虽然名字一样,但这两者是不同的概念。light userdata 没有分配空间,仅仅是一个指针(指针指向的内存不属于 lua 管理的部分),它没有对应的 metatable。就像数值常量一样,light userdata 并不由 lua 的 garbage collector 管理,light userdata 指向的内存由用户自己管理。一般来说,使用 full userdata 的开销并不比 light userdata 大多少。
light userdata 一般用在相等判断上。由于 full userdata 是一种类型,使用 ==
的时候只有和本身比较结果才是 true,而 light userdata 则是和任何指向该指针的类型比较结果都是 true。一个典型的应用是在 gui 窗口系统中。我们使用 full userdata 代表每个窗口系统,每个元素可能是整个窗口结构,也可能是一个指向由该系统创建的窗口的指针。当一个事件到达时(例如一个鼠标点击事件),系统根据鼠标的点击位置调用特定的回调函数。为了通知 lua 到底应该让哪个窗口处理这个事件,我们在 lua 中维护一个 light userdata table,light userdata 的值就是窗口的内存地址,这样就能根据地址找到对应的窗口。