在前面一章介紹的數組實現方法,我們不必擔心如何管理資源,只需要分配內存。 每一個表示數組的 userdatum 都有自己的內存,這個內存由 Lua 管理。當數組變為垃圾 (也就是說,當程序不需要)的時候,Lua 會自動收集並釋放內存。
生活總是不那麼如意。有時候,一個對象除了需要物理內存以外,還需要文件描述符、窗口句柄等類似的資源。(通常這些資源也是內存,但由系統的其他部分來管理)。 在這種情況下,當一個對象成為垃圾並被收集的時候,這些相關的資源也應該被釋放。一些面向對象的語言為了這種需要提供了一種特殊的機制(稱為 finalizer或者析構器)。Lua以_gc 元方法的方式提供了 finalizers。這個元方法只對 userdata 類型的值有效。當一個userdatum 將被收集的時候,並且usedatum 有一個_gc 域,Lua 會調用這個域的值 (應該是一個函數):以userdatum 作為這個函數的參數調用。這個函數負責釋放與 userdatum 相關的所有資源。
為了闡明如何將這個元方法和 API作為一個整體使用,這一章我們將使用 Lua擴展應用的方式,介紹兩個例子。第一個例子是前面己經介紹的遍歷一個目錄的函數的另一種實現。第二個例子是一個綁定 ExpatCExpat 開源的 XML 解析器)實現的 XML 解析 器。
29.1 目錄迭代器
前面我們實現了一個 dir 函數,給定一個目錄作為參數,這個函數以一個table的方 式返回目錄下所有文件。我們新版本的dir函數將返回一個迭代子,每次調用這個迭代 子的時候會返回目錄中的一個入口(entry)。按新版本的實現方式,我們可以使用循環 來遍歷整個目錄:
for fname in dir(".") do print(fname) end
在 C 語言中,我們需要 DIR這種結構才能夠迭代一個目錄。通過 opendir 才能創建 一個 DIR 的實例,並且必須顯式的調用 closedir 來釋放資源。我們以前實現的 dir 用一個 本地變量保存 DIR 的實例,並且在獲取目錄中最後一個文件名之後關閉實例。但我們新實現的 dir中不能在本地變量中保存 DIR 的實例,因為有很多個調用都要訪問這個值,另外,也不能僅僅在獲取目錄中最後一個文件名之後關閉目錄。如果程序循環過程中中斷退出,迭代子根本就不會取得最後一個文件名,所以,為了保證 DIR 的實例一定能夠被釋放掉,我們將它的地址保存在一個 userdatum 中,並使用這個 userdatum 的 gc的 元方法來釋放目錄結構。
盡管我們實現中userdatum的作用很重要,但這個用來表示一個目錄的userdatum,並不需要在Lua可見范圍之內。Dir函數返回一個迭代子函數,迭代子函數需要在Lua的可見 范圍之內。目錄可能是迭代子函數的一個upvalue。這樣一來,迭代子函數就可以直接訪問這個結構(指目錄結構,即userdatum),但是Lua不可以(也不需要)訪問這個結構。
總的來說,我們需要三個 C 函數。第一,dir 函數,一個 Lua 調用他產生迭代器的 工廠,這個函數必須打開 DIR結構並將他作為迭代函數的 upvalue。第二,我們需要一 個迭代函數。第三,_gc 元方法,負責關閉 DIR 結構。一般來說,我們還需要一個額外的函數來進行一些初始的操作,比如為目錄創建 metatable,並初始化這個 metatable。
首先看我們的 dir 函數:
#include#include /* forward declaration for the iteratorfunction */ static int dir_iter (lua_State *L); static int l_dir (lua_State *L) { const char *path = luaL_checkstring(L, 1); /* create auserdatum to storea DIR address */ DIR **d = (DIR**)lua_newuserdata(L, sizeof(DIR *)); /* set its metatable */ luaL_getmetatable(L, "LuaBook.dir"); ua_setmetatable(L, -2); /* try toopen the givendirectory */ *d =opendir(path); if (*d == NULL)/* error openingthe directory? */ luaL_error(L, "cannot open %s: %s", path, strerror(errno)); /* createsand returns theiterator function (its sole upvalue,the directory userdatum, is already on the stacktop */ lua_pushcclosure(L, dir_iter, 1); return 1; }
這兒有一點需要注意的,我們必須在打開目錄之前創建 userdatum。如果我們先打開 目錄,然後調用 lua_newuserdata 會拋出錯誤,這樣我們就無法獲取DIR 結構。按照正確 的順序,DIR結構一旦被創建,就會立刻和 userdatum 關聯起來;之後不管發生什麼,_gc元方法都會自動的釋放這個結構。
第二個函數是迭代器:
static int dir_iter (lua_State *L) { 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; } else return 0; /* no morevalues to return*/ }
gc 元方法用來關閉目錄,但有一點需要小心:因為我們在打開目錄之前創建 userdatum,所以不管 opendir 的結果是什麼userdatum 將來都會被收集。如果 opendir 失敗,將來就沒有什麼可以關閉的了:
static int dir_gc (lua_State *L) { DIR *d = *(DIR**)lua_touserdata(L, 1); if (d) closedir(d); return 0; }
最後一個函數打開這個只有一個函數的庫:
int luaopen_dir (lua_State *L) { luaL_newmetatable(L, "LuaBook.dir"); /* set its gcfield */ lua_pushstring(L, "_gc"); lua_pushcfunction(L, dir_gc); lua_settable(L,-3); /* register the dirfunction */ lua_pushcfunction(L, l_dir); lua_setglobal(L, "dir"); return 0; }
整個例子有一個注意點。開始的時候,dir_gc 看起來應該檢查他的參數是否是一個目錄。否則,一個惡意的使用者可能用其他類型的參數(比如,文件)調用這個函數導 致嚴重的後果。然而,在 Lua 程序中無法訪問這個函數:他被存放在目錄的 metatable 中,Lua 程序從來不會訪問這些目錄。