Lua资源管理详解:文件、内存与用户数据的高效管理 | 南锋

南锋

南奔万里空,脱死锋镝余

Lua资源管理详解:文件、内存与用户数据的高效管理

函数dir会遍历目录并返回一张包含指定目录下所有内容的表。这里对dir新的实现会返回一个迭代器,每次调用这个迭代器时它都会返回一个新元素。通过这种实现,我们就能使用如下的循环来遍历目录:

1
2
3
for fname in dir.open(".")do
print(fname)
end

要在C语言中遍历一个目录,我们需要用到DIR结构体。DIR的实例由opendir创建,且必须通过调用closedir显示地释放。在之前的实现中,我们将DIR的示例当做局部变量,并在或获取最后一个文件名后释放了它。而在新的实现中,由于必须通过多次调用来查询该值,因此不能把DIR的实例保存到局部变量中。此外,不能在获取最后一个文件名后再释放DIR的示例,因为如果程序从循环中跳出,那么迭代器永远不会获取最后一个文件名。因此,为了确保DIR的实例能被正确释放,需要把该实例的地址存入一个用户数据中,并且用这个用户数据的远方__gc来释放该结构体。
尽管用户数据在我们实现中处于核心地位,但这个表示目录数据并不一定需要对Lua可见。函数dir.open会返回一个Lua可见的迭代函数,而目录可以作为迭代函数的一个上值。这样,迭代函数能直接访问这个结构体,而Lua代码则不能。
总之,我们需要三个C语言函数。首先,我们需要函数dir.open,该函数是一个工厂函数,Lua调用该函数来创建迭代器;它必须打开一个DIR结构体,并将这个结构体作为上值创建一个迭代函数的闭包。其次,我们需要迭代函数。最后,我们需要__gc元方法,该元方法用于释放DIR结构体。通常情况下,我们还需要一个额外的函数进行一些初始化工作,例如为目录创建和初始化元表。
先来看函数dir.open,参加下面示例

示例 工厂函数dir.open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include "lua.h"
#include "luaxlib.h"

/*迭代函数的前向声明*/
static int dir_iter(lua_State *L);

static int l_dir (lua_State *L){
const char *path = luaL_checkstring(L,1);
/*创建一个保存DIR结构体的用户数据*/
DIR **d = (DIR **)lua_newuserdata(L,sizeof(DIR *));

/* 预先初始化*/
*d = NULL

/*设置元表*/
luaL_getmetatable(L,"LuaBook.dir");
lua_setmetatable(L,-2);

/*尝试打开指定目录*/
*d = opendir(path);
if (*d == NULL) /* 打开目录失败*/
luaL_error(L, "cannot open %s:%s",path,strerror(errno));
/*创建并返回迭代函数;该函数唯一的上值,即代表目录的用户数据本身就位于栈顶*/
lua_pushcclosure(L,dir_iter,1);
return 1;
}

在这个函数中要注意的是,必须在打开目录前先创建用户数据。如果先打开目录再调用lua_newuserdata,那么会引发内存错误,该函数会丢失并泄露DIR结构体。如果顺序正确,DIR结构体一旦被创建就会立即与用户数据相关联;无论此后发生什么,元方法__gc最终都会将其释放。
另一个需要注意的点是用户数据的一致性。一旦设置了元表,元方法__gc就一定会被调用。因此,在设置元表前,我们需要使用NULL预先初始化用户数据,以确保用户数据具有定义明确的值。
下一个函数dir_iter也就是迭代器本身。

示例 dir库中的其他函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int dir_iter(lua_State *L){
DIR *d = *(DIR **)lua_touserdata(L, lua_upvalueindex(1));
struct dirent *entry = readdir(d);
if (entry != NULL){
lua_pushstring(L,entry ->d_name);
return 1;
}
else return 0; /* 遍历完成*/
}

static int dir_gc(lua_State *L) {
DIR *d = *(DIR **)lua_touserdata(L,1);
if(d) closedir(d);
return 0;
}

