本节是对lua中的userdata的一个小小总结,其示例参考自《Lua程序设计》(第四版)
问题:在lua中使用布尔数组。
虽然lua中可以使用表(table)来实现布尔数组,但是其空间利用率极低。
因此,可以使用lua中的自定义类型实现。
1.位操作
首先是和位相关的操作,如下所示:
//对于x,获取第y位的值
#define getbit(x,y) (((x)>>(y)&1)
//对于x,把第y位置1。先把1左移y位,然后进行位或操作
#define setbit(x,y) x |= (1<<y)
//对于x,把第y位置0。先把1左移y位,然后取反,与x做位与操作
#define clrbit(x,y) x&=~(1<<y)
以上三种是在本示例中会用到的宏函数(宏替换)
2.内存对齐
这里会提到内存对齐的原因是和布尔数组的创建长度有关。内存对齐的文章有很多,这里不再赘述。
3.元表(metatable)和元方法(metamethod)
元表可以修改一个值在面对一个未知操作时的行为。例如,假设a和b都是表,那么可以通过元表定义lua语言如何计算表达式a+b,当lua语言视图将两个表相加时,它会先检查两者之一是否有元表企鹅该元表中是否有__add字段。如果找到了该字段,就调用该字段的值。(摘自《lua程序设计》第四版)。元方法类似于c++中的运算符重载,使用元表和元方法可以重新定义一些常见的行为。比如
- __index 当访问一个表中不存在的字段时,解释器会首先查找一个名为__index的元方法,如果没有这个元方法,则会返回nil,否则,则由这个元方法来提供最终的结果。
- __newindex 类似于__index,不同则在于__newindex用于表的更新而__index用于表的查询。当对一个表中不存在的索引赋值时,解释器会查找__newindex元方法,如果这个存在,那么解释器就调用它而不执行赋值。
- __len lua中求表长度的语法是在表的前面加上#,比如表名为set,那么set的长度为#set,当解释器发现#set时,会查找__len的元方法,如果有,则调用该元方法并返回值。
4.userdata
最后则是本节的重头戏。
首先,需要构建一个结构体,用于保存布尔数组:
typedef struct BitArray
{
int size;
unsigned int values[1]; //可变部分
} BitArray;
在BitArray中,values为可变部分,详细内容可以参考这个帖子。简单的说,这种空数组的形式,可以理解为指针,但是要比指针更加的方便和安全。
对于32位的机器来说,根据默认的内存对齐,那么BitArray的总字节数为8个字节,也就是说,values一开始就有4个字节的空间(这个需要注意)。
然后是声明一些宏:
#define CHAR_BIT 8
//无符号整型的位数
#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))
#define checkarray(pL) (BitArray*)luaL_checkudata(pL, 1, "LuaBook.array")
BITS_PER_WORD表示unsigned int所占的位数;I_WORD表示对于索引i来说,它在哪一个无符号整型上,而I_BIT则对应了一个掩码,通过它可以方便地取出或更改索引i所对应的值。而checkarray则相对来说比较复杂,它的主要功能就是用于标识本节所实现的布尔数组,必须要有一个名为LuaBook.array的元表。
static int newarray(lua_State* pL)
{
int i;
size_t nbytes;
BitArray* array;
//比特位的个数
int n = (int)luaL_checkinteger(pL, 1);
luaL_argcheck(pL, n >= 1, 1, "invalid size");
//和内存对齐有关 BitArray 12字节 vlaues 之后默认有4个字节
nbytes = sizeof(BitArray) + I_WORD(n - 1) * sizeof(unsigned int);
array = lua_newuserdata(pL, nbytes);
//设置元表
luaL_getmetatable(pL, "LuaBook.array");
lua_setmetatable(pL, -2);
//初始化
array->size = n;
for (i = 0; i < I_WORD(n - 1); i++)
array->values[i] = 0;
return 1;
}
首先是创建布尔数组,该方法需要一个值,该值用于标识布尔数组的长度,newarray函数会根据这个长度调用lua_newusedata()来分配内存。需要注意的是,分配的布尔数组的内存可能比正常用到的要小一个字节,不过由于内存对齐的缘故,而多了4B,所以综合来看,上面分配内存会多3B或4B。
lua_newuserdata会返回一个开辟好的内存空间,并把该空间同时压入栈顶。后面的程序为这个userdata分配了一个名为LuaBook.array的元表。最后则是对布尔数组进行初始化为全0。
static unsigned int *getparams(lua_State* pL, unsigned int *mask)
{
BitArray* array = checkarray(pL);
int index = (int)luaL_checkinteger(pL, 2) - 1;
luaL_argcheck(pL, 0 <= index && index < array->size, 2, "index out of range");
*mask = I_BIT(index);
return &array->values[I_WORD(index)];
}
getparams函数,用于从lua_State中获取到合法的BitArray,并返回索引index所对应的unsigned int(大小为4B的内存块)和掩码mask。
static int setarray(lua_State* pL)
{
unsigned int mask;
unsigned int *entry = getparams(pL, &mask);
luaL_checkany(pL, 3);
if (lua_toboolean(pL, 3))
*entry |= mask;
else
*entry &= ~mask;
return 0;
}
该函数会根据第三个参数的值,来觉得对索引为index,是置一操作还是清零操作。
static int getarray(lua_State* pL)
{
unsigned int mask;
unsigned int *entry = getparams(pL, &mask);
lua_pushboolean(pL, *entry & mask);
return 1;
}
该函数用于获取index对应的位的值。
static int getsize(lua_State* pL)
{
BitArray* array = checkarray(pL);
luaL_argcheck(pL, array != NULL, 1, "'array' expected");
lua_pushinteger(pL, array->size);
return 1;
}
getsize函数用于获取布尔数组的长度,并返回。
static const struct luaL_Reg arraylib_m[] =
{
{"set", setarray},
{"get", getarray},
{"size", getsize},
{"__tostring", array2string},
{"__newindex", setarray},
{"__index", getarray},
{"__len", getsize},
{NULL, NULL}
};
static const struct luaL_Reg arraylib_f[] =
{
{"new", newarray},
{NULL, NULL}
};
这两个变量的作用下面会提到。
int lua_openarray(lua_State* pL)
{
//创建元表
luaL_newmetatable(pL, "LuaBook.array");
//复制元表
/*
lua_pushvalue(pL, -1);
//mt.__index = mt
lua_setfield(pL, -2, "__index");
*/
//注册元方法
luaL_setfuncs(pL, arraylib_m, 0);
//创建一个新的表,并把这些函数注册到这个表中
luaL_newlib(pL, arraylib_f);
return 1;
}
lua_openarray函数是加载布尔数组的重要一步,在该函数中,首先创建一个名称为LuaBook.array的元表,并复制了移分,然后让 mt.__index = mt,接着把arraylib_m中的方法全部都注册到了这个元表之中,这一步我目前有些疑问(经我个人测试,上面的那两句注释后不影响)。
lua_newlib(pL, arraylib_f)创建了一个新的表,并把这些函数注册到这个表中。
在lua_openarray方法中,虽然创建了LuaBook.array元表,但是绑定元表的过程在newarray中。
#include <cstdio>
#include <string>
#include "lua.hpp"
extern "C"
{
#include "bool_array.h"
}
int main()
{
char buffer[256];
int error = 0;
//打开lua
lua_State* pL = luaL_newstate();
//打开所有的标准库
luaL_openlibs(pL);
//注册函数
luaL_requiref(pL, "array", lua_openarray, 1);
lua_pop(pL, 1);
while (fgets(buffer, sizeof(buffer), stdin) != NULL)
{
//编译用户输入的每一行内容,并向栈中压入编译后得到的函数
//以保护模式弹出编译后的函数运行
error = luaL_loadstring(pL, buffer) || lua_pcall(pL, 0, 0, 0);
if (error)
{
fprintf(stderr, "%s\n", lua_tostring(pL, -1));
lua_pop(pL, 1);
}
}
lua_close(pL);
return 0;
}
然后则是把布尔数组加载进来,它主要的代码是这句:
//注册函数
luaL_requiref(pL, "array", lua_openarray, 1);
到这一步,我们已经注册了一个array.new方法在lua环境中了。
运行上述程序,我们就可以愉快的玩耍了,示例如下:
a = array.new(100)
a[3] = true
print(a[3])
print(a)