烦恼一般都是想太多了。

0%

PIL.31.C中的自定义用户定义类型

通过实现了一个 BitArray 来展示如何将 C 中的数据类型交给 Lua 来进行管理。基本的逻辑就是利用了 userdata 这么一个 Lua 的数据类型。

对于要实现的这个 Boolean 数组,先定义了以下几个宏:

#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))”
  • BITS_PER_WORD 是每个 usigned int 中的位数。
  • I_WORD 用来计算索引 i 处于哪一个 WORD。
  • I_BIT 计算出访问在 WORD 中元素的掩码。

将数组定义成如下:

typedef struct BitArray {
int size;
unsigned int value[1];
} BitArray;

这里 value 的大小是 1 ,而不是0,是因为 C89 不允许值为 0 的数组,我们会在实际代码中的分配这个尺寸,有 n 个元素的 BitArray 大小用以下表达式来计算:

sizeof(BitArray) + I_WORD(n-1)*sizeof(unsigned int)

之所以将 n 减去,是因为我们在定义 BitArray 的时候,就已经给他留出了一 1 个元素的位置。

UserData

在第一个版本,我们会显式的调用 set/get 来获取值,如:

a = array.new(1000)
for i=1, 1000 do
array.set(a, i, i % 2 == 0)
end

print(array.get(a, 10))
print(array.get(a, 11))
print(array.size(a))

在后面会改造成用面象对象的版本,如 a:get(i) 这样。

这几个操作的 C 代码如下:

static int newArray (lua_State *L){
int i;
size_t nbytes;
BitArray *a;
int n = (int)luaL_checkinteger(L, 1); /* 需要的 bit 数量 */
luaL_argcheck(L, n >= 1, 1, "invalid size");
nbytes = sizeof(BitArray) + I_WORD(n - 1)*sizeof(unsigned int);

a = (BitArray *)lua_newuserdata(L, nbytes);
a->size = n;
for(i = 0; i <= I_WORD(n - 1); i++)
a->value[i] = 0; /* 初始化 */
return 1;
}