static const struct luaL_Reg dirlib [] = {
{"open",l_dir},
(NULL,NULL)
};

int luaopen_dir (lua_State *L){
luaL_newmetatable(L,"luaBook.dir");
/*设置__gc字段*/
lua_pushcfunction(L,dir_gc);
lua_setfield(L,-2,"__gc");
/*创建库*/
luaL_newlib(L,dirlib);
return 1;
}

上述代码很简单,它从上值中获取DIR结构体的地址,然后调用readdir读取下一个元素。
函数dir_gc就是元方法__gc,该元方法用于关闭目录。正如之前提到的,该元方法必须做好防御措施:如果初始化出现错误,那么目录可能会是NULL。
整个示例中还有一点需要注意。dir_gc似乎应该检查其参数是否为一个目录以及目录是否已经被关闭;否则,恶意用户可能会用其他类型的用户数据来调用dir_gc或者关闭一个目录两次,这样会造成灾难性后果。然而,Lua程序时无法访问这个函数的:该函数被保存在目录的元表中,而用户数据又被保存为迭代函数的上值,因此Lua代码无法访问这些目录。

XML解析器

接下来,我们介绍一种使用Lua语言编写的Expat绑定的简单实现,称为lxp。Expat是一个用C语言编写的开源XML1.0解析器,实现了SAX,即Simple API for XML。SAX是一套基于事件的API,这就意味着一个SAX解析器在读取XML文档时会边读取边通过回调函数向应用上报读取到的内容。例如,如果让Expat解析形如”hi“的字符串,那么Expat会生成三个事件:当读取到子串”“时,生成开始元素事件;当读取到”hi”时,生成文本事件,也称为字符数据事件;当读取到”“时,生成结束元素事件。每个事件都会调用应用中相应的回调处理器。
在此我们不会介绍整个Expat库,只关注于那些用于演示与Lua交互的新技术部分。虽然Expat可以处理很多不同的事件,但我们只考虑前面示例中所提到的三个事件(开始元素、结束元素和文本事件)。
本例中用到的Expat API很少。首先,我们需要用于创建和销毁Expat解析器的函数:

1
2
XML_Parser XML_ParserCreate(const char *encoding);
void XML_PaserFree(XML_Parser p);

参数encoding是可选的,本例中将使用NULL。
当解析器创建完成后,必须注册回调处理器:

1
2
void XML_SetElementHandler(SML_Parser p,XML_StartElementHandler start,XML_EndElementHandler end);
void XML_SetCharacterDataHandler(XML_Parser p, XML_CharacterDataHandler hndl);

第一个函数为开始元素和结束元素事件注册了处理函数,第二个函数为文本(XML术语中的字符数据character data)事件注册了处理函数。
所有回调处理函数的第一个参数都是用户数据,开始元素事件的处理函数还能接收标签名(tag name)及其属性(attribute):

1
typedef void(*XML_StartElementHandler)(void *uData, const char *name, const char **atts);

属性是一个以NULL结尾的字符串数组,其中每对连续的字符串保存一个属性的名称和值。结束元素事件处理函数除了用户数据外还有一个额外的参数,即标签名:

1
typedef void(XML_EndElementHandler)(void *uData, const char *name);

最后,文本事件处理函数只接收文本作为额外参数,该文本字符串不是以NULL结尾的,它有一个显示的长度:

1
2
3
typedef void(*XML_CharacterDataHandler)(void *uData, const char *s, int len);
```c
为了将文本输入Expat,可以使用如下的函数:

int XML_Parser(XML_Parser p, const char *s, int len, int isLast);

1
2
3
4
Expat通过连续调用函数XML_Parse一段一段地接收要解析的文档。XML_Parse的最后一个参数,布尔类型的isLast,告知Expat该片段是否是文档的最后一个片段。如果检测到解析错误,XML_返回零。
Expat中要用到的最后一个函数允许我们设置传递给事件处理函数的用户数据:
```c
void XML_SetUserData(XML_Parser p, void *uData);

