Q:Lua的”finalizer”?
A:在我们之前看到的使用”userdata”的例子中,我们只关心如何创建并使用”userdata”,从未关心何时以及如何释放我们创建的”userdata”,因为这些事都由Lua的垃圾回收器帮我们处理。然而很多时候,程序并不会这么简单,有可能在其中还会涉及到文件句柄,窗口句柄等,此时这些资源就需要创建者进行管理。
一些面向对象语言提供了析够器用来帮助用户管理这些资源,Lua同样提供了类似的机制,”finalizer”,它以名为__gc
的”metamethod”的形式供用户使用。__gc
存储的必须是一个函数,并且只能供”userdata”使用。当一个”userdata”将要被垃圾回收器收集时,Lua会寻找其”metatable”中是否有__gc
这个”metamethod”,如果有,Lua会以”userdata”本身作为参数调用这个函数,在函数中,用户可以释放那些需要手动管理的资源。
Q:如何使用”finalizer”,第一个例子?
A:在之前的章节(快速掌握Lua 5.3 —— 从Lua中调用C函数的“如何在C中调用注册给Lua的C函数”部分),我们实现过一个mydir
函数,它遍历指定目录中的所有文件,最终返回一个存储这些文件名的”table”。接下来,我们将重新实现这个函数,而这回我们将让其返回一个”itrator”。这样,当我们遍历一个目录时,我们可以通过类似如下的方式获取目录中的文件名, for fname in mydir(".") do print(fname) end
之前的例子中,我们将DIR
作为局部变量保存,在mydir
函数中一次性读取指定目录中所有的文件名并保存在”table”中,之后在函数返回之前将DIR
释放。而在接下来的例子中,因为每次”iterator”被调用时都需要使用它,所以我们不能将DIR
作为局部变量保存,同时不能将其在函数返回前释放,我们仅能在遍历完目录中最后一个文件时将其释放。因此,我们将DIR
实例的地址存储在”userdata”中,之后在__gc中
手动将其释放。
“mylib.c”文件中:
#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <dirent.h>
#include <errno.h>
// "iterator"函数。每次被调用返回指定目录中的一个文件名。
static int dir_iter(lua_State *L)
{
// 创建的"userdata"是"DIR **"类型的;从"userdata"中取出实际的"DIR"指针。
DIR *d = *(DIR **)lua_touserdata(L, lua_upvalueindex(1));
struct dirent *entry;
if((entry = readdir(d)) != NULL) // 读取目录中的一个文件。
{
lua_pushstring(L, entry->d_name); // 将其文件名入栈。
return 1; // 将文件名返回给Lua。
}
else
{
return 0; // 目录遍历完毕,没有返回值,终止Lua中的"for"循环。
}
}
// 注册给Lua的全局函数"dir"实际存储的函数。
static int l_dir(lua_State *L)
{
const char *path = luaL_checkstring(L, 1); // 第一个参数是需要遍历的目录。
/* 创建一个"userdata"用来存储"DIR"指针。
* 我们必须在下面的"opendir"之前创建"userdata"。
* 因为如果先调用"opendir",那么一旦在执行"lua_newuserdata"的过程中出错,
* 我们将失去"DIR"指针,这将导致内存泄漏
* (丢失了"DIR"指针相当于丢失了其指向的"DIR"结构体)。
* 正确的顺序是,我们一旦调用"opendir"获得"DIR"指针,就要马上与userdata关联,
* 这样无论之后发生什么,在"__gc"中都可以释放"DIR"指针。
*/
DIR **d = (DIR **)lua_newuserdata(L, sizeof(DIR *));
// 设置刚创建的"userdata"的"metatable"。
luaL_getmetatable(L, "LuaBook.dir");
lua_setmetatable(L, -2);
// 打开需要遍历的目录,获得"DIR"指针。
*d = opendir(path);
if(*d == NULL)
{
luaL_error(L, "cannot open %s: %s", path,strerror(errno));
}
/* 创建一个C中的"Closure",将栈顶的"userdata"作为其"upvalue",
* "Closure"的主体函数部分是"dir_iter"函数。
* 最终将创建的C中的"Closure"入栈,并作为返回值返回给Lua。
*/
lua_pushcclosure(L, dir_iter, 1);
return 1;
}
/* "metamethod.__gc"实际存储的函数。
* 当"userdata"将要被Lua的垃圾回收器收集时,Lua会以"userdata"本身作为参数调用此函数。
*/
static int dir_gc(lua_State *L)
{
DIR *d = *(DIR **)lua_touserdata(L, 1);
if (d) closedir(d);
return 0;
}
/* 注意这里,与之前例子中的"luaopen_mylib"函数有细微的差别。
* 在注册提供给外部使用的函数时没有使用"luaL_newlib" + "luaL_Reg"结构体数组的形式。
* 而是直接将提供给外部使用的函数注册为Lua的全局函数。
* 这种方式下,在Lua中无需通过"require"的返回值调用函数,
* 而是可以直接调用函数(详见"a.lua"中的代码)。
* 此种方式下也就无需返回C库本身,所以返回值为0。
* 此种方式不推荐。
*/
int luaopen_mylib(lua_State *L)
{
// 标识全局唯一的"metatable",创建并入栈。
luaL_newmetatable(L, "LuaBook.dir");
// "metamethod.__gc = gc"。
lua_pushstring(L, "__gc");
lua_pushcfunction(L, dir_gc);
lua_settable(L, -3);
// 注册提供给外部使用的"dir"函数。
lua_pushcfunction(L, l_dir);
lua_setglobal(L, "dir");
return 0;
}
将”mylib.c”编译为动态连接库,
prompt> gcc mylib.c -fPIC -shared -o mylib.so -Wall
prompt> ls
mylib.c mylib.so a.lua
“a.lua”文件中:
require "mylib" -- C库中的函数是以Lua全局函数的形式提供的,所以无需获得C库的实例。
for fname in dir(".") do print(fname) end -- 直接调用函数即可。
--[[ results:
mylib.so
mylib.c
.
a.lua
..
]]
Q:如何使用”finalizer”,第二个例子?
A:这个例子中,我们将使用Lua实现一个XML解析器的简单封装,其核心使用”Expat”。”Expat”是一个使用C语言编写的XML解析器,它实现了”SAX”(the Simple API for XML)。”SAX”是一套基于事件驱动的API,当其读取XML时,会通过回调函数的方式向应用程序报告它所读取到的关键信息。举个例子,当我们使用”Expat”解析以下这段XML时, <tag cap="5">hi</tag>
“Expat”会产生三个事件。
当其读取了<tag cap="5">
时,会触发”start-element”事件;
当其读取了hi
时,会触发”text”事件(也称做”character data”事件);
当其读取了</tag>
时,会触发”end-element”事件;
其所处发的每一个事件,都会调用应用程序所指定的对应的回调函数。
当然,”Expat”还有很多其他种类的事件。但在我们接下来的例子中,仅会涉及以上3种事件。
首先,我们来看看如何创建和销毁”Expat”解析器:
#include <expat.h>
// "encoding"是可选参数,例子中将使用"NULL"。
XML_Parser XML_ParserCreate(const char *encoding);
void XML_ParserFree(XML_Parser p);
有了解析器,我们需要知道如何注册其所触发的事件的回调函数:
/* 注册"start-element"和"end-element"事件回调函数的函数。
* 参数1:"Expat"解析器。
* 参数2:"start-element"事件的回调函数。
* 参数3:"end-element"事件的回调函数。
*/
XML_SetElementHandler(XML_Parser p,
XML_StartElementHandler start,
XML_EndElementHandler end);
/* 注册"text"事件回调函数的函数。
* 参数1:"Expat"解析器。
* 参数2:"text"事件的回调函数。
XML_SetCharacterDataHandler(XML_Parser p,
XML_CharacterDataHandler hndl);
所有的回调函数都会接收一些用户数据(在例子中,这些用户数据将传递Lua的”userdata”)作为其第一个参数。
“start-element”事件的回调函数还会额外的接收”tag”的名称以及其携带的属性(在上面的例子中是cap="5"
),
/* 参数1:"userdata"。
* 参数2:名字。
* 参数3:属性。
*/
typedef void (*XML_StartElementHandler)(void *uData,
const char *name,
const char **atts);
“end-element”事件的回调函数会额外的接收”tag”的名称,
/* 参数1:"userdata"。
* 参数2:名字。
*/
typedef void (*XML_EndElementHandler)(void *uData,
const char *name);
“text”事件的回调函数会额外的接收解析出来的文本。文本以非\0
结尾的字符串传递,字符串的长度由参数指定,
/* 参数1:"userdata"。
* 参数2:解析出来的文本。
* 参数3:文本的长度。
*/
typedef void (*XML_CharacterDataHandler)(void *uData,
const char *s,
int len);
向”Expat”传递XML很简单,使用以下函数,
/* 参数1:"Expat"解析器。
* 参数2:XML。
* 参数3:XML的长度。
* 参数4:是否为整个XML中的最后一部分。
*/
int XML_Parse(XML_Parser p,
const char *s,
int len,
int isFinal);
你可以分段向”Expat”传递文本,而且无需以\0
结尾(通过”len”参数指定文本的长度),当传递整段XML的最后一部分时,将”isFinal”参数设置为true
。
最后我们要知道的是,如何告诉”Expat”我们要传递的”userdata”。
/* 参数1:"Expat"解析器。
* 参数2:"userdata"。
*/
void XML_SetUserData(XML_Parser p,
void *uData);
接下来,让我们来看看如何使用这些函数。最简单的方法当然是将这些函数全部注册到Lua中,然后在Lua代码中逐一的使用。这样虽然C库的编写方便了,但使用此C库的Lua代码将变得困难而复杂,这违背了提供C库的意义。所以在C库中,我们将对这些函数做一定的封装。
“mylib.c”文件中:
#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <expat.h> // 使用"Expat"需要的头文件。
typedef struct lxp_userdata {
lua_State *L; // lua虚拟机。
XML_Parser parser; // "Expat"解析器。
int tableref; // 回调函数"table"在"registry"中的索引值。
} lxp_userdata;
// "text"事件的回调函数。
static void f_CharData(void *ud, const char *s, int len)
{
lxp_userdata *xpu = (lxp_userdata *)ud; // 通过"Expat"传递的"userdata"。
lua_State *L = xpu->L;
// 从回调函数"table"中获取"text"事件的回调函数,并入栈。
lua_pushstring(L, "CharacterData"); // key:"CharacterData"
/* callback_table["CharacterData"]。
* "lxp_parse"已将回调函数"table"放在了虚拟栈中索引为3的位置。
*/
lua_gettable(L, 3);
if(lua_isnil(L, -1)) // 未指定此类事件的回调函数。
{
lua_pop(L, 1); // 弹出字符串"CharacterData"。
return; // 直接返回。
}
// 调用回调函数"table"中指定的回调函数。
/* 第一个参数是封装"Expat"解析器的"userdata"。
* "lxp_parse"已将"userdata"放在了虚拟栈中索引为1的位置。
*/
lua_pushvalue(L, 1);
lua_pushlstring(L, s, len); // 第二个参数是解析出来的XML内容。
lua_pushinteger(L, len); // 第三个参数是内容的长度。
lua_call(L, 3, 0); // 调用table["CharacterData"]存储的回调函数,传递3个参数。
}
// "end-element"事件的回调函数。
static void f_EndElement(void *ud, const char *name)
{
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
lua_pushstring(L, "EndElement");
lua_gettable(L, 3);
if(lua_isnil(L, -1))
{
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1); // 第一个参数是封装"Expat"解析器的"userdata"。
/* 第二个参数是"tag"的名称。
* 使用的"lua_pushstring",针对于以'\0'结尾的字符串。
*/
lua_pushstring(L, name);
lua_call(L, 2, 0); // 调用table["EndElement"]存储的回调函数,传递2个参数。
}
// "start-element"事件的回调函数。
static void f_StartElement(void *ud, const char *name, const char **atts)
{
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
lua_pushstring(L, "StartElement");
lua_gettable(L, 3);
if(lua_isnil(L, -1))
{
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1); // 第一个参数是封装"Expat"解析器的"userdata"。
lua_pushstring(L, name); // 第二个参数是"tag"的名称。
// 第三个参数是"tag"的属性,通过"table"的方式传递。
lua_newtable(L); // 创建"table"。
while(*atts) // 逐一的将属性以“属性名-属性值”作为"key-value"对存入"table"中。
{
lua_pushstring(L, *atts++); // 属性的名字。
lua_pushstring(L, *atts++); // 属性的值。
lua_settable(L, -3); // "table[key] = value"。
}
lua_call(L, 3, 0); // 调用table["StartElement"]存储的回调函数,传递2个参数。
}
// 创建以及初始化"Expat"解析器。
static int lxp_make_parser(lua_State *L)
{
XML_Parser p;
lxp_userdata *xpu;
// 创建对解析器进行封装的"userdata"。
xpu = (lxp_userdata *)lua_newuserdata(L, sizeof(lxp_userdata));
// 初始化"userdata"中的变量。
xpu->tableref = LUA_REFNIL;
xpu->parser = NULL;
// 设置"userdata"的"metatable"(全局唯一标识)。
luaL_getmetatable(L, "Expat");
lua_setmetatable(L, -2);
// 创建"Expat"解析器,并存储。
p = xpu->parser = XML_ParserCreate(NULL);
if(!p)
{
luaL_error(L, "XML_ParserCreate failed");
}
// 将回调函数"table"存入"registry"中。
luaL_checktype(L, 1, LUA_TTABLE); // 检查第一个参数是否为回调函数"table"。
// 将回调函数"table"复制一份,再次入栈(因为下面"luaL_ref"会弹出"table")。
lua_pushvalue(L, 1);
/* 将回调函数"table"存入"registry"中,
* 并获得"registry"中的唯一索引,弹出回调函数"table"。
*/
xpu->tableref = luaL_ref(L, LUA_REGISTRYINDEX);
// 注册需要"Expat"传递的"userdata"。
XML_SetUserData(p, xpu);
/* 注册"Expat"三个事件的回调函数。
* 注意,此处并没有直接使用回调函数"table"中的函数,
* 而是使用了固定了函数,固定的函数中再调用回调函数"table"中对应的函数。
* 因为Expat无法直接调用Lua的函数,所以需要这种固定的C函数做中转。
*/
XML_SetElementHandler(p, f_StartElement, f_EndElement);
XML_SetCharacterDataHandler(p, f_CharData);
return 1;
}
// 解析XML。
static int lxp_parse(lua_State *L)
{
int status;
size_t len;
const char *s;
lxp_userdata *xpu;
// 检查第一个参数是否为封装"Expat"解析器的"userdata"。
xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
luaL_argcheck(L, xpu, 1, "expat parser expected");
// 第二个参数为XML字符串,获取并得到其长度。
s = luaL_optlstring(L, 2, NULL, &len);
/* 从"registry"中获取回调函数"table"并入栈。
* 此时回调函数"table"处于虚拟栈中索引3的位置。
* 注册给"Expat"的各事件回调函数将直接从虚拟栈中索引3的位置获取回调函数"table"。
*/
lua_rawgeti(L, LUA_REGISTRYINDEX, xpu->tableref);
xpu->L = L; // 获得Lua的虚拟机。
// 调用"Expat"解析XML。这里如果传递的字符串"s"为"NULL",则代表整个XML解析完成。
status = XML_Parse(xpu->parser, s, (int)len, s == NULL);
/* 将"XML_Parse"的返回值以bool值的形式返回给Lua
* (Lua代码中会使用"assert"接收这个返回值)。
*/
lua_pushboolean(L, status);
return 1;
}
// 释放"Expat"解析器以及封装"Expat"解析器的"userdata"。
static int lxp_close(lua_State *L)
{
lxp_userdata *xpu;
// 检查"userdata"是否合法。
xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
luaL_argcheck(L, xpu, 1, "expat parser expected");
// 释放在"registry"中存储的回调函数"table"。
luaL_unref(L, LUA_REGISTRYINDEX, xpu->tableref);
xpu->tableref = LUA_REFNIL;
// 释放"Expat"解析器。
if(xpu->parser)
{
XML_ParserFree(xpu->parser);
}
xpu->parser = NULL;
return 0;
}
// 隐式提供给外部调用的函数均以"metamethod"的方式提供。
static const struct luaL_Reg lxp_meths[] = {
{"parse", lxp_parse},
{"close", lxp_close},
{"__gc", lxp_close}, // "finalizer"。
{NULL, NULL}
};
// 显式提供给外部调用的函数只有"new"。
static const struct luaL_Reg lxp_funcs[] = {
{"new", lxp_make_parser},
{NULL, NULL}
};
int luaopen_mylib(lua_State *L)
{
luaL_newmetatable(L, "Expat");
/* metatable.__index = metatable */
lua_pushliteral(L, "__index");
lua_pushvalue(L, -2); // 将栈中的"metatable"复制一份儿,再次入栈。
lua_rawset(L, -3);
luaL_setfuncs(L, lxp_meths, 0); // 将"metamethods"都存入"metatable"中。
luaL_newlib(L, lxp_funcs); // 注册显式提供给外部调用的函数。
return 1;
}
将”mylib.c”编译为动态连接库。注意,这里需要链接上”Expat”库,
prompt> gcc mylib.c -fPIC -shared -o mylib.so -lexpat -Wall
prompt> ls
mylib.c mylib.so a.lua
“a.lua”文件中:
local mylxp = require "mylib"
-- 以下的这组回调函数,实现了将XML中的内容以树状图层级的形式打印,可打印"tag"的属性。
local count = 0
local callbacks = {
-- "start-element"事件的回调函数。
StartElement = function (parser, tagname, atts)
io.write("+ ", string.rep(" ", count), tagname) -- "tag"的名称。
if atts then -- "tag"的属性。
io.write("[")
for k, v in pairs(atts) do
io.write(k, "=", v, ";")
end
io.write("]")
end
io.write("\n")
count = count + 1
end,
-- "text"事件的回调函数。
CharacterData = function (parser, s, len)
if "\n" ~= s then -- 忽略"tag"内容中的换行符。
io.write("* ", string.rep(" ", count), s, "\n")
end
end,
-- "end-element"事件的回调函数。
EndElement = function (parser, tagname)
count = count - 1
io.write("- ", string.rep(" ", count), tagname, "\n")
end,
}
-- XML。
local t = {
[[<?xml version="1.0" encoding="utf-8" standalone="yes"?>]],
[[<hello attr1="123">]],
[[<num1>ldskfj</num1>]],
[[<num2>dslfjsdlfj</num2>]],
[[</hello>]],
}
p = mylxp.new(callbacks) -- 创建"Expat"解析器。
for k, v in pairs(t) do -- 逐一的解析XML。
assert(p:parse(v)) -- 解析每一段XML。
assert(p:parse("\n"))
end
assert(p:parse()) -- 告知"Expat"解析器整个XML解析完成(传递空的字符串)。
p:close() -- 释放"Expat"解析器。
--[[ results:
+ hello[attr1=123;attr2=456;]
+ num1[]
* ldskfj
- num1
+ num2[]
* dslfjsdlfj
- num2
- hello
]]
附加:
1、如何安装”Expat”?
(1) 从 https://sourceforge.net/projects/expat/ 下载”Expat”。
(2) tar xvf expat-2.1.1.tar.bz2
解压下载下来的文件,会得到一个”expat-2.1.1”目录。
(3) 在”expat-2.1.1”目录执行cmake .
创建”Makefile”。
(4) make
编译”Expat”源码。
(5) make install
安装”Expat”。
(6) “Expat”所用到的动态连接库(.so 文件)会被存放到”/usr/local/lib/”中,查看此路径是否在$LD_LIBRARY_PATH
中,如果不在,则手动添加。
prompt> echo $LD_LIBRARY_PATH
/usr/lib32
prompt> export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/
/usr/lib32:/usr/local/lib/