static int setarray (lua_State *L) {
BitArray *a = (BitArray *)lua_touserdata(L, 1);
int index = (int)luaL_checkinteger(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 getarray (lua_State *L) {
BitArray *a = (BitArray *)lua_touserdata(L, 1);
int index = (int)luaL_checkinteger(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 中表示 C 类型, Lua 提供了一个叫做 userdata 的基础类型。一个 userdata 提供了一块内存区域, Lua 没有对其进行任何预定义的操作,因此我们可以用它来存储任何内容。

void *lua_newuserdata (lua_State *L, size_t size);

这个函数会分配一块 size 大小的内存,然后会将返回的 userdata 压到栈上,然后返回这个内存块的地址。如果因为某些原因我们需要通过其他途径来分配内存,我们可以创建一个指针大小的 userdata,然后用它来存在指向真正内存区域的指针。在 第32章 资源管理 有进行介绍。

我们的第一个函数 newarray() 使用 lua_newuserdata 来创建新数组。代码很直观,其检查我们数组大小(以 bit 来计量),计算出以 byte 为的大小,以这个合适的大小来创建一个 userdata,初始化内容,最后返回 userdata 到指针。

setarray 接收三个参数:数组本身,索引,新值。其假设索引是从 1 开始的,和 Lua 一样。因为 Lua 可以用任何值来表示 Boolean,所以我们用 luaL_checkany 来检查是否存在第三个参数就行了。如果我们的参数错误,那么就会得出一个比较明确的提示信息:

array.set(0, 11, 0)
--> stdin:1: bad argument #1 to 'set' ('array' expected)
array.set(a, 1)
--> stdin:1: bad argument #3 to 'set' (value expected)”

最后一个函数 getarray,与 setarray 类似,不细说。

我们也要定义一个函数来获取我们数组的大小:

static int getsize (lua_State *L) {
BitArray *a = (BitArray *)lua_touserdata(L, 1);
luaL_argcheck(L, a != NULL, 1, "'array' expected");
lua_pushinteger(L, a->size);
return 1;
}

接下来我们就需要把这些函数给注册到 Lua,我们利用了 luaL_newlib 函数:

static const struct luaL_Reg arraylib [] = {
{"new", newarray},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};

int luaopen_array (lua_State *L) {
luaL_newlib(L, arraylib);
return 1;
}

这样我们就可以用文首的那种形式来调用这些函数,构造对象了。

元表(Metatables)

我们当前的实现有一个很主要的危险。如果用户以写出了这样的代码:array.set(io.stdin, 1, false)io.stdin 是一个值是一个 userdata,其中存储一个指向流 File** 的指针。因为其是一个 userdata*,因此 array.set 会接受这个参数。可能的结果就是内存错误。Lua 库中这种行为是不可预知的。无论我们如何使用一个库,我们都不应该让 C 环境或者 Lua 系统给崩溃掉。

通常,用来区别一个 userdata 类型与另外一个 userdata 类型的方法是为其创建一个特定的 元表。每当我们创建一个 userdata 的时候,我们就为其指定一个对应的 元表;每当我们获取一个 userdata 的时候,我们都要检查其是否拥有对应的 元表。Lua 代码是无法修改 userdata元表 的,所以其无法欺骗这种检查。

我们也需要空间来存在这个新的 metatable,以便我们在创建 userdata 的时候能够访问它,同时用它来检查一个 userdata 是否有正确的类型。有两种可选的方法:1)存储在全局注册表;2)作为库函数中的 上值 存储。这个可以根据自己的需求来,为了将 C 类型注册到全局注册表,使用 类型名称 作为键,使用 metatable 作为值。需要注意的就是,键名一定不要和其他的相冲突。我们的例子中使用 LuaBook.array 来作为键。

Lua的辅助库提供了一些函数来帮助我们。

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),将其放在栈顶,以 tname 为索引存储在全局注册表内。
  • luaL_getmetatable 从全局注册表内用键 tname 获取 metatable。
  • luaL_checkudata 检查栈上的位置 index 是否是一个 userdata,同时其有一个名叫 tname 的元表。不是 userdata 或没有对应的元表都会产生一个错误,否则的话,其会返回 userdata 的地址。

现在开始我们的改造,第一步是改变打开库的函数,使其先创建 metatable:

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

下一步就是改造创建对象的函数:

static int newarray (lua_State *L) {
int i;
size_t nbytes;
BitArray *a;
int n = (int)luaL_checkinteger(L, 1); /* 需要的 bit 数量 */
luaL_argcheck(L, n >= 1, 1, "invalid size");
nbytes = sizeof(BitArray) + I_WORD(n - 1)*sizeof(unsigned int);

a = (BitArray *)lua_newuserdata(L, nbytes);
a->size = n;
for(i = 0; i <= I_WORD(n - 1); i++)
a->value[i] = 0; /* 初始化 */

luaL_getmetatable(L, "LuaBook.array");
lua_setmetatable(L, -2);

return 1; /* new userdata is already on the stack */
}

lua_setmetatable 会从栈顶弹出一个表,并此表设置为 对应索引处的元表。在我们的代码中,索引处的对象是 userdata

最后,setarray,getarray,getsize 必须检查他们收到的参数是否是一个有效的数组。我们简化任务,点定义了宏:

#define checkarray(L) \
(BitArray *)luaL_checkudata(L, 1, "LuaBook.array")

改造后的 getsize 看起来是这样:

static int getsize (lua_State *L) {
BitArray *a = checkarray(L);
lua_pushinteger(L, a->size);
return 1;
}

因为 setarray, getarray 都使用相同的代码来读取和检查其第二个参数,我们来构造一个新的辅助函数:

static unsigned int *getparams (lua_State *L,
unsigned int *mask) {
BitArray *a = checkarray(L);
int index = (int)luaL_checkinteger(L, 2) - 1;

luaL_argcheck(L, 0 <= index && index < a->size, 2,
"index out of range");

*mask = I_BIT(index); /* mask to access correct bit */
return &a->values[I_WORD(index)]; /* word address */
}
static int setarray (lua_State *L) {
unsigned int mask;
unsigned int *entry = getparams(L, &mask);
luaL_checkany(L, 3);
if (lua_toboolean(L, 3))
*entry |= mask;”
int index = (int)luaL_checkinteger(L, 2) - 1;

luaL_argcheck(L, 0 <= index && index < a->size, 2,
"index out of range");

*mask = I_BIT(index); /* mask to access correct bit */
return &a->values[I_WORD(index)]; /* word address */
}

static int setarray (lua_State *L) {
unsigned int mask;
unsigned int *entry = getparams(L, &mask);
luaL_checkany(L, 3);
if (lua_toboolean(L, 3))
*entry |= mask;
else
*entry &= ~mask;

return 0;
}

static int getarray (lua_State *L) {
unsigned int mask;
unsigned int *entry = getparams(L, &mask);
lua_pushboolean(L, *entry & mask);
return 1;
}

采用这种办法,之前说到的代码就不会被禁止执行:

a = array.get(io.stdin, 10)
--> bad argument #1 to 'get' (LuaBook.array expected, got FILE*)”

面向对象访问

我们的下一步动作是将我们新的类型转换为对象,然后我们就可以用面向对象的语法来操纵它了,如:

a = array.new(1000)
print(a:size()) --> 1000
a:set(10, true)
print(a:get(10)) --> true”

我们要记住一 的:a:size()a.size(a) 一致的,不过是个语法糖而已。因此,我们必须让表达式a.size 来返回我们的函数 getsize()。这里的关键在于 __index 元方法。对于表来说,如果其在表内找不到一个键的存在,那么就其就会调用这个元方法。对于 userdata,其总是会访问到,因为 userdata 是没有任何键的。

假设我们运行下面的代码:

do
local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size
end

首先,我们创建一个数组,目的只是为了获取它的元表,然后将其赋值给 metaarray。(我们不可以从 Lua 代码内设置 userdata 的元表,但是我们可以获取它)。接着我们设置 metaarray.__index = metaarray。当我们计算 a.size的时候,Lua 在 对象 a 内找不到键 size,因为对象是一个userdata。那么 Lua 就会尝试在 a 的元表中的 元方法 __index 获取值。然而,metaarray.size 是 array.size,所以 a.size(a) 会调用到 array.size(a),和我们的希望一致。

当然,我们可以在 C 中写类似的东西,甚至干得更好:现在这些数组是对象,有其自定义的操作哦,我们不再需要将这些操作放在表 array 内。我们的库只需要导出的只有一个函数 new,用来创建数组对象。所有其他的操作都只是以方法的形式提供。 C 代码可以直接这样进行注册。

操作 getsize, getarray, setarray 不需要做什么改变,改变的是我们怎么样注册他们。也就是说,我们需要改变我们打开这个库的代码。首先,我们需要两个独立的函数列表:一个针对常规的函数,一个针对对象的方法:

static const struct luaL_Reg arraylib_f [] = {
{"new", newarray},
{NULL, NULL}
};

static const struct luaL_Reg arraylib_m [] = {
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};

新版本的 luaopen_array 函数必须创建 元表,同时将其赋给自己的 __index 元方法,同时将所有的方法都注册到这个元表内,最后创建和填充 array 表:

int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array"); /* create metatable */
lua_pushvalue(L, -1); /* duplicate the metatable */
lua_setfield(L, -2, "__index"); /* mt.__index = mt */
luaL_setfuncs(L, arraylib_m, 0); /* register metamethods */
luaL_newlib(L, arraylib_f); /* create lib table */
return 1;
}