现在,让我们看一下如何在Lua中使用这个库。第一种方法是一种直接的方法,即简单地把所有函数导出给Lua。另一个更好的方法是让这些函数适配Lua。例如,因为Lua语言不是强类型的,所以不需要为每一种回调函数设置不同的函数。我们可以做得更好,甚至免去所有注册回调函数的函数。我们要做的只是创建解析器时提供一个包含所有事件处理函数的回调函数表,其中每一个键值对是与相应时间对应的键和事件处理函数。例如,如果需要打印出一个文档的布局,可以使用如下的回调函数表:

1
2
3
4
5
6
7
8
9
10
11
12
local count = 0
callbacks = {
StartElement = function(parser, tagname)
io.write("+ ", string.rep(" ",count),tagname,"\n")
count = count + 1
end,

EndElement = function (parser,tagname)
count = count - 1
io.write("- ",string.rep(" ",count),tagname,"\n")
end,
}

输入内容”“时,这些事件处理函数会打印出如下内容:

1
2
3
4
+ to
+ yes
- yes
- to

有了这个API,我们就不再需要那些操作回调函数的函数了,可以直接在回调函数表中操作它们。因此,整个API只需用到三个函数:一个用于创建解析器,一个用于解析文本,一个用于关闭解析器。实际上,我们可以将后两个函数实现为解析器对象的方法。该API的典型用法如下:

1
2
3
4
5
6
7
8
9
local lxp = require "lxp"
p = lxp.new(callbacks)
for l in io.lines() do
assert(p:parse(l))
assert(p:parse("\n"))
end

assert(p:parse())
p:close()

现在,让我们来看看如何实现它。首先要决定如何在Lua语言中表示一个解析器。我们会很自然地想到使用用户数据来包含C语言结构体,但是需要在用户数据中放些什么东西呢?我们至少需要实际的Expat解析器来回调函数表。由于这些解析器对象都是Expat回调函数接收的,并且回调函数需要调用Lua语言,因此还需要保存Lua状态。我们可以直接在C语言结构体中保存Expat解析器和Lua状态;而对于作为Lua语言值的回调函数表,一个选择是在注册表中为其创建引用并保存该引用,另一个选择是使用用户值。每个用户数据都可以有一个与其直接关联的唯一的Lua语言值,这个值就被叫做用户值。要是使用这种方式的话,解析器对象的定义形如:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "expat.h"
#include "lua.h"
#include "luaxlib.h"

typedef struct lxp_userdata{
XML_Parser parser;
lua_State *L;
}lxp_userdata;

下一步是创建解析器对象的函数lxp_make_parser,参考下例

示例 创建XML解析器对象的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 回调函数的前向声明 */
static void f_StartElemnt (void *ud, const char *name, const char **atts);
static void f_CharData(void *ud, const char *s,int len);
static int lxp_make_parser(lua_State *L){
XML_Parser p;
/* 创建解析器对象 */
lxp_userdata *xpu = (lxp_userdata *)lua_newuserdata(L, sizeof(lxp_userdata));
/* 预先初始化以防止错误发生 */
xpu->parser = NULL;

/* 设置元表*/
luaL_getmetatable(L, "Expat");
lua_setmetatable(L,-2);

/* 创建Expat解析器 */
p = xpu-> parser = XML_ParserCreate(NULL);
if(!p)
luaL_error(L,"XML_ParserCreate failed");

/* 检查并保存回调函数表*/
luaL_checktype(L,1,LUA_TTABLE );
lua_pushvalue(L,1); /* 回调函数表入栈 */
lua_setuservalue(L,-2); /* 将回调函数表设置为用户值*/

/* 设置Expat解析器 */
XML_SetUserData(p,xpu);

XML_SetElementHandler(p,f_StartElemnt, f_EndElement);
XML_SetCharacterDataHandler(p,f_CharData);
return 1;
}

