快速掌握Lua 5.3 —— 资源管理
2016-05-22 21:21
337 查看
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/
相关文章推荐
- 详解Lua中的表的概念及其相关操作方法
- Lua编程示例(二):面向对象、metatable对表进行扩展
- 把Lua编译进nginx步骤方法
- Lua脚本自动生成APK包
- Lua中的元表(metatable)、元方法(metamethod)详解
- Lua中的metatable介绍
- Lua中ipair和pair的区别
- Lua中的函数精讲笔记
- 浅谈Lua的面向对象特性
- 详解Lua中的变量相关知识点
- Lua脚本语言入门笔记
- Lua脚本调用外部脚本
- 详解Lua中的if语句的使用方法
- Lua中调用函数使用点号和冒号的区别
- Lua中的闭合函数、非全局函数与函数的尾调用详解
- Lua中强大的元方法__index详解
- Lua中调用C++函数示例
- Lua面向对象之类和继承浅析
- Lua性能优化技巧(一):前言
- Lua中获取table长度问题探讨