Chapter 28: User-Defined Types in C
在前面的章节中我们已经看到如何通过编写新的C函数来扩展Lua。现在,我们将讨论如何通过编写C 代码来扩展Lua 以获得新类型。
我们的例子是一个相当简单的类型:布尔数组。当然,我们可以使用lua 的表来实现,但是每一个entry 用C 来实现只需要一个bit,这比表实现节省3% 的内存。
定义布尔数组类型
-- lualib.h
#define LUA_ARRAYLIBNAME "array"
int luaopen_array (lua_State *L);
-----------------------------------
-- linit.c
static const luaL_Reg lualibs[] = {
{"", luaopen_base},
{LUA_LOADLIBNAME, luaopen_package},
{LUA_TABLIBNAME, luaopen_table},
{LUA_IOLIBNAME, luaopen_io},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_DBLIBNAME, luaopen_debug},
{LUA_MATHLIBNAME, luaopen_my},
{LUA_MATHLIBNAME, luaopen_array}, /* add a item */
{NULL, NULL}
};
-----------------------------------
-- lmylib.c
#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 newarray (lua_State *L) {
int i, n;
size_t nbytes;
NumArray *a;
n = luaL_checkint(L, 1);
luaL_argcheck(L, n >= 1, 1, "invalid size");
nbytes = sizeof(NumArray) + I_WORD(n - 1)*sizeof(unsigned int);
a = (NumArray *)lua_newuserdata(L, nbytes);
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 */
}
static int setarray (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1);
int index = luaL_checkint(L, 2) - 1;
luaL_checkany(L, 3);
luaL_argcheck(L, a != NULL, 1, "'array' expected");
luaL_argcheck(L, 0 <= index && index < a->size, 2,
"index out of range");
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) {
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;
}
static int getsize (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1);
luaL_argcheck(L, a != NULL, 1, "'array' expected");
lua_pushinteger(L, a->size);
return 1;
}
static const struct luaL_Reg arraylib [] = {
{"new", newarray},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};
int luaopen_array (lua_State *L) {
luaL_register(L, LUA_ARRAYLIBNAME, arraylib);
return 1;
}
-----------------------------------
-- a.lua
a = array.new(1000)
print(a) --> userdata: 00471230
print(array.size(a)) --> 1000
for i=1,1000 do
array.set(a, i, i%5 == 0)
end
print(array.get(a, 10)) --> true
宏I_WORD 返回某个bit 对应的int 数组元素的索引(想像一下用int 数组表示bit 数组的情形,一个int 元素对应32 bit 元素)。I_BIT 计算一个掩码以在这个word 的内部访问正确的bit。
数组以这个结构来表示:
typedef struct NumArray {
int size;
unsigned int values[1]; /* variable part */
} NumArray;
size 为1 的数组values 只是用来占位,因为C89 标准不允许size 为0 的数组;当分配数组的时侯我们将会定义真实的size。 下面这个表达式对一个具有n个 元素的数组给出这个size :
sizeof(NumArray) + I_WORD(n - 1)*sizeof(unsigned int)
(不需要给I_WORD 添加一个,因为原始结构已经包含一个元素的空间)
28.1 Userdata
函数lua_newuserdata 为给定的size 分配一块内存,将相应的userdatum 压栈,并返回块地址:
void *lua_newuserdata (lua_State *L, size_t size);
newarray 使用lua_newuserdata 创建bool 数组
static int newarray (lua_State *L) {
int i, n;
size_t nbytes;
NumArray *a;
int tmp;
n = luaL_checkint(L, 1);
luaL_argcheck(L, n >= 1, 1, "invalid size");
nbytes = sizeof(NumArray) + I_WORD(n - 1)*sizeof(unsigned int);
a = (NumArray *)lua_newuserdata(L, nbytes);
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 */
}
(luaL_checkint 宏只是一个类型转换luaL_checkinteger)
创建新给组的语法是a=array.new(1000) ,往数组存一个元素用array.set(array,index,value)。稍后我们将看到如何使用元表来支持传统语法:array[index]=value。
static int setarray (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1);
int index = luaL_checkint(L, 2) - 1;
luaL_checkany(L, 3);
luaL_argcheck(L, a != NULL, 1, "'array' expected");
luaL_argcheck(L, 0 <= index && index < a->size, 2,
"index out of range");
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 接受任意值作为一个boolean,我们使用luaL_checkany 来检查第三个参数。它只保证这里有一个值(任意值)。
28.2 Metatables
我们的实现有一个主要的漏洞。假设用户写类似这样的代码array.set(io.stdin,1,false)。io.stdin 是一个userdatum,一个指向流的指针(FILE*)。因为是一个userdatum,array.set 高兴地接受它作为有效参数;造成的结果可能是内存corruption。
区分一个userdata 类型和另一个userdata 类型的不同通常是为那个类型创建一个唯一元表。我们每次创建一个userdata 都使用相应的元表来标记。而每次获得一个userdata 我们检查它是否拥有正确的元表。因为Lua 代码不能更改一个userdatum 的元表,它不能欺骗我们的代码。
有三个地方可以存储元表:registry,environment,或upvalue。在registry 中注册一个新的C 类型习惯是用类型名作为索引,元表作为值。
auxiliary 库有几个辅助函数:
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 创建一个新表(被用作元表)放在栈顶,并在registry 中将这个表关联到一个给定的名字。
luaL_getmetatable 从registry获取与tname 相关联的元表。最后,luaL_checkudata 检查栈中给定位置上的一个对象是否是一个带有正确元表名字的userdatum。如果没有正确的元表或不是一个userdata 就引发错误,否则返回userdata 的地址。
现在开始实现。第一步是改变luaopen_array。新版本必须为arrays 创建元表:
int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array");
luaL_register(L, "array", arraylib);
return 1;
}
static int newarray (lua_State *L) {
int i, n;
size_t nbytes;
NumArray *a;
int tmp;
n = luaL_checkint(L, 1);
luaL_argcheck(L, n >= 1, 1, "invalid size");
nbytes = sizeof(NumArray) + I_WORD(n - 1)*sizeof(unsigned int);
a = (NumArray *)lua_newuserdata(L, nbytes);
a->size = n;
for (i=0; i <= I_WORD(n-1); i++)
a->values[i] = 0; /* initialize array */
luaL_getmetatable(L, "LuaBook.array");
lua_setmetatable(L, -2);
return 1; /* new userdatum is already on the stack */
}
…
…
…
(完整代码见后面)
现在,这个语句array.get(io.stdin,10) 会得到这样的错误:
error: bad argument #1 to 'getarray' ('array' expected)
28.3 Object-Oriented Access
-- lualib.h
#define LUA_ARRAYLIBNAME "array"
int luaopen_array (lua_State *L);
-----------------------------------
-- linit.c
static const luaL_Reg lualibs[] = {
{"", luaopen_base},
{LUA_LOADLIBNAME, luaopen_package},
{LUA_TABLIBNAME, luaopen_table},
{LUA_IOLIBNAME, luaopen_io},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_DBLIBNAME, luaopen_debug},
{LUA_MATHLIBNAME, luaopen_my}, /* add a item */
{LUA_MATHLIBNAME, luaopen_array}, /* add a item */
{NULL, NULL}
};
-----------------------------------
-- larraylib.c
#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;
#define checkarray(L) /
(NumArray *)luaL_checkudata(L, 1, "LuaBook.array")
unsigned int *getindex (lua_State *L,unsigned int *mask);
static int newarray (lua_State *L) {
int i, n;
size_t nbytes;
NumArray *a;
int tmp;
n = luaL_checkint(L, 1);
luaL_argcheck(L, n >= 1, 1, "invalid size");
tmp = I_WORD(n - 1);
nbytes = sizeof(NumArray) + I_WORD(n - 1)*sizeof(unsigned int);
a = (NumArray *)lua_newuserdata(L, nbytes);
a->size = n;
for (i=0; i <= I_WORD(n-1); i++)
a->values[i] = 0; /* initialize array */
luaL_getmetatable(L, "LuaBook.array");
lua_setmetatable(L, -2);
return 1; /* new userdatum is already on the stack */
}
static int setarray (lua_State *L) {
unsigned int mask;
unsigned int *entry = getindex(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 = getindex(L, &mask);
lua_pushboolean(L, *entry & mask);
return 1;
}
static int getsize (lua_State *L) {
NumArray *a = checkarray(L);
luaL_argcheck(L, a != NULL, 1, "'array' expected");
lua_pushinteger(L, a->size);
return 1;
}
static unsigned int *getindex (lua_State *L,unsigned int *mask) {
NumArray *a = checkarray(L);
int index = luaL_checkint(L, 2) - 1;
luaL_argcheck(L, 0 <= index && index < a->size, 2,
"index out of range");
/* return element address */
*mask = I_BIT(index);
return &a->values[I_WORD(index)];
}
int array2string (lua_State *L) {
NumArray *a = checkarray(L);
lua_pushfstring(L, "array(%d)", a->size);
return 1;
}
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_register(L, NULL, arraylib_m);
luaL_register(L, "array", arraylib_f);
return 1;
}
-----------------------------------
-- a.lua
a = array.new(1000)
a[10] = true -- setarray
print(a[10]) -- getarray --> true
print(#a) -- getsize --> 1000
28.5 Light Userdata
到目前为到我们看到的userdata 被称为full userdata。Lua 还支持另一种userdata,称为light userdata。
Light userdatum 是一个表示C 指针的值(就是void* 的值)。要将一个light userdattum 入栈,调用lua_pushlightuserdata:
void lua_pushlightuserdata (lua_State *L, void *p);
light userdata 不是buffers,而是一个指针。它们没有元表。像数值一样不被垃圾收集器所管理。