《Programming in Lua 3》读书笔记(二十五)

来源:互联网 发布:mysql 导入数据乱码 编辑:程序博客网 时间:2024/06/07 12:59
日期: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 dataprint(array.size(a))     --> 1000for i = 1,1000 do     array.set(a,i,i % 5 == 0)     --设定endprint(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 = metarraymetaarray.set = array.setmetaarray.get = array.getmetaarray.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.getmetaarray.__newindex = array.setmetaarray.__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对象。
0 0
原创粉丝点击