这里,我们再次使用了 luaL_setfunc 来将 arraylib_m 中的方法注册到元表。然后我们使用 luaL_newlib 来建立一个新表,同时将 arraylib_f 中的方法注册进去。

最后,我们为我们的类型添加一个 __tostring 方法,以便于让 print(a) 会打印出 array,加上其大小。

int array2string (lua_State *L) {
BitArray *a = checkarray(L);
lua_pushfstring(L, "array(%d)", a->size);
return 1;
}

数组访问

一个更好的选择是使用面向对象中的数组概念来访问我们的数组:用 a[i] 代替 a:get(i)。在我们的例子中 ,这实现起来很简单 ,我们我们的 getarry, setarray 本身就接受索引作为参数的。一个比较快的在 Lua 中进行实现的方法如下:

local metaarray = getmetatable(array.new(1))
metaarray.__index = array.get
metaarray.__newindex = array.set
metaarray.__len = array.size

注意了,这种形式,不是我们以面向对象的 C 代码注册的时候使用的,而时使用前一种形式的。

当我们以 a[1] 的形式进行访问的时候,实际上访问的是 array.get(a,1),所以就能很好的达到我们的目标。

当然,我们也可以在 C 里面做这个事情:

static const struct luaL_Reg arraylib_f [] = {
{"new", newarray},
{NULL, NULL}
};

