《Programming in Lua 3》读书笔记(二十五)
日期:2014.8.11
PartⅣ The C API
29 User-Defined Types in C
在之前的例子里,已经介绍过如果通过用C写函数来扩展Lua。在本章,将会介绍通过用C写新的类型来扩展Lua,将会使用到元方法等特性来实现这个功能。
以一个例子来介绍本章将要介绍的,例子实现的功能是实现了一个简单的类型:boolean arrays。实现这个功能主要是这种方法不需要太复杂的算法,因此可以将精力放在API的讨论上。当然我们可以在Lua中用一个table来实现,但是用一个C来实现,where we store each entry in one single bit(指的是用一个位数来表现boolean值?).,比用table来实现节省了3%的内存开销。
实现这个类型首先是需要做一些定义:
#include <limits.h> #define BITS_PER_WORD (CHAR_BIT*sizeof(unsigned int)) #define I_WORD(i) ((unsigned int)(i) / BIT_PER_WORD) #define I_BIT(i) (1 << ((unsigned int)(i) % BIT_PER_WORD))
BITS_PER_WORD 表示一个无符整型数中的位的数量。宏I_WORD 计算给定的数中位的数量,宏I_BIT 则计算了求一个数正数位的掩码。
以下面的struct代表我们定义的类型:
e.g. typedef struct NumArray { int size; unsigned int values[1]; } NumArray;
定义数组values的大小为1,实现一个占位符,因为C 89 不允许数组的大小为0.当我们allocate 这个数组的时候将会重新设定其实际大小。下面的表达式则计算了n个元素数组的实际大小:
e.g. sizeof(NumArray) + I_WORD( n -1 ) * sizeof(unsigned int)
29.1 UserData
首先要考虑的是在Lua中用什么来代表NumArray这个数据结构。Lua提供了一个基础的类型:userdata。一个userdatum提供了一块内存区域,没有做任何预定义的操作,因此可以用这个类型存储任何东西。
函数lua_newuserdata 根据给定的大小分配了一块内存区域,将相应的userdatum推进栈中,然后返回这个内存块的地址:
void* lua_newuserdata(lua_State *L,size_t size);
而如果需要以其他用途来分配内存,使用给定大小的指针创建一个userdatum,然后用一个指针存储至实际的内存块上是非常容易的。在后面的章节会介绍这个。
结合使用lua_newuserdata ,那么创建一个新的boolean arrays 将会是这样实现的:
e.g. static int newarray(lua_State *L) { int i; size_t nbytes; NumArray *a; int n = luaL_checkint(L,1); luaL_argcheck(L,n >= 1,1,"invalid siez"); 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; return 1; }
一旦newarray注册到了Lua中,那么就可以通过如a = array.new(1000) 来创建新的array了。
而使用arrar.set(a,index,value) 来存储一个条目(一个元素?)。而且与Lua中其余数据结构一致,新的arrary的index值从1开始,下面的函数设定一个数组给定index的值
static int 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); else a->values[I_WORD(index)] &= ~I_BIT(index); return 0; }
函数中要到了一些位运算,感觉看起来好吃力。因为了Lua中的boolean变量接受任何类型的值,因此在这里使用luaL_checkany 来检测三个参数:保证每个参数都有一个对应的值,(函数要三个参数,那么就需要有三个参数)。如果不满足条件,那么就会报错:
e.g. array.set(0,11,0) --stdin:1: bad argument #1 to 'set' ('array' expected) 这里的意思是第一个参数必须是数组'array'类型的 array.set(a,1) --stdin:1: bad argument #3 to 'set' (value expected) 这里函数只传递两个参数,因此报错的是少了第三个参数,第三个参数必须有值。
下面的这个函数从数据中得到值:
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 range"); lua_pushboolean(L,a->value[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; }
处理完以上的操作之后,便是初始化库,然后加入到Lua中去:
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; }
从上面的操作可以看出,使Lua支持用C定义的类型,就是用到了自定义库的特性。用C写好库,然后注册到Lua中,再在Lua中使用就可以了。
打开了自定义的库之后,便可以通过以下方式使用我们新写的类型了:
a = array.new(1000) print(a) --> user data print(array.size(a)) --> 1000 for i = 1,1000 do array.set(a,i,i % 5 == 0) --设定 end print(array.get(a,10)) --true --得到
29.2 Metatables
上部分实现的功能有一个安全隐患。假如用户使用array.set(io.stdin,1,false) 来设定了一个值。此时userdatum指针指向的是一个流(FILE*),因为其类型是一个userdatum ,array.set 会接受这个值。此时会造成内存冲突的问题(而其实得到的error meg是告诉你index溢出了)。这是不被Lua的库所接受的。
通常,为我们新定义的类型创建一个独特的metatable作为唯一标识符,用来与其它的userdata区分开来是不错的方法。每次我们创建了一个userdata,都会用与之相对应的metatable来标记这个userdata;而每次我们得到一个userdata,则都会检测是否是正确的metatable。因为Lua的代码是不能修改userdatum的metatabel的,所以不用担心这会影响我们的代码。
下一步需要注意的就是,如何存储我们这里要用到的metatable。在上一章中提到了两种存储数据的方式:registry 和 upvalue。通常在Lua中,要将任意定义的C Type注册至registry中,会使用类型名字作为index,然后以metatable作为value,同时我们也需要考虑到命名冲突的问题,因此在这里使用"LuaBook.array"作为名字。
然后就是使用辅助库中的函数来实现我们这里需要的功能了:
int luaL_newtatable(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);
第一个函数将会创建一个新的table(用来作为metatable),将创建好的table放在栈顶,然后以给定的名字存储至rigistry中;第二个函数,从rigistry中根据给定的名字得到一个metatable;第三个函数,检测给定index位置的对象的metatable 与 名为tname 的metatable是否相等,如果不相同,将会引发错误,相同的话就会返回userdata的位置。
修改打开库的函数,添加创建新的metatable的功能:
int luaopen_array(lua_State *L) { luaL_newmetatable(L,"LuaBook.array"); /* 这里加入了创建metatable 的功能 */ luaL_newlib(L,arraylib); return 1; }
再修改创建array的函数,为每次创建的array设置metatable:
static int newarray(lua_State *L) { //new luaL_getmetatable(L,"LuaBook.array"); lua_setmetatable(L,-2); return 1; }
函数lua_setmetatable 从栈中推出一个table,然后将其设定为给定index对象的metatable。
然后在使用setarray , getarray ,getsize 的时候就需要对第一个参数做检测了。
之后,如果第一个参数错误了,如:array.get(io.stdin,10),那么编译器将会抛出错误:
error: bad argument #1 to 'get' ('array' expected)
29.3 Object-Oriented Access
这部分将要实现的是,将我们新实现的这个类型转换为一个对象,使得我们可以用面向对象的语法(object-oriented syntax)来对其进行操作,如:
a = array.new(1000) print(a:size()) --使用了冒号操作符 a:set(10,true) …
这里的a:size() 相当于 a.size(a) ,实现这个功能的关键在与使用了__index 元方法。在table中 ,如果没有找到给定key的value,那么lua就会调用这个元方法。而对于userdata来说,因为其根本就没有key,所以每次都会调用这个元方法。
示例:
local metaarray = getmetatable(array.new(1)) metaarray.__index = metarray metaarray.set = array.set metaarray.get = array.get metaarray.size = array.size
第一行代码的功能主要是:创建一个新的array,然后得到它的metatable,赋值给metaarray(尽管lua中不能给userdata设置metatable,但是可以得到metatable)。后面的代码就是设置metatable的相关元方法。当我们调用a.size 计算size的时候,Lua不能从对象a中找到“size”这个key,就会从字段 __index 中去寻找这个值,而此时 __index 对应的便是metaarry本身,而我们设定了metaarray.size
= array.size ,所以a.size(a)返回结果便是array.size(a),如我们所愿了。
我们也可用用C来实现上述的特性,并且在C中可以做的更好了:因为此时array是一个对象了,对象有其自己内部封装好的操作,因此我们就不必要将这些如getsize 等的操作放至要注册的列表中了。只需要将创建新对象的函数放至列表即可。所有的其他操作函数都成为了对象的方法。
之前实现的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 此时就需要创建metatable,然后将其自身赋值为 __index ,注册其余的操作函数等,最后创建array table:
int luaopen_array(lua_State *L) { luaL_newmetatable(L,"LuaBook.array"); lua_pushvalue(L,-1); lua_setfield(L,-2,"__index"); luaL_setfuncs(L,arraylib_m,0); luaL_newlib(L,arraylib_f); return 1; }
在这里使用luaL_setfuncs 将arraylib_m列表内的函数配置到metatable中,然后使用luaL_newlib 创建一个新的table,然后注册arraylib_f中的函数。
29.4 Array Access
数组中实现面向对象的语法形式,还可以使用通常数组的语法形式来实现。如,相比于使用a:get(i),我们也可以用a[i]来实现。我们可以通过定义一些元方法来实现我们的需求:
e.g. local metaarray = getmetatable(array.new(1)) metaarray.__index = array.get metaarray.__newindex = array.set metaarray.__len = array.size
这样我们就可以用数组语法来实现我们需要的功能了:
a = array.new(1000) a[10] = true -- 'setarray' print(a[10]) -- 'getarray' print(#a) -- 'getsize'
同样的,我们也需要将这些元方法在C中进行注册,也是通过修改初始化函数来实现。
29.5 Light Userdata
我们之前使用到的Userdata被称之为full userdata,除此之外,还有另一种类型的userdata,称之为light userdata.
一个light userdatum 是一个代表C指针的value(一个 void* 的value)。light userdata 是一个value,而不是一个对象;因此是不能被创建的。使用函数lua_pushlightuserdata 来将一个light userdatum 推进至栈中:
void lua_pushlightuserdata(lua_State *L,void *p);
尽管都称为userdata,但是light userdata 和 full userdata 是不同的两个概念。light userdata不是buffers,而仅是指针而已。light user data没有metatable,而与number一样,light uesrdata 是不被garbage collector管理的。
有的时候,light userdata 执行的是full userdata的轻量化替代工作。但是这也不是绝对化的。首先,light userdata 没有metatable,因此没有办法知道它们的类型;其次,full userdata也并不是占用很大开销的。
真正上使用light userdata是用来做对比的。因为full userdata是对象,只与自己相比才会相等。而light userdata代表的是一个C指针,因此它会与任意代表同一个指针的userdata相等。所以我们可以使用light userdata在Lua中寻找到C对象。