PartⅣ The C API
30 Managing Resources
上一节中实现的自定义类型,我们并没有关注于资源管理的问题。上一节实现的数组是需要关心内存问题,而这些问题由Lua实现管理。但是很多时候事情不那么简单,有些对象不仅需要内存空间,还会需要如窗口句柄、文件描述等资源。尽管说这些也是内存开销,但是这些资源是由系统的其他组件管理的。这种情况下,当一个对象被回收了,我们也需要合适的机制来实现回收这些额外的资源。
在17.6章节中,介绍了Lua提供的finalizer,由__gc 元方法所组成的一种机制。为了能在C和API中也能正常的进行资源管理,本章将会从Lua中扩展两个机制。第一个例子是一个函数遍历一个目录的实现;第二个例子是Expat的绑定,一个打开XML资源的解析器。
30.1 A Directory Iterator
之前已经实现过一个类似的dir函数,函数返回一个包含给定目录下所有文件的table。在这里我们要实现每次调用返回一个迭代器,代表一个条目(相当于之前table中的一个元素),这样我们就可以用以下的方式实现遍历一个目录:
e.g.
for fname in dir.open(".") do
print(fname)
end
在C中要实现遍历一个目录,此时需要一个DIR的数据结构。而且数据结构DIR的实例需要由opendir创建,而且必须在调用closedir的时候立即释放该实例。之前介绍的实现dir函数是将DIR的实例作为一个局部变量存储,然后在得到最后一个文件名字后释放该实例。然后在这里,不能将DIR的实例以局部变量来存储,这里考虑的是将会有多处的访问会访问该实例。不仅如此,也不能仅是在得到最后一个文件名字的时候做释放工作,因为假如程序中途退出了循环,那么就会不会再有释放了。所以,这里将DIR的实例地址以一个userdataum的形式存储,然后使用__gc
元方法来管理这个userdataum,用来释放这个资源。
需要借助三个C函数来实现我们的需求:1,需用dir.open 函数,Lua用来创建迭代器,这个函数用来打开一个DIR 结构,然后创建一个将这个结构以upvalue的形式的closure;2,一个iterator 函数;3,__gc 元方法,用来释放资源。当然也需要一个额外的函数用来做参数的初始化工作。
首先,dir.open 函数。这里要注意的是,一定要在打开目录之前创建好userdatum,因为如果顺序反了的话,会引发内存错误。在顺序正确的基础上,一创建好DIR 结构,就立马与userdatum链接起来了,之后__gc 元方法就会接管对其的释放工作。
然后,dir_iter函数,函数从upvalue中得到DIR结构的地址,然后调用readdir读取下一个条目。
接着,函数dir_gc 代表 __gc 元方法。这个方法关掉目录,但是这里要注意一点:因为是在打开目录之前先创建好了userdatum,所以无论opendidr的结果是如何,都会对userdatum进行回收。
最后,luaopen_dir 函数,是打开这个库的函数。
都是C函数形式实现的。
30.2 An XML Parser
这里介绍的是简单的实现Lua和Expat的绑定,叫做lxp。Expat是一个开源的用C写的XML1.0解析器。该解析器实现为SAX(the Simple API for XML)。SAX是一个基于API的事件分发器,即SAX解析器读取一行XML文档,然后通过回调来通知程序读取到的信息。如我们使用Expat解析一个字符串"<tag cap="5">hi</tag>",将会生成三个事件:当读取子串"<tag
cap-"5>"生成一个 start-element 事件;读取到"hi"的时候生成text event 事件;读取到"</tag>"的时候生成endelement event 事件。每个事件在程序中对应一个相应的回调事件。
Expat有非常多的事件,在这里主要是介绍上面提到的三个事件。
首先,介绍创建和清除Expat解析器的函数:
e.g. XML_Parser XML_ParserCreate(const char *encoding); /* 创建 */ void XML_ParserFree(XML_Parser p); /* 清除 */
第一个函数中的参数 encoding 是可选的,这里使用NULL。
得到了解析器之后,就是需要注册回调句柄了:
void XML_SetElementHandler(XML_Parser p, XML_StartElementHandler start, /* start 事件 */ XML_EndElementHandler end); /* end 事件 */ void XML_SetCharacterDataHandler(XML_Parser p, XML_CharacterDataHandler hndl); /* text 事件 */
所有的回调句柄接受user data 作为第一个参数。而且start-element 句柄也接受tag的名字和tag的属性作为其参数。
typedef void (*XML_StartElementHandler) (void *uData,const char *name,const char **atts);
而end-element句柄则只接受一个额外的参数,tag的名字:
typedef void(*XML_EndElementHandler)(void *uData,const char *name);
text 句柄则接受的是text作为其参数加上这个text的长度:
typedef void(*XML_CharacterDataHandler)(void *uData,const char *s,int len);
传递要解析的信息给解析器,用以下这个函数:
int XML_Parse(XML_Parser p,const char *s,int len,int isLast);
Expat通过连续调用XML_Parse,以块来接受需要解析的文档。最后一个参数isLast表示每次解析的块是否是文档中最后的一块。我们这里使用了每个块的实际长度作为参数,所以我们不需要给每个块做0终止符标记。该函数在遇到解析错误的时候将会返回0。
最后的函数便是,设置user data,传递给每个句柄用的:
void XML_SetUserData(XML_Parser p ,void *uData);