static const struct luaL_Reg arraylib_m [] = {
{"__newindex", setarray},
{"__index", getarray},
{"__len", getsize},
{"__tostring", array2string},
{NULL, NULL}
};

int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array");
luaL_setfuncs(L, arraylib_m, 0);
luaL_newlib(L, arraylib_f);
return 1;
}

当然,我们如果不需要数组形式访问的话就不需要这样了。

LightUserdata

我们之前所说的 userdata 实际上指的是 full userdata。Lua 有另外一种 userdata,叫做 Light userdata。

一个 Light Userdata 实际上是一个代表 C 指针的值,一个 void * 值。一个 Light Userdata 是一个值,不是一个对象;我们不会创建它(就跟我们不会创建 number 一样)。

void lua_pushlightuserdata (lua_State *L, void *p);

用来将一个 light userdata 压到栈上。

尽管名字看起来相似,但是 full userdata 和 light userdata 是完全不同的东西。 light userdata 不是缓冲区,而是一个裸指针,其没有 元表。和 number 一样,他们不是归 Lua 的垃圾回收器管理的。

有的时候,人们可以会使用 light userdata 来当作 full userdata 的廉价版。然而,这并不是他的典型使用方法。首先 light userdata 没有元表,我们没有办法知道它的类型。然后,不要管名字,实际上 full userdata 开销并不大。与 malloc 相比,其只会增加一点点内存上的开销。

Light userdata 的真正用途要从其等于什么来看。一个 Full userdata 是一个对象,其只等于其本身。一个 Light userdata,相反,代表的是一个 C 指针值。因此其等于这个指针所代表内容。因此,我们可以使用 Light userdata 来在 Lua 中找到 C 对象。

Light 一个典型的应用场景是在 是让 Lua 对象像一个 C 对象的代理一样工作。具体点说, I/O 库 在 Lua 中使用 Light userdata 来表示 C 流对象。当我们的动作从 Lua 到 C 的时候,从 Lua 对象到 C 对象的映射是很容易的。每个 Lua 流都保持一个对其 C 流的指针。然而,当从 C 操作 Lua 的时候,就有点麻烦。举个例子,当我们在 I/O 系统中有些回调的时候,这些回调接收 C 流来知道他们要操作哪个流。但是这个时候,我们如何对应的 Lua 对象是什么?因为 C 流是由 C 标准库定义的,我们不能存储任何东西在里面(Lua 对象的引用)。

Light userdata 提供了一个很好的解决方案。我们建立一个表,其键就是 light userdata(存储了流的地址),值是 full userdata 表示了 Lua 中的流。在回调中 ,我们一旦拥有了流地址,我们将其当做一个 Light userdata 使用,从表中获取对应的 Lua 对象。(这个表应该是弱引用值,不然 Full userdata 永远不会被回收)。

总结

Full Userdata

  1. 对于我们自定义的类型,在 Lua 中可以用 Full Userdata 来表示。
  2. 但 Full Userdata 是没有类型的,我们想要指定其类型的话,就需要用一个特定的 元表 来进行标识。当我们对 C 函数的接收从 Lua 传递过来的参数进行类型验证的时候,这个就很有用了。

面向对象与 Lua 库

如果一个 Full Userdata 想以面向对象的那种形式来进行调用函数,我们有两种方法来进行设置,但事实上都是利用了一个事实: Full Userdata 的元表中的 __index 元方法来达成目的。

事实上,两种方法的区别就是:在什么地方设置对象的元表的差异。

在 Lua 中实现

将库函数全部导出。在 Lua 调用对应的函数,生成一个 Full Userdata 对象。如果对象在建立的时候已经设置了元表,我们直接在元表 __index 指向的表内设置对应的库函数即可。一般来说,我们都会将 元表的 __index 设置为元表自身,所以直接在元表内进行操作是可行的。因为我们是在 Lua 内设置元表对应到我们的库函数,所以导出所有的库函数这是必然需要的。

在 C 中实现

只导出新建对象的函数到 Lua。将其余所有的要作为方法的函数放到一个全局的表内,然后将所有的要用做方法的函数导出到这个全局表内。然后在新建对象的时候,就将此表设置为对象的元表。