programming in lua (10)

第 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 的值就是窗口的内存地址,这样就能根据地址找到对应的窗口。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注