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/