该函数有四个主要步骤。

  • 第一步遵循常见的模 式:先创建用户数据,然后使用一致性的值预先初始化用户数据,最后设置用户数据的元表(其中的预先初始化确保如果在初始化过程中发生了错误,解析器能够以一致性的状态处理用户数据)。
  • 第二步中,该函数创建了一个Expat解析器,将其存储到用户数据中,并检查了错误。
  • 第三步保证该函数的第一个参数是一个表(回调函数表),并将其作为用户值赋给了新的用户数据。
  • 最后一步初始化Expat解析器,将用户数据设为传递给回调函数的对象,并设置了回调函数。请注意,这些回调函数对于所有的解析器来说都是相同的;毕竟,用户无法在C语言中动态地创建新函数。不同点在于,这些固定的C语言函数会通过回调函数表来决定每次应该调用哪些Lua函数。
    接下来是解析函数lxp_parse,该函数用于解析XML数据片段。

    示例 解析XML片段的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    static int lxp_parse(lua_State *L){
    int status;
    size_t len;
    const char *s;
    lxp_userdata *xpu;
    /* 获取并检查第一个参数 */
    xpu = (lxp_userdata *)luaL_checkudata(L,1,"Expat");

    /* 检查解析器是否已经被关闭了*/
    luaL_argcheck(L,xpu->parser != NULL, 1, "parser is closed");

    /* 获取第二个参数*/
    s = luaL_optlstring(L,2,NULL, &len);

    /* 将回调函数表放在栈索引为3的位置 */
    lua_settop(L,2);
    lua_getuservalue(L,1);
    xpu->L = L; /*设置Lua状态*/

    /*调用Expat解析字符串*/
    status = XML_Parse(xpu->parser , s, (int)len , s == NULL);

    /* 返回错误码 */
    lua_pushboolean(L,status);
    return 1;
    }
    该函数有两个参数,即解析器对象和一个可选的XML数据。如果调用该函数时未传入XML数据,那么它谁通知Expat文档已结束。
    当lxp_parse调用XML_Parse时,后一个函数会为指定文件片段中找到的每个相关元素调用处理函数。这些处理函数需要访问回调函数表,因此lxp_parse会将这个表放到栈索引为3的位置。在调用XML_Parse时还有一个细节:请注意,该函数的最后一个参数会告诉Expat文本的指定片段是否为最后一个片段。当不带参数调用parse时,s是NULL,这样最后一个参数就为真。
    现在我们把注意力放到处理回调的f_CharData、f_StartElement和f_EndElement函数上。这三个函数的代码结构类似,它们都会检查回调函数表是否为指定的事情定义了Lua处理函数,如果是,则准备好参数并调用这个处理函数。
    首先来看实力中的梳理函数f_CharData.

    示例 字符数据事件的处理函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static void f_CharData(void *ud , const char *s, int len){
    lxp_userdata *xpu = (lxp_userdata *)ud;
    lua_State *L = xpu -> L;

    /* 从回调函数表中获取处理函数 */
    lua_getfield(L,3,"CharacterDara");
    if (lua_isnil(L,-1)) { /* 没有处理函数? */
    lua_pop(L,1);
    return;
    }

    lua_pushvalue(L,1); /* 解析器压栈*/
    lua_pushlstring(L,1,len); /* 压入字符数据 */
    lua_call(L,2,0); /* 调用处理函数 */
    }
    该函数的代码很简单。由于创建解析器时调用了XML_SetUserData,所以处理函数的第一个参数是lxp_userdata结构体。在获取Lua状态后,处理函数就可以访问由lxp_parse设置的位于栈索引3位置的回调函数表,以及位于栈索引1位置的解析器。然后,该函数就可以用解析器和字符数据作为参数调用Lua中对应的处理函数了。
    处理函数f_EndElement与f_CharData十分相似,参见示例

    示例 结束元素事件的处理函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static void f_EndElement(void *ud, const char *name){
    lxp_userdata *xpu = (lxp_userdata *)ud;
    lua_State *L = xpu ->L;

    lua_getfield(L,3,"EndElement");
    if (lua_isnil(L,-1)){ /*没有处理函数?*/
    lua_pop(L,1);
    return;
    }
    lua_pushvalue(L,1); /* 解析器压栈 */
    lua_pushstring(L,name); /* 压入标签名 */
    lua_call(L,2,0); /* 调用处理函数 */
    }
    该函数也以解析器和标签名(也是一个字符串,但是以null结尾)作为参数调用相应的Lua处理函数。
    示例演示了最后一个处理函数f_StartElement.

    示例 开始元素事件的处理函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    static void f_StartElemnt (void *ud, const char *name, const char **atts){
    lxp_userdata *xpu = (lxp_userdata *)ud;
    lua_State *L = xpu->L;

    lua_getfield(L,3,"StartElement");
    if (lua_isnil(L,-1)) { /* 没有处理函数? */}
    lua_pop(L,1);
    return;
    }
    lua_pushvalue(L,1); /* 解析器压栈 */
    lua_pushstring(L,name); /* 压入标签名*/

    /* 创建并填充属性表 */
    lua_newtable(L);
    for (; *atts; atts += 2){
    lua_pushstring(L, *(atts + 1));
    lua_Setfield(l,-2,*atts); /* table[*atts] = *(atts + 1) */
    }
    lua_call(L,3,0); /* 调用处理函数 */
    该函数以解析器、标签名和一个属性列表为参数,调用了Lua处理函数。处理函数f_StartElement比其他的处理函数稍微复杂一点,因为它需要将属性的标签列表转换为Lua语言。f_StartElement使用了一种非常自然的转换方法,即创建一张包含属性名和属性值的表。例如,类似这样的开始标签:
    1
    <to method = "post" proirity = "hight">
    会产生如下的属性表:
    1
    {method = "post", priority = "hight"}
    解析器的最后一个方法是close,参见示例

    示例 关闭XML解析器的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static int lxp_close(lua_State *L){
    lxp_userdata *xpu = (lxp_userdata *)luaL_checkudata(L,1,"Expat");

    /* 释放Expat解析器 */
    if (xpu->parser)
    XML_ParserFree(xpu->parser);
    xpu->parser = NULL; /* 避免重复关闭*/
    return 0;
    }
    当关闭解析器时,必须释放其资源,也就是Expat结构体。请注意,由于在创建解析器时可能会发生错误,解析器可能没有这些资源。此外还需注意,如何像关闭解析器一样,在一致的状态中保存解析器,这样当我们试图再次关闭解析器或者垃圾收集器结束解析器时才不会产生问题。实际上,我们可以将这个函数当做终结器来使用。这样便可以确保,即使程序员没有关闭解析器,每个解析器最终也会释放其资源。
    下面示例是最后一步,它演示了打开库的luaopen_lxp。luaopen_lxp将前面所有的部分组织到一起。

    示例 lxp库的初始化代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    static const struct luaL_Reg lxp_meths[] = {
    {"parse", lxp_parse},
    {"close", lxp_close},
    {"__gc", lxp_close},
    {NULL,NULL}
    };

    static const struct luaL_Reg lxp_funcs[] = {
    {"new",lxp_make_parser},
    {NULL,NULL}
    };

    int luaopen_lxp(lua_State *L){
    /*创建元表*/
    luaL_newmetatble(L,"Expat");
    /* metatable.__index = metatable */
    lua_pushvalue(L,-1);
    lua_setfield(L,-2,"__index");

    /*注册方法*/
    luaL_setfuncs(L,lxp_meths,0);

    /* 注册(只有lxp.new) */
    luaL_newlib(L,lxp_funcs);
    return 1;
    }
+