Lua與C API交互全面解析(史上最全的整理)

C API 縱覽

Lua 是一個嵌入式的語言,意味着 Lua 不僅可以是一個獨立運行的程序包也可以是一個用來嵌入其他應用的程序庫。你可能覺得奇怪:如果 Lua 不只是獨立的程序,爲什麼到目前爲止貫穿整本書我們都是在使用 Lua 獨立程序呢?這個問題的答案在於 Lua 解釋器(可執行的 lua)。Lua 解釋器是一個使用 Lua 標準庫實現的獨立的解釋器,她是一個很小的應用(總共不超過 500 行的代碼)。解釋器負責程序和使用者的接口:從使用者那裏獲取文件或者字符串,並傳給 Lua 標準庫,Lua 標準庫負責最終的代碼運行。

Lua 可以作爲程序庫用來擴展應用的功能,也就是 Lua 可以作爲擴展性語言的原因所在。同時,Lua 程序中可以註冊有其他語言實現的函數,這些函數可能由 C 語言(或其他語言)實現,可以增加一些不容易由 Lua 實現的功能。這使得 Lua 是可擴展的。與上面兩種觀點(Lua 作爲擴展性語言和可擴展的語言)對應的 C 和 Lua 中間有兩種交互方式。
第一種,C 作爲應用程序語言,Lua 作爲一個庫使用;
第二種,反過來,Lua 作爲程序語言,C 作爲庫使用。這兩種方式,C 語言都使用相同的 API 與 Lua 通信,因此 C 和Lua 交互這部分稱爲 C API。

C API 是一個 C 代碼與 Lua 進行交互的函數集。他有以下部分組成:讀寫 Lua 全局變量的函數,調用 Lua 函數的函數,運行 Lua 代碼片斷的函數,註冊 C 函數然後可以在Lua 中被調用的函數,等等。(本書中,術語函數實際上指函數或者宏,API 有些函數爲了方便以宏的方式實現)

C API 遵循 C 語言的語法形式,這 Lua 有所不同。當使用 C 進行程序設計的時候,我們必須注意,類型檢查,錯誤處理,內存分配都很多問題。API 中的大部分函數並不檢查他們參數的正確性;你需要在調用函數之前負責確保參數是有效的。如果你傳遞了錯誤的參數,可能得到 “segmentation fault” 這樣或者類似的錯誤信息,而沒有很明確的錯誤信息可以獲得。另外,API 重點放在了靈活性和簡潔性方面,有時候以犧牲方便實用爲代價的。一般的任務可能需要涉及很多個 API 調用,這可能令人煩惱,但是他給你提供了對細節的全部控制的能力,比如錯誤處理,緩衝大小,和類似的問題。如本章的標題所示,這一章的目標是對當你從 C 調用 Lua 時將涉及到哪些內容的預覽。如果不能理解某些細節不要着急,後面我們會一一詳細介紹。不過,在 Lua 參考手冊中有對指定函數的詳細描述。另外,在 Lua 發佈版中你可以看到 API 的應用的例子,Lua 獨立的解釋器(lua.c)提供了應用代碼的例子,而標準庫(lmathlib.c、lstrlib.c 等等)提供了程
序庫代碼的例子。

從現在開始,你戴上了 C 程序員的帽子,當我們談到“你/你們”,我們意思是指當你使用 C 編程的時候。在 C 和 Lua 之間通信關鍵內容在於一個虛擬的棧。幾乎所有的API 調用都是對棧上的值進行操作,所有 C 與 Lua 之間的數據交換也都通過這個棧來完成。另外,你也可以使用棧來保存臨時變量。棧的使用解決了 C 和 Lua 之間兩個不協調的問題:
第一,Lua 會自動進行垃圾收集,而 C 要求顯示的分配存儲單元,兩者引起的矛盾。
第二,Lua 中的動態類型和 C 中的靜態類型不一致引起的混亂。我們將在 24.2 節詳細地介紹棧的相關內容。

第一個示例程序

通過一個簡單的應用程序讓我們開始這個預覽:一個獨立的 Lua 解釋器的實現。我們寫一個簡單的解釋器,代碼如下:

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main (void)
{
char buff[256];
int error;
 lua_State *L = lua_open(); /* opens Lua */
 luaopen_base(L); /* opens the basic library */
 luaopen_table(L); /* opens the table library */
 luaopen_io(L); /* opens the I/O library */
 luaopen_string(L); /* opens the string lib. */
 luaopen_math(L); /* opens the math lib. */
while (fgets(buff, sizeof(buff), stdin) != NULL) {
 error = luaL_loadbuffer(L, buff, strlen(buff),
 "line") || lua_pcall(L, 0, 0, 0);
 if (error) {
 fprintf(stderr, "%s", lua_tostring(L, -1));
 lua_pop(L, 1);/* pop error message from the stack */
 }
 }
 lua_close(L);
return 0;
}

頭文件 lua.h 定義了 Lua 提供的基礎函數。其中包括創建一個新的 Lua 環境的函數(如 lua_open),調用 Lua 函數(如 lua_pcall)的函數,讀取/寫入 Lua 環境的全局變量的函數,註冊可以被 Lua 代碼調用的新函數的函數,等等。所有在 lua.h 中被定義的都有一個 lua_前綴。

頭文件 lauxlib.h 定義了輔助庫(auxlib)提供的函數。同樣,所有在其中定義的函數等都以 luaL_打頭(例如,luaL_loadbuffer)。輔助庫利用 lua.h 中提供的基礎函數提供了更高層次上的抽象;所有 Lua 標準庫都使用了 auxlib。基礎 API 致力於 economy and orthogonality,相反 auxlib 致力於實現一般任務的實用性。當然,基於你的程序的需要而創建其它的抽象也是非常容易的。需要銘記在心的是,auxlib 沒有存取 Lua 內部的權限。它完成它所有的工作都是通過正式的基本 API。

Lua 庫沒有定義任何全局變量。它所有的狀態保存在動態結構 lua_State 中,而且指向這個結構的指針作爲所有 Lua 函數的一個參數。這樣的實現方式使得 Lua 能夠重入(reentrant)且爲在多線程中的使用作好準備。

函數 lua_open 創建一個新環境(或 state)。lua_open 創建一個新的環境時,這個環境並不包括預定義的函數,甚至是 print。爲了保持 Lua 的苗條,所有的標準庫以單獨的包提供,所以如果你不需要就不會強求你使用它們。頭文件 lualib.h 定義了打開這些庫的函數。例如,調用 luaopen_io,以創建 io table 並註冊 I/O 函數(io.read,io.write 等等)到 Lua 環境中。

創建一個 state 並將標準庫載入之後,就可以着手解釋用戶的輸入了。對於用戶輸入的每一行,C 程序首先調用 luaL_loadbuffer 編譯這些 Lua 代碼。如果沒有錯誤,這個調用返回零並把編譯之後的 chunk 壓入棧。(記住,我們將在下一節中討論魔法般的棧)之後,C 程序調用 lua_pcall,它將會把 chunk 從棧中彈出並在保護模式下運行它。和luaL_laodbuffer 一樣,lua_pcall 在沒有錯誤的情況下返回零。在有錯誤的情況下,這兩個函數都將一條錯誤消息壓入棧;我們可以用 lua_tostring 來得到這條信息、輸出它,用lua_pop 將它從棧中刪除。

注意,在有錯誤發生的情況下,這個程序簡單的輸出錯誤信息到標準錯誤流。在 C中,實際的錯誤處理可能是非常複雜的而且如何處理依賴於應用程序本身。Lua 核心決不會直接輸出任何東西到任務輸出流上;它通過返回錯誤代碼和錯誤信息來發出錯誤信號。每一個應用程序都可以用最適合它們自己的方式來處理這些錯誤。爲了討論的簡單,現在我們假想一個簡單的錯誤處理方式,就象下面代碼一樣,它只是輸出一條錯誤信息、關閉 Lua state、退出整個應用程序。

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void error (lua_State *L, const char *fmt, ...) {
 va_list argp;
 va_start(argp, fmt);
 vfprintf(stderr, argp);
 va_end(argp); 
 lua_close(L);
 exit(EXIT_FAILURE);
}

稍候我們再詳細的討論關於在應用代碼中如何處理錯誤.因爲你可以將Lua和C/C++代碼一起編譯,lua.h 並不包含這些典型的在其他 C 庫中出現的整合代碼:

#ifdef __cplusplus
extern "C" {
#endif
 ...
#ifdef __cplusplus
}
#endif

因此,如果你用 C 方式來編譯它,但用在 C++中,那麼你需要象下面這樣來包含 lua.h頭文件。

extern "C" {
#include <lua.h>
}

一個常用的技巧是建立一個包含上面代碼的 lua.hpp 頭文件,並將這個新的頭文件包含進你的 C++程序。

堆棧

當在 Lua 和 C 之間交換數據時我們面臨着兩個問題:動態與靜態類型系統的不匹配和自動與手動內存管理的不一致。

在 Lua 中,我們寫下 a[k]=v 時,k 和 v 可以有幾種不同的類型(由於 metatables 的存在,a 也可能有不同的類型)。如果我們想在 C 中提供類似的操作,無論怎樣,操作表的函數(settable)必定有一個固定的類型。我們將需要幾十個不同的函數來完成這一個的操作(三個參數的類型的每一種組合都需要一個函數)。

我們可以在 C 中聲明一些 union 類型來解決這個問題,我們稱之爲 lua_Value,它能夠描述所有類型的 Lua 值。然後,我們就可以這樣聲明 settable void lua_settable (lua_Value a, lua_Value k, lua_Value v);

這個解決方案有兩個缺點。
第一,要將如此複雜的類型映射到其它語言可能很困難;Lua 不僅被設計爲與 C/C++易於交互,Java,Fortran 以及類似的語言也一樣。
第二,Lua負責垃圾回收:如果我們將 Lua 值保存在 C 變量中,Lua 引擎沒有辦法瞭解這種用法;它可能錯誤地認爲某個值爲垃圾並收集他。

因此,Lua API 沒有定義任何類似 lua_Value 的類型。替代的方案,它用一個抽象的棧在 Lua 與 C 之間交換值。棧中的每一條記錄都可以保存任何 Lua 值。無論你何時想要從 Lua 請求一個值(比如一個全局變量的值),調用 Lua,被請求的值將會被壓入棧。無論你何時想要傳遞一個值給 Lua,首先將這個值壓入棧,然後調用 Lua(這個值將被彈出)。我們仍然需要一個不同的函數將每種 C 類型壓入棧和一個不同函數從棧上取值(譯註:只是取出不是彈出),但是我們避免了組合式的爆炸(combinatorial explosion)。另外,因爲棧是由 Lua 來管理的,垃圾回收器知道那個值正在被 C 使用。

幾乎所有的 API函數都用到了棧。正如我們在第一個例子中所看到的,luaL_loadbuffer 把它的結果留在了棧上(被編譯的 chunk 或一條錯誤信息);lua_pcall 從棧上獲取要被調用的函數並把任何臨時的錯誤信息放在這裏。

Lua 以一個嚴格的 LIFO 規則(後進先出;也就是說,始終存取棧頂)來操作棧。當你調用 Lua 時,它只會改變棧頂部分。你的C代碼卻有更多的自由;更明確的來講,你可以查詢棧上的任何元素,甚至是在任何一個位置插入和刪除元素。

壓入元素

API 有一系列壓棧的函數,它將每種可以用 C 來描述的 Lua 類型壓棧:空值(nil)用 lua_pushnil,數值型(double)用 lua_pushnumber,布爾型(在 C 中用整數表示)用lua_pushboolean,任意的字符串(char類型,允許包含’\0’字符)用 lua_pushlstring,C語言風格(以’\0’結束)的字符串(const char)用 lua_pushstring:

void lua_pushnil (lua_State *L);
void lua_pushboolean (lua_State *L, int bool);
void lua_pushnumber (lua_State *L, double n);
void lua_pushlstring (lua_State *L, const char *s,
 size_t length);
void lua_pushstring (lua_State *L, const char *s);

同樣也有將 C 函數和 userdata 值壓入棧的函數,稍後會討論到它們。

Lua 中的字符串不是以零爲結束符的;它們依賴於一個明確的長度,因此可以包含任意的二進制數據。將字符串壓入串的正式函數是 lua_pushlstring,它要求一個明確的長度作爲參數。對於以零結束的字符串,你可以用 lua_pushstring(它用 strlen 來計算字符串長度)。Lua 從來不保持一個指向外部字符串(或任何其它對象,除了 C 函數——它總是靜態指針)的指針。對於它保持的所有字符串,Lua 要麼做一份內部的拷貝要麼重新利用已經存在的字符串。因此,一旦這些函數返回之後你可以自由的修改或是釋放你的緩衝區。

無論你何時壓入一個元素到棧上,你有責任確保在棧上有空間來做這件事情。記住,你現在是 C 程序員;Lua 不會寵着你。當 Lua 在起始以及在 Lua 調用 C 的時候,棧上至少有 20 個空閒的記錄(lua.h 中的 LUA_MINSTACK 宏定義了這個常量)。對於多數普通的用法棧是足夠的,所以通常我們不必去考慮它。無論如何,有些任務或許需要更多的棧空間(如,調用一個不定參數數目的函數)。在這種情況下,或許你需要調用下面這個函數:

int lua_checkstack (lua_State *L, int sz);

它檢測棧上是否有足夠你需要的空間(稍後會有關於它更多的信息)。

查詢元素

API 用索引來訪問棧中的元素。在棧中的第一個元素(也就是第一個被壓入棧的)有索引 1,下一個有索引 2,以此類推。我們也可以用棧頂作爲參照來存取元素,利用負索引。在這種情況下,-1 指出棧頂元素(也就是最後被壓入的),-2 指出它的前一個元素,以此類推。例如,調用 lua_tostring(L, -1)以字符串的形式返回棧頂的值。我們下面將看到,在某些場合使用正索引訪問棧比較方便,另外一些情況下,使用負索引訪問棧更方便。

API 提供了一套 lua_is*函數來檢查一個元素是否是一個指定的類型,*可以是任何Lua 類型。因此有 lua_isnumber,lua_isstring,lua_istable 以及類似的函數。所有這些函數都有同樣的原型:

int lua_is... (lua_State *L, int index);

lua_isnumber 和 lua_isstring 函數不檢查這個值是否是指定的類型,而是看它是否能被轉換成指定的那種類型。例如,任何數字類型都滿足 lua_isstring。

還有一個 lua_type 函數,它返回棧中元素的類型。(lua_is中的有些函數實際上是用了這個函數定義的宏)在 lua.h 頭文件中,每種類型都被定義爲一個常量:LUA_TNIL、LUA_TBOOLEAN 、 LUA_TNUMBER 、 LUA_TSTRING 、 LUA_TTABLE 、LUA_TFUNCTION、LUA_TUSERDATA 以及 LUA_TTHREAD。這個函數主要被用在與一個 switch 語句聯合使用。當我們需要真正的檢查字符串和數字類型時它也是有用的爲了從棧中獲得值,這裏有 lua_to函數:

int lua_toboolean (lua_State *L, int index);
double lua_tonumber (lua_State *L, int index);
const char * lua_tostring (lua_State *L, int index);
size_t lua_strlen (lua_State *L, int index);

即使給定的元素的類型不正確,調用上面這些函數也沒有什麼問題。在這種情況下,lua_toboolean、lua_tonumber 和 lua_strlen 返回 0,其他函數返回 NULL。由於 ANSI C 沒有提供有效的可以用來判斷錯誤發生數字值,所以返回的 0 是沒有什麼用處的。對於其他函數而言,我們一般不需要使用對應的 lua_is函數:我們只需要調用 lua_is,測試返回結果是否爲 NULL 即可。

Lua_tostring 函數返回一個指向字符串的內部拷貝的指針。你不能修改它(使你想起那裏有一個 const)。只要這個指針對應的值還在棧內,Lua 會保證這個指針一直有效。

當一個 C 函數返回後,Lua 會清理他的棧,所以,有一個原則:永遠不要將指向 Lua 字符串的指針保存到訪問他們的外部函數中。

Lua_string 返回的字符串結尾總會有一個字符結束標誌 0,但是字符串中間也可能包含 0,lua_strlen 返回字符串的實際長度。特殊情況下,假定棧頂的值是一個字符串,下面的斷言(assert)總是有效的:

const char *s = lua_tostring(L, -1); /* any Lua string */
size_t l = lua_strlen(L, -1); /* its length */
assert(s[l] == '\0');
assert(strlen(s) <= l);

其他堆棧操作

除開上面所提及的 C 與堆棧交換值的函數外,API 也提供了下列函數來完成通常的堆棧維護工作:

int lua_gettop (lua_State *L);
void lua_settop (lua_State *L, int index);
void lua_pushvalue (lua_State *L, int index);
void lua_remove (lua_State *L, int index);
void lua_insert (lua_State *L, int index);
void lua_replace (lua_State *L, int index);

函數 lua_gettop 返回堆棧中的元素個數,它也是棧頂元素的索引。注意一個負數索引-x 對應於正數索引 gettop-x+1。lua_settop 設置棧頂(也就是堆棧中的元素個數)爲一個指定的值。如果開始的棧頂高於新的棧頂,頂部的值被丟棄。否則,爲了得到指定的大小這個函數壓入相應個數的空值(nil)到棧上。特別的,lua_settop(L,0)清空堆棧。你也可以用負數索引作爲調用 lua_settop 的參數;那將會設置棧頂到指定的索引。利用這種技巧,API 提供了下面這個宏,它從堆棧中彈出 n 個元素:

#define lua_pop(L,n) lua_settop(L, -(n)-1)

函數 lua_pushvalue 壓入堆棧上指定索引的一個摶貝到棧頂;lua_remove 移除指定索引位置的元素,並將其上面所有的元素下移來填補這個位置的空白;lua_insert 移動棧頂元素到指定索引的位置,並將這個索引位置上面的元素全部上移至棧頂被移動留下的空隔;最後,lua_replace 從棧頂彈出元素值並將其設置到指定索引位置,沒有任何移動操作。注意到下面的操作對堆棧沒有任何影響:

lua_settop(L, -1); /* set top to its current value */
lua_insert(L, -1); /* move top element to the top */

爲了說明這些函數的用法,這裏有一個有用的幫助函數,它 dump 整個堆棧的內容:

static void stackDump (lua_State *L) { 
int i;
int top = lua_gettop(L);
for (i = 1; i <= top; i++) { /* repeat for each level */
 int t = lua_type(L, i);
 switch (t) {
 case LUA_TSTRING: /* strings */
 printf("`%s'", lua_tostring(L, i));
 break;
 case LUA_TBOOLEAN: /* booleans */
 printf(lua_toboolean(L, i) ? "true" : "false");
 break;
 case LUA_TNUMBER: /* numbers */
 printf("%g", lua_tonumber(L, i));
 break;

 default: /* other values */
 printf("%s", lua_typename(L, t));
 break;
 }
 printf(" "); /* put a separator */
 }
 printf("\n"); /* end the listing */
}

這個函數從棧底到棧頂遍歷了整個堆棧,依照每個元素自己的類型打印出其值。它用引號輸出字符串;以%g 的格式輸出數字;對於其它值(table,函數,等等)它僅僅輸出它們的類型(lua_typename 轉換一個類型碼到類型名)。

下面的函數利用 stackDump 更進一步的說明了 API 堆棧的操作。

#include <stdio.h>
#include <lua.h>
static void stackDump (lua_State *L) {
 ...
} 
int main (void) {
 lua_State *L = lua_open();
 lua_pushboolean(L, 1); lua_pushnumber(L, 10);
 lua_pushnil(L); lua_pushstring(L, "hello");
 stackDump(L);
 /* true 10 nil `hello' */
 lua_pushvalue(L, -4); stackDump(L);
 /* true 10 nil `hello' true */
 lua_replace(L, 3); stackDump(L);
 /* true 10 true `hello' */
 lua_settop(L, 6); stackDump(L);
 /* true 10 true `hello' nil nil */
 lua_remove(L, -3); stackDump(L);
 /* true 10 true nil nil */
 lua_settop(L, -5); stackDump(L);
 /* true */
 lua_close(L);
return 0;
}

C API 的錯誤處理

不象 C++或者 JAVA 一樣,C 語言沒有提供一種異常處理機制。爲了改善這個難處,Lua 利用 C 的 setjmp 技巧構造了一個類似異常處理的機制。(如果你用 C++來編譯 Lua,那麼修改代碼以使用真正的異常並不困難。)

Lua 中的所有結構都是動態的:它們按需增長,最終當可能時又會縮減。意味着內存分配失敗的可能性在 Lua 中是普遍的。幾乎任意操作都會面對這種意外。Lua 的 API中用異常發出這些錯誤而不是爲每步操作產生錯誤碼。這意味着所有的 API 函數可能拋出一個錯誤(也就是調用 longjmp)來代替返回。

當我們寫一個庫代碼時(也就是被 Lua 調用的 C 函數)長跳轉(long jump)的用處幾乎和一個真正的異常處理一樣的方便,因爲 Lua 抓取了任務偶然的錯誤。當我們寫應用程序代碼時(也就是調用 Lua 的 C 代碼),無論如何,我們必須提供一種方法來抓取這些錯誤。

應用程序中的錯誤處理

典型的情況是應用的代碼運行在非保護模式下。由於應用的代碼不是被 Lua 調用的,Lua 根據上下文情況來捕捉錯誤的發生(也就是說,Lua 不能調用 setjmp)。在這些情況下,當 Lua 遇到像 “not enough memory” 的錯誤,他不知道如何處理。他只能調用一個panic 函數退出應用。(你可以使用 lua_atpanic 函數設置你自己的 panic 函數)

不是所有的 API 函數都會拋出異常,lua_open、lua_close、lua_pcall 和 lua_load 都是安全的,另外,大多數其他函數只能在內存分配失敗的情況下拋出異常:比如,luaL_loadfile 如果沒有足夠內存來拷貝指定的文件將會失敗。有些程序當碰到內存不足時,他們可能需要忽略異常不做任何處理。對這些程序而言,如果 Lua 導致內存不足,panic 是沒有問題的。

如果你不想你的應用退出,即使在內存分配失敗的情況下,你必須在保護模式下運行你的代碼。大部分或者所有你的 Lua 代碼通過調用 lua_pcall 來運行,所以,它運行在保護模式下。即使在內存分配失敗的情況下,lua_pcall 也返回一個錯誤代碼,使得 lua解釋器處於和諧的(consistent)狀態。如果你也想保護所有你的與 Lua 交互的 C 代碼,你可以使用 lua_cpcall。(請看參考手冊,有對這個函數更深的描述,在 Lua 的發佈版的lua.c 文件中有它應用的例子)

類庫中的錯誤處理

Lua 是安全的語言,也就是說,不管你些什麼樣的代碼,也不管代碼如何錯誤,你都可以根據 Lua 本身知道程序的行爲。另外,錯誤也會根據 Lua 被發現和解釋。你可以與 C 比較一下,C 語言中很多錯誤的程序的行爲只能依據硬件或者由程序計數器給出的錯誤出現的位置被解釋。

不論什麼時候你向 Lua 中添加一個新的 C 函數,你都可能打破原來的安全性。比如,一個類似 poke 的函數,在任意的內存地址存放任意的字節,可能使得內存癱瘓。你必須想法設法保證你的插件(add-ons)對於 Lua 來講是安全的,並且提高比較好的錯誤處理。

正如我們前面所討論的,每一個 C 程序都有他自己的錯勿處理方式,當你打算爲Lua 寫一個庫函數的時候,這裏有一些標準的處理錯誤的方法可以參考。不論什麼時候,C 函數發現錯誤只要簡單的調用 lua_error(或者 luaL_error,後者更好,因爲她調用了前者並格式化了錯誤信息)。Lua_error 函數會清理所有在 Lua 中需要被清理的,然後和錯誤信息一起回到最初的執行 lua_pcall 的地方。

擴展你的程序

作爲配置語言是 LUA 的一個重要應用。在這個章節裏,我們舉例說明如何用 LUA 設置一個程序。讓我們用一個簡單的例子開始然後展開到更復雜的應用中。

首先,讓我們想象一下一個簡單的配置情節:你的 C 程序(程序名爲 PP)有一個窗口界面並且可以讓用戶指定窗口的初始大小。顯然,類似這樣簡單的應用,有多種解決方法比使用 LUA 更簡單,比如環境變量或者存有變量值的文件。但,即使是用一個簡單的文本文件,你也不知道如何去解析。所以,最後決定採用一個 LUA 配置文件(這就是 LUA 程序中的純文本文件)。在這種簡單的文本形式中通常包含類似如下的信息行:

-- configuration file for program `pp' 
-- define window size 
width = 200 
height = 300 

現在,你得調用 LUA API 函數去解析這個文件,取得 width 和 height 這兩個全局變量的值。下面這個取值函數就起這樣的作用:

#include <lua.h> 
#include <lauxlib.h> 
#include <lualib.h> 
void load (char *filename, int *width, int *height) { 
 lua_State *L = lua_open(); 
 luaopen_base(L); 
 luaopen_io(L); 
 luaopen_string(L); 
 luaopen_math(L); 
if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0)) 
 error(L, "cannot run configuration file: %s",  lua_tostring(L, -1)); 
 lua_getglobal(L, "width"); 
 lua_getglobal(L, "height"); 
if (!lua_isnumber(L, -2)) 
 error(L, "`width' should be a number\n"); 
if (!lua_isnumber(L, -1)) 
 error(L, "`height' should be a number\n"); 
 *width = (int)lua_tonumber(L, -2); 
 *height = (int)lua_tonumber(L, -1); 
 lua_close(L); 
} 

首先,程序打開 LUA 包並加載了標準函數庫(雖然這是可選的,但通常包含這些庫是比較好的編程思想)。然後程序使用 luaL_loadfile 方法根據參數 filename 加載此文件中的信息塊並調用 lua_pcall 函數運行,這些函數運行時若發生錯誤(例如配置文件中有語法錯誤),將返回非零的錯誤代碼並將此錯誤信息壓入棧中。通常,我們用帶參數 index值爲-1 的 lua_tostring 函數取得棧頂元素(error 函數我們已經在 24.1 章節中定義)。

解析完取得的信息塊後,程序會取得全局變量值。爲此,程序調用了兩次lua_getglobal 函數,其中一參數爲變量名稱。每調用一次就把相應的變量值壓入棧頂,所以變量 width 的 index 值是-2 而變量 height 的 index 值是-1(在棧頂)。(因爲先前的棧是空的,需要從棧底重新索引,1 表示第一個元素 2 表示第二個元素。由於從棧頂索引,不管棧是否爲空,你的代碼也能運行)。接着,程序用 lua_isnumber 函數判斷每個值是否爲數字。lua_tonumber 函數將得到的數值轉換成 double 類型並用(int)強制轉換成整型。最後,關閉數據流並返回值。

Lua 是否值得一用?正如我前面提到的,在這個簡單的例子中,相比較於 lua 用一個只包含有兩個數字的文件會更簡單。即使如此,使用 lua 也帶來了一些優勢。首先,它爲你處理所有的語法細節(包括錯誤);你的配置文件甚至可以包含註釋!其次,用可以用 lua 做更多複雜的配置。例如,腳本可以向用戶提示相關信息,或者也可以查詢環境變量以選擇合適的大小:

-- configuration file for program 'pp' 
if getenv("DISPLAY") == ":0.0" then
 width = 300; height = 300 
else 
 width = 200; height = 200 
end 

在這樣簡單的配置情節中,很難預料用戶想要什麼;不過只要腳本定義了這兩個變量,你的 C 程序無需改變就可運行。

最後一個使用 lua 的理由:在你的程序中很容易的加入新的配置單元。方便的屬性添加使程序更具有擴展性。

表操作

現在,我們打算使用 Lua 作爲配置文件,配置窗口的背景顏色。我們假定最終的顏色有三個數字(RGB)描述,每一個數字代表顏色的一部分。通常,在 C 語言中,這些數字使用[0,255]範圍內的整數表示,由於在 Lua 中所有數字都是實數,我們可以使用更自然的範圍[0,1]來表示。

一個粗糙的解決方法是,對每一個顏色組件使用一個全局變量表示,讓用戶來配置這些變量:

-- configuration file for program 'pp' 
width = 200 
height = 300 
background_red = 0.30 
background_green = 0.10 
background_blue = 0 

這個方法有兩個缺點:第一,太冗餘(爲了表示窗口的背景,窗口的前景,菜單的背景等,一個實際的應用程序可能需要幾十個不同的顏色);第二,沒有辦法預定義共同部分的顏色,比如,假如我們事先定義了WHITE,用戶可以簡單的寫background = WHITE來表示所有的背景色爲白色。爲了避免這些缺點,我們使用一個 table 來表示顏色:

background = {r=0.30, g=0.10, b=0} 

表的使用給腳本的結構帶來很多靈活性,現在對於用戶(或者應用程序)很容易預定義一些顏色,以便將來在配置中使用:

BLUE = {r=0, g=0, b=1} 
... 
background = BLUE 

爲了在 C 中獲取這些值,我們這樣做:

lua_getglobal(L, "background"); 
if (!lua_istable(L, -1)) 
 error(L, "`background' is not a valid color table"); 
red = getfield("r"); 
green = getfield("g"); 
blue = getfield("b"); 

一般來說,我們首先獲取全局變量 backgroud 的值,並保證它是一個 table。然後,我們使用 getfield 函數獲取每一個顏色組件。這個函數不是 API 的一部分,我們需要自己定義他:

#define MAX_COLOR 255 
/* assume that table is on the stack top */ 
int getfield (const char *key) { 
int result; 
 lua_pushstring(L, key); 
 lua_gettable(L, -2); /* get background[key] */
 //上面兩行可以替換爲:lua_gettable(L,-1,key)
if (!lua_isnumber(L, -1)) 
 error(L, "invalid component in background color"); 
 result = (int)lua_tonumber(L, -1) * MAX_COLOR; 
 lua_pop(L, 1); /* remove number */
return result; 
} 

這裏我們再次面對多態的問題:可能存在很多個 getfield 的版本,key 的類型,value的類型,錯誤處理等都不盡相同。Lua API 只提供了一個 lua_gettable 函數,他接受 table在棧中的位置爲參數,將對應 key 值出棧,返回與 key 對應的 value。
我們上面的 getfield函數假定 table 在棧頂,因此,lua_pushstring 將 key 入棧之後,table 在-2 的位置。返回之前,getfield 會將棧恢復到調用前的狀態。

我們對上面的例子稍作延伸,加入顏色名。用戶仍然可以使用顏色 table,但是也可以爲共同部分的顏色預定義名字,爲了實現這個功能,我們在 C 代碼中需要一個顏色

table:
struct ColorTable { 
char *name; 
unsigned char red, green, blue; 
} colortable[] = { 
 {"WHITE", MAX_COLOR, MAX_COLOR, MAX_COLOR}, 
 {"RED", MAX_COLOR, 0, 0}, 
 {"GREEN", 0, MAX_COLOR, 0}, 
 {"BLUE", 0, 0, MAX_COLOR}, 
 {"BLACK", 0, 0, 0}, 
 ... 
 {NULL, 0, 0, 0} /* sentinel */
}; 

我們的這個實現會使用顏色名創建一個全局變量,然後使用顏色 table 初始化這些全局變量。結果和用戶在腳本中使用下面這幾行代碼是一樣的:

WHITE = {r=1, g=1, b=1} 
RED = {r=1, g=0, b=0} 
... 

腳本中用戶定義的顏色和應用中(C 代碼)定義的顏色不同之處在於:應用在腳本之前運行。

爲了可以設置 table 域的值,我們定義個輔助函數 setfield;這個函數將 field 的索引和 field 的值入棧,然後調用 lua_settable:

/* assume that table is at the top */ 
void setfield (const char *index, int value) { 
 lua_pushstring(L, index); 
 lua_pushnumber(L, (double)value/MAX_COLOR); 
 lua_settable(L, -3); 
 //上下兩行可以替換成:lua_setfield(L,-2,index);
} 

與其他的 API 函數一樣,lua_settable 在不同的參數類型情況下都可以使用,他從棧中獲取所有的參數。lua_settable 以 table 在棧中的索引作爲參數,並將棧中的 key 和 value出棧,用這兩個值修改 table。Setfield 函數假定調用之前 table 是在棧頂位置(索引爲-1)。 將 index 和 value 入棧之後,table 索引變爲-3。

Setcolor 函數定義一個單一的顏色,首先創建一個 table,然後設置對應的域,然後將這個 table 賦值給對應的全局變量:

void setcolor (struct ColorTable *ct) { 
 lua_newtable(L); /* creates a table */
 setfield("r", ct->red); /* table.r = ct->r */
 setfield("g", ct->green); /* table.g = ct->g */
 setfield("b", ct->blue); /* table.b = ct->b */
 lua_setglobal(ct->name); /* 'name' = table */
} 

lua_newtable 函數創建一個新的空 table 然後將其入棧,調用 setfield 設置 table 的域,最後 lua_setglobal 將 table 出棧並將其賦給一個全局變量名。

有了前面這些函數,下面的循環註冊所有的顏色到應用程序中的全局變量:

int i = 0; 
while (colortable[i].name != NULL) 
 setcolor(&colortable[i++]); 

記住:應用程序必須在運行用戶腳本之前,執行這個循環。

對於上面的命名顏色的實現有另外一個可選的方法。用一個字符串來表示顏色名,而不是上面使用全局變量表示,比如用戶可以這樣設置 background = “BLUE”。所以,background 可以是 table 也可以是 string。對於這種實現,應用程序在運行用戶腳本之前不需要做任何特殊處理。但是需要額外的工作來獲取顏色。當他得到變量 background 的值之後,必須判斷這個值的類型,是 table 還是 string:

lua_getglobal(L, "background"); 
if (lua_isstring(L, -1)) { 
const char *name = lua_tostring(L, -1); 
int i = 0; 
while (colortable[i].name != NULL && 
 strcmp(colorname, colortable[i].name) != 0) 
 i++; 
if (colortable[i].name == NULL) /* string not found? */
 error(L, "invalid color name (%s)", colorname); 
else { /* use colortable[i] */
 red = colortable[i].red; 
 green = colortable[i].green; 
 blue = colortable[i].blue; 
 } 
} else if (lua_istable(L, -1)) { 
 red = getfield("r"); 
 green = getfield("g"); 
 blue = getfield("b"); 
} else
 error(L, "invalid value for `background'"); 

哪個是最好的選擇呢?在 C 程序中,使用字符串表示不是一個好的習慣,因爲編譯器不會對字符串進行錯誤檢查。然而在 Lua 中,全局變量不需要聲明,因此當用戶將顏色名字拼寫錯誤的時候,Lua 不會發出任何錯誤信息。比如,用戶將 WHITE 誤寫成 WITE,background 變量將爲 nil(WITE 的值沒有初始化),然後應用程序就認爲 background 的值爲 nil。沒有其他關於這個錯誤的信息可以獲得。另一方面,使用字符串表示,background的值也可能是拼寫錯了的字符串。因此,應用程序可以在發生錯誤的時候,定製輸出的錯誤信息。應用可以不區分大小寫比較字符串,因此,用戶可以寫"white",“WHITE”,甚至"White"。但是,如果用戶腳本很小,並且顏色種類比較多,註冊成百上千個顏色(需要創建成百上千個 table 和全局變量),最終用戶可能只是用其中幾個,這會讓人覺得很怪異。在使用字符串表示的時候,應避免這種情況出現。

調用 Lua 函數

Lua 作爲配置文件的一個最大的長處在於它可以定義個被應用調用的函數。比如,你可以寫一個應用程序來繪製一個函數的圖像,使用 Lua 來定義這個函數。

使用 API 調用函數的方法是很簡單的:
首先,將被調用的函數入棧;
第二,依次將所有參數入棧;
第三,使用 lua_pcall 調用函數;
最後,從棧中獲取函數執行返回的結果。

看一個例子,假定我們的配置文件有下面這個函數:

function f (x, y) 
return (x^2 * math.sin(y))/(1 - x) 
end 

並且我們想在 C 中對於給定的 x,y 計算 z=f(x,y)的值。假如你已經打開了 lua 庫並且運行了配置文件,你可以將這個調用封裝成下面的 C 函數:

/* call a function `f' defined in Lua */ 
double f (double x, double y) { 
double z; 
/* push functions and arguments */ 
 lua_getglobal(L, "f"); /* function to be called */
 lua_pushnumber(L, x); /* push 1st argument */
 lua_pushnumber(L, y); /* push 2nd argument */
/* do the call (2 arguments, 1 result) */ 
if (lua_pcall(L, 2, 1, 0) != 0) 
 error(L, "error running function `f': %s", 
 lua_tostring(L, -1)); 
/* retrieve result */ 
if (!lua_isnumber(L, -1)) 
 error(L, "function `f' must return a number"); 
 z = lua_tonumber(L, -1); 
 lua_pop(L, 1); /* pop returned value */
return z; 
} 

可以調用 lua_pcall 時指定參數的個數和返回結果的個數。第四個參數可以指定一個錯誤處理函數,我們下面再討論它。和 Lua 中賦值操作一樣,lua_pcall 會根據你的要求調整返回結果的個數,多餘的丟棄,少的用 nil 補足。在將結果入棧之前,lua_pcall 會將棧內的函數和參數移除。如果函數返回多個結果,第一個結果被第一個入棧,因此如果有 n 個返回結果,第一個返回結果在棧中的位置爲-n,最後一個返回結果在棧中的位置爲-1。

如果 lua_pcall 運行時出現錯誤,lua_pcall 會返回一個非 0 的結果。另外,他將錯誤信息入棧(仍然會先將函數和參數從棧中移除)。在將錯誤信息入棧之前,如果指定了錯誤處理函數,lua_pcall 毀掉用錯誤處理函數。使用 lua_pcall 的最後一個參數來指定錯誤處理函數,0 代表沒有錯誤處理函數,也就是說最終的錯誤信息就是原始的錯誤信息。否則,那個參數應該是一個錯誤函數被加載的時候在棧中的索引,注意,在這種情況下,錯誤處理函數必須要在被調用函數和其參數入棧之前入棧。

對於一般錯誤,lua_pcall 返回錯誤代碼 LUA_ERRRUN。有兩種特殊情況,會返回特殊的錯誤代碼,因爲他們從來不會調用錯誤處理函數。第一種情況是,內存分配錯誤,對於這種錯誤,lua_pcall 總是返回 LUA_ERRMEM。第二種情況是,當 Lua 正在運行錯誤處理函數時發生錯誤,這種情況下,再次調用錯誤處理函數沒有意義,所以 lua_pcall立即返回錯誤代碼 LUA_ERRERR。

通用的Lua函數調用

看一個稍微高級的例子,我們使用 C 的 vararg 來封裝對 Lua 函數的調用。我們的封裝後的函數(call_va)接受被調用的函數明作爲第一個參數,第二參數是一個描述參數和結果類型的字符串,最後是一個保存返回結果的變量指針的列表。使用這個函數,我們可以將前面的例子改寫爲:

call_va("f", "dd>d", x, y, &z); 

字符串 “dd>d” 表示函數有兩個 double 類型的參數,一個 double 類型的返回結果。我們使用字母 ‘d’ 表示 double;‘i’ 表示 integer,‘s’ 表示 strings;’>’ 作爲參數和結果的分隔符。如果函數沒有返回結果,’>’ 是可選的。

#include <stdarg.h> 
void call_va (const char *func, const char *sig, ...) { 
	va_list vl; 
	int narg, nres; /* number of arguments and results */
	va_start(vl, sig); 
	lua_getglobal(L, func); /* get function */
	/* push arguments */ 
	narg = 0; 
	while (*sig) { /* push arguments */
		switch (*sig++) { 
			case 'd': /* double argument */
				lua_pushnumber(L, va_arg(vl, double)); 
			break; 
			case 'i': /* int argument */
				lua_pushnumber(L, va_arg(vl, int)); 
			break; 
			case 's': /* string argument */
				lua_pushstring(L, va_arg(vl, char *)); 
			break; 
			case '>': 
				goto endwhile; 
			default: 
			error(L, "invalid option (%c)", *(sig - 1)); 
		} 
		narg++; 
		luaL_checkstack(L, 1, "too many arguments"); 
	} endwhile: 
	/* do the call */ 
	nres = strlen(sig); /* number of expected results */
	if (lua_pcall(L, narg, nres, 0) != 0) /* do the call */
		error(L, "error running function `%s': %s", 
	func, lua_tostring(L, -1)); 
	/* retrieve results */ 
	nres = -nres; /* stack index of first result */
	while (*sig) { /* get results */
		switch (*sig++) { 
			case 'd': /* double result */
				if (!lua_isnumber(L, nres)) 
					error(L, "wrong result type"); 
				*va_arg(vl, double *) = lua_tonumber(L, nres); 
			break; 
			case 'i': /* int result */
				if (!lua_isnumber(L, nres)) 
					error(L, "wrong result type"); 
				va_arg(vl, int *) = (int)lua_tonumber(L, nres); 
			break; 
			case 's': /* string result */
				if (!lua_isstring(L, nres)) 
					error(L, "wrong result type"); 
				*va_arg(vl, const char **) = lua_tostring(L, nres); 
				break; 
			default: 
				error(L, "invalid option (%c)", *(sig - 1)); 
		} 
		nres++; 
	} 
	va_end(vl); 
} 

儘管這段代碼具有一般性,這個函數和前面我們的例子有相同的步驟:將函數入棧,參數入棧,調用函數,獲取返回結果。大部分代碼都很直觀,但也有一點技巧。首先,不需要檢查 func 是否是一個函數,lua_pcall 可以捕捉這個錯誤。第二,可以接受任意多個參數,所以必須檢查棧的空間。第三,因爲函數可能返回字符串,call_va 不能從棧中彈出結果,在調用者獲取臨時字符串的結果之後(拷貝到其他的變量中),由調用者負責彈出結果。

調用 C 函數

擴展 Lua 的基本方法之一就是爲應用程序註冊新的 C 函數到 Lua 中去。

當我們提到 Lua 可以調用 C 函數,不是指 Lua 可以調用任何類型的 C 函數(有一些包可以讓 Lua 調用任意的 C 函數,但缺乏便捷和健壯性)。正如我們前面所看到的,當C 調用 Lua 函數的時候,必須遵循一些簡單的協議來傳遞參數和獲取返回結果。相似的,從 Lua 中調用 C 函數,也必須遵循一些協議來傳遞參數和獲得返回結果。另外,從 Lua調用 C 函數我們必須註冊函數,也就是說,我們必須把 C 函數的地址以一個適當的方式傳遞給 Lua 解釋器。

當 Lua 調用 C 函數的時候,使用和 C 調用 Lua 相同類型的棧來交互。C 函數從棧中獲取她的參數,調用結束後將返回結果放到棧中。爲了區分返回結果和棧中的其他的值,每個 C 函數還會返回結果的個數(the function returns (in C) the number of results it is leaving on the stack.)。這兒有一個重要的概念:用來交互的棧不是全局變量,每一個函數都有他自己的私有棧。當 Lua 調用 C 函數的時候,第一個參數總是在這個私有棧的index=1 的位置。甚至當一個 C 函數調用 Lua 代碼(Lua 代碼調用同一個 C 函數或者其他的 C 函數),每一個 C 函數都有自己的獨立的私有棧,並且第一個參數在 index=1 的位置。

C 函數

先看一個簡單的例子,如何實現一個簡單的函數返回給定數值的 sin 值(更專業的實現應該檢查他的參數是否爲一個數字):

static int l_sin (lua_State *L) { 
double d = lua_tonumber(L, 1); /* get argument */
 lua_pushnumber(L, sin(d)); /* push result */
return 1; /* number of results */
} 

任何在 Lua 中註冊的函數必須有同樣的原型,這個原型聲明定義就是 lua.h 中的lua_CFunction:

typedef int (*lua_CFunction) (lua_State *L); 

從 C 的角度來看,一個 C 函數接受單一的參數 Lua state,返回一個表示返回值個數的數字。所以,函數在將返回值入棧之前不需要清理棧,函數返回之後,Lua 自動的清除棧中返回結果下面的所有內容。

我們要想在 Lua 使用這個函數,還必須首先註冊這個函數。我們使用lua_pushcfunction 來完成這個任務:他獲取指向 C 函數的指針,並在 Lua 中創建一個function 類型的值來表示這個函數。一個 quick-and-dirty 的解決方案是將這段代碼直接放到 lua.c 文件中,並在調用lua_open 後面適當的位置加上下面兩行:

lua_State *L = luaL_newstate(); //打開Lua
luaL_openlibs(L); //打開lua庫
lua_pushcfunction(L, l_sin); 
lua_setglobal(L, "mysin"); 

第一行將類型爲 function 的值入棧,第二行將 function 賦值給全局變量 mysin。這樣修改之後,重新編譯 Lua,你就可以在你的 Lua 程序中使用新的 mysin 函數了。在下面一節,我們將討論以比較好的方法將新的 C 函數添加到 Lua 中去。

對於稍微專業點的 sin 函數,我們必須檢查 sin 的參數的類型。有一個輔助庫中的luaL_checknumber 函數可以檢查給定的參數是否爲數字:當有錯誤發生的時候,將拋出一個錯誤信息;否則返回作爲參數的那個數字。將上面我們的函數稍作修改:

static int l_sin (lua_State *L) { 
double d = luaL_checknumber(L, 1); 
 lua_pushnumber(L, sin(d)); 
return 1; /* number of results */
} 

根據上面的定義,如果你調用 mysin(‘a’),會得到如下信息:

bad argument #1 to 'mysin' (number expected, got string) 

注意看看 luaL_checknumber 是如何自動使用:參數 number(1),函數名(“mysin”),期望的參數類型(“number”),實際的參數類型(“string”)來拼接最終的錯誤信息的。

下面看一個稍微複雜的例子:寫一個返回給定目錄內容的函數。Lua 的標準庫並沒有提供這個函數,因爲 ANSI C 沒有可以實現這個功能的函數。在這兒,我們假定我們的系統符合 POSIX 標準。我們的 dir 函數接受一個代表目錄路徑的字符串作爲參數,以數組的形式返回目錄的內容。比如,調用 dir("/home/lua")可能返回{".", “…”, “src”, “bin”, “lib”}。當有錯誤發生的時候,函數返回 nil 加上一個描述錯誤信息的字符串。

#include <dirent.h> 
#include <errno.h> 
static int l_dir (lua_State *L) { 
 DIR *dir; 
struct dirent *entry; 
int i; 
const char *path = luaL_checkstring(L, 1); 
/* open directory */ 
 dir = opendir(path); 
if (dir == NULL) { /* error opening the directory? */
 lua_pushnil(L); /* return nil and ... */
 lua_pushstring(L, strerror(errno)); /* error message */
 return 2; /* number of results */
 } 
/* create result table */ 
 lua_newtable(L); 
 i = 1; 
while ((entry = readdir(dir)) != NULL) { 
 lua_pushnumber(L, i++); /* push key */
 lua_pushstring(L, entry->d_name); /* push value */
 lua_settable(L, -3); 
 } 
 closedir(dir); 
return 1; /* table is already on top */
} 

輔助庫的 luaL_checkstring 函數用來檢測參數是否爲字符串,與 luaL_checknumber類似。(在極端情況下,上面的 l_dir 的實現可能會導致小的內存泄漏。調用的三個 Lua函數 lua_newtable、lua_pushstring 和 lua_settable 可能由於沒有足夠的內存而失敗。其中任何一個調用失敗都會拋出錯誤並且終止 l_dir,這種情況下,不會調用 closedir。正如前面我們所討論過的,對於大多數程序來說這不算個問題:如果程序導致內存不足,最好的處理方式是立即終止程序。另外,在 29 章我們將看到另外一種解決方案可以避免這個問題的發生)

C 函數庫

一個 Lua 庫實際上是一個定義了一系列 Lua 函數的 chunk,並將這些函數保存在適當的地方,通常作爲 table 的域來保存。Lua 的 C 庫就是這樣實現的。除了定義 C 函數之外,還必須定義一個特殊的用來和 Lua 庫的主 chunk 通信的特殊函數。一旦調用,這個函數就會註冊庫中所有的 C 函數,並將他們保存到適當的位置。像一個 Lua 主 chunk一樣,她也會初始化其他一些在庫中需要初始化的東西。

Lua 通過這個註冊過程,就可以看到庫中的 C 函數。一旦一個 C 函數被註冊之後並保存到 Lua 中,在 Lua 程序中就可以直接引用他的地址(當我們註冊這個函數的時候傳遞給 Lua 的地址)來訪問這個函數了。換句話說,一旦 C 函數被註冊之後,Lua 調用這個函數並不依賴於函數名,包的位置,或者調用函數的可見的規則。通常 C 庫都有一個外部(public/extern)的用來打開庫的函數。其他的函數可能都是私有的,在 C 中被聲明爲 static。

當你打算使用 C 函數來擴展 Lua 的時候,即使你僅僅只想註冊一個 C 函數,將你的C 代碼設計爲一個庫是個比較好的思想:不久的將來你就會發現你需要其他的函數。一般情況下,輔助庫對這種實現提供了幫助。luaL_openlib 函數接受一個 C 函數的列表和他們對應的函數名,並且作爲一個庫在一個 table 中註冊所有這些函數。看一個例子,假定我們想用一個我們前面提過的 l_dir 函數創建一個庫。首先,我們必須定義庫函數:

static int l_dir (lua_State *L) { 
 ... /* as before */
} 

第二步,我們聲明一個數組,保存所有的函數和他們對應的名字。這個數組的元素類型爲 luaL_reg:是一個帶有兩個域的結構體,一個字符串和一個函數指針。

static const struct luaL_reg mylib [] = { 
 {"dir", l_dir}, 
 {NULL, NULL} /* sentinel */
}; 

在我們的例子中,只有一個函數 l_dir 需要聲明。注意數組中最後一對必須是{NULL, NULL},用來表示結束。第三步,我們使用 luaL_openlib 聲明主函數:

int luaopen_mylib (lua_State *L) { 
 luaL_openlib(L, "mylib", mylib, 0); //luaL_register(L, "mylib", mylib)
return 1; 
} 

luaL_openlib 的第二個參數是庫的名稱。這個函數按照指定的名字創建(或者 reuse)一個表,並使用數組 mylib 中的 name-function 對填充這個表。luaL_openlib 還允許我們爲庫中所有的函數註冊公共的 upvalues。例子中不需要使用 upvalues,所以最後一個參數爲 0luaL_openlib 返回的時候,將保存庫的表放到棧內。luaL_openlib 函數返回 1,返回這個值給 Lua。(The luaopen_mylib function returns 1 to return this value to Lua)(和Lua 庫一樣,這個返回值是可選的,因爲庫本身已經賦給了一個全局變量。另外,像在Lua 標準庫中的一樣,這個返回不會有額外的花費,在有時候可能是有用的。)

完成庫的代碼編寫之後,我們必須將它鏈接到 Lua 解釋器。最常用的方式使用動態連接庫,如果你的 Lua 解釋器支持這個特性的話(我們在 8.2 節已經討論過了動態連接庫)。在這種情況下,你必須用你的代碼創建動態連接庫(windows 下.dll 文件,linux 下.so文件)。到這一步,你就可以在 Lua 中直接使用 loadlib 加載你剛纔定義的函數庫了,下面這個調用:

mylib = loadlib("fullname-of-your-library", "luaopen_mylib") 

將 luaopen_mylib 函數轉換成 Lua 中的一個 C 函數,並將這個函數賦值給 mylib(那就是爲什麼 luaopen_mylib 必須和其他的 C 函數有相同的原型的原因所在)。然後,調用mylib(),將運行 luaopen_mylib 打開你定義的函數庫。

此時在Lua中調用require "mylib"即可調用C++函數了

如果你的解釋器不支持動態鏈接庫,你必須將你的新的函數庫重新編譯到你的 Lua中去。
除了這以外,還不要一些方式告訴獨立運行的 Lua 解釋器,當他打開一個新的狀態的時候必須打開這個新定義的函數庫。宏定義可以很容易實現這個功能。第一,你必須使用下面的內容創建一個頭文件(我們可以稱之爲 mylib.h):

int luaopen_mylib (lua_State *L); 
#define LUA_EXTRALIBS { "mylib", luaopen_mylib }, 

第一行聲明瞭打開庫的函數。第二行定義了一個宏 LUA_EXTRALIBS 作爲函數數組的新的入口,當解釋器創建新的狀態的時候會調用這個宏。(這個函數數組的類型爲struct luaL_reg[],因此我們需要將名字也放進去)

爲了在解釋器中包含這個頭文件,你可以在你的編譯選項中定義一個宏LUA_USERCONFIG。對於命令行的編譯器,你只需添加一個下面這樣的選項即可:

-DLUA_USERCONFIG=\"mylib.h\" 

(反斜線防止雙引號被 shell 解釋,當我們在 C 中指定一個頭文件時,這些引號是必需的。)在一個整合的開發環境中,你必須在工程設置中添加類似的東西。然後當你重新編譯 lua.c 的時候,它包含 mylib.h,並且因此在函數庫的列表中可以用新定義的LUA_EXTRALIBS 來打開函數庫。

Lua調用C模塊的步驟:

  1. 創建C模塊
  2. 將C模塊編譯生成一個動態鏈接庫(.dll)
  3. 在lua中require”動態鏈接庫名稱“(不需要擴展名.dll)
  4. 可以在lua中使用C模塊中定義的函數了

撰寫 C 函數的技巧

官方的 API 和輔助函數庫 都提供了一些幫助程序員如何寫好 C 函數的機制。在這一章我們將討論數組操縱、string 處理、在 C 中存儲 Lua 值等一些特殊的機制。

數組操作

Lua 中數組實際上就是以特殊方式使用的 table 的別名。我們可以使用任何操縱 table的函數來對數組操作,即 lua_settable 和 lua_gettable。然而,與 Lua 常規簡潔思想(economy and simplicity)相反的是,API 爲數組操作提供了一些特殊的函數。這樣做的原因出於性能的考慮:因爲我們經常在一個算法(比如排序)的循環的內層訪問數組,所以這種內層操作的性能的提高會對整體的性能的改善有很大的影響。

API 提供了下面兩個數組操作函數:

void lua_rawgeti (lua_State *L, int index, int key); 
void lua_rawseti (lua_State *L, int index, int key); 

關於的 lua_rawgeti 和 lua_rawseti 的描述有些使人糊塗,因爲它涉及到兩個索引:index 指向 table 在棧中的位置;key 指向元素在 table 中的位置。當 t 使用負索引的時候(otherwise,you must compensate for the new item in the stack),調用 lua_rawgeti(L,t,key)等價於:

lua_pushnumber(L, key); 
lua_rawget(L, t); 

調用 lua_rawseti(L, t, key)(也要求 t 使用負索引)等價於:

lua_pushnumber(L, key); 
lua_insert(L, -2); /* put 'key' below previous value */
lua_rawset(L, t); 

注意這兩個寒暑都是用 raw 操作,他們的速度較快,總之,用作數組的 table 很少使用 metamethods。
下面看如何使用這些函數的具體的例子,我們將前面的 l_dir 函數的循環體:

lua_pushnumber(L, i++); /* key */
lua_pushstring(L, entry->d_name); /* value */
lua_settable(L, -3); 

改寫爲:

lua_pushstring(L, entry->d_name); /* value */
lua_rawseti(L, -2, i++); /* set table at key 'i' */ 

下面是一個更完整的例子,下面的代碼實現了 map 函數:以數組的每一個元素爲參數調用一個指定的函數,並將數組的該元素替換爲調用函數返回的結果。

int l_map (lua_State *L) { 
int i, n; 
/* 1st argument must be a table (t) */ 
 luaL_checktype(L, 1, LUA_TTABLE); 
/* 2nd argument must be a function (f) */ 
 luaL_checktype(L, 2, LUA_TFUNCTION); 
 n = luaL_getn(L, 1); /* get size of table */
for (i=1; i<=n; i++) { 
 lua_pushvalue(L, 2); /* push f */
 lua_rawgeti(L, 1, i); /* push t[i] */
 lua_call(L, 1, 1); /* call f(t[i]) */
 lua_rawseti(L, 1, i); /* t[i] = result */
 } 
return 0; /* no results */
} 

這裏面引入了三個新的函數。luaL_checktype(在 lauxlib.h 中定義)用來檢查給定的參數有指定的類型;否則拋出錯誤。luaL_getn 函數棧中指定位置的數組的大小(table.getn是調用 luaL_getn 來完成工作的)。lua_call 的運行是無保護的,他與 lua_pcall 相似,但是在錯誤發生的時候她拋出錯誤而不是返回錯誤代碼。當你在應用程序中寫主流程的代碼時,不應該使用 lua_call,因爲你應該捕捉任何可能發生的錯誤。當你寫一個函數的代碼時,使用 lua_call 是比較好的想法,如果有錯誤發生,把錯誤留給關心她的人去處理。

字符串處理

當 C 函數接受一個來自 lua 的字符串作爲參數時,有兩個規則必須遵守:當字符串正在被訪問的時候不要將其出棧;永遠不要修改字符串。
當 C 函數需要創建一個字符串返回給 lua 的時候,情況變得更加複雜。這樣需要由C 代碼來負責緩衝區的分配和釋放,負責處理緩衝溢出等情況。然而,Lua API 提供了一些函數來幫助我們處理這些問題。

標準 API 提供了對兩種基本字符串操作的支持:子串截取和字符串連接。記住,lua_pushlstring 可以接受一個額外的參數,字符串的長度來實現字符串的截取,所以,如果你想將字符串 s 從 i 到 j 位置(包含 i 和 j)的子串傳遞給 lua,只需要:

lua_pushlstring(L, s+i, j-i+1); 

下面這個例子,假如你想寫一個函數來根據指定的分隔符分割一個字符串,並返回一個保存所有子串的 table,比如調用:

split("hi,,there", ",") 

應該返回表{“hi”, “”, “there”}。我們可以簡單的實現如下,下面這個函數不需要額外的緩衝區,可以處理字符串的長度也沒有限制。

static int l_split (lua_State *L) { 
const char *s = luaL_checkstring(L, 1); 
const char *sep = luaL_checkstring(L, 2); 
const char *e; 
int i = 1; 
 lua_newtable(L); /* result */
/* repeat for each separator */ 
while ((e = strchr(s, *sep)) != NULL) { 
 lua_pushlstring(L, s, e-s); /* push substring */
 lua_rawseti(L, -2, i++); 
 s = e + 1; /* skip separator */
 } 
/* push last substring */ 
 lua_pushstring(L, s); 
 lua_rawseti(L, -2, i); 
return 1; /* return the table */
} 

在 Lua API 中提供了專門的用來連接字符串的函數 lua_concat。等價於 Lua 中的…操作符:自動將數字轉換成字符串,如果有必要的時候還會自動調用 metamethods。另外,她可以同時連接多個字符串。調用 lua_concat(L,n)將連接(同時會出棧)棧頂的 n 個值,並將最終結果放到棧頂。

另一個有用的函數是 lua_pushfstring:

const char *lua_pushfstring (lua_State *L, const char *fmt, ...); //類似Csprintf

這個函數某種程度上類似於 C 語言中的 sprintf,根據格式串 fmt 的要求創建一個新的字符串。與 sprintf 不同的是,你不需要提供一個字符串緩衝數組,Lua 爲你動態的創建新的字符串,按他實際需要的大小。也不需要擔心緩衝區溢出等問題。這個函數會將結果字符串放到棧內,並返回一個指向這個結果串的指針。當前,這個函數只支持下列幾個指示符: %%(表示字符 ‘%’)、%s(用來格式化字符串)、%d(格式化整數)、%f(格式化 Lua 數字,即 doubles)和 %c(接受一個數字並將其作爲字符),不支持寬度和精度等選項。

當我們打算連接少量的字符串的時候,lua_concat 和 lua_pushfstring 是很有用的,然而,如果我們需要連接大量的字符串(或者字符),這種一個一個的連接方式效率是很低的,正如我們在 11.6 節看到的那樣。我們可以使用輔助庫提供的 buffer 相關函數來解決這個問題。Auxlib 在兩個層次上實現了這些 buffer。第一個層次類似於 I/O 操作的 buffers:集中所有的字符串(或者但個字符)放到一個本地 buffer 中,當本地 buffer 滿的時候將其傳遞給 Lua(使用 lua_pushlstring)。第二個層次使用 lua_concat 和我們在 11.6 節中看到的那個棧算法的變體,來連接多個 buffer 的結果。

爲了更詳細地描述 Auxlib 中的 buffer 的使用,我們來看一個簡單的應用。下面這段代碼顯示了 string.upper 的實現(來自文件 lstrlib.c):

static int str_upper (lua_State *L) { 
 size_t l; 
 size_t i; 
 luaL_Buffer b; 
const char *s = luaL_checklstr(L, 1, &l); 
 luaL_buffinit(L, &b); 
for (i=0; i<l; i++) 
 luaL_putchar(&b, toupper((unsigned char)(s[i]))); 
 luaL_pushresult(&b); 
return 1; 
} 

使用 Auxlib 中 buffer 的第一步是使用類型 luaL_Buffer 聲明一個變量,然後調用luaL_buffinit 初始化這個變量。初始化之後,buffer 保留了一份狀態 L 的拷貝,因此當我們調用其他操作 buffer 的函數的時候不需要傳遞 L。宏 luaL_putchar 將一個單個字符放入 buffer。Auxlib 也提供了 luaL_addlstring 以一個顯示的長度將一個字符串放入 buffer, 而 luaL_addstring 將一個以 0 結尾的字符串放入 buffer。最後,luaL_pushresult 刷新 buffer並將最終字符串放到棧頂。這些函數的原型如下:

void luaL_buffinit (lua_State *L, luaL_Buffer *B); 
void luaL_putchar (luaL_Buffer *B, char c); 
void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l); 
void luaL_addstring (luaL_Buffer *B, const char *s); 
void luaL_pushresult (luaL_Buffer *B); 

使用這些函數,我們不需要擔心 buffer 的分配,溢出等詳細信息。正如我們所看到的,連接算法是有效的。函數 str_upper 可以毫無問題的處理大字符串(大於 1MB)。

當你使用auxlib中的buffer時,不必擔心一點細節問題。你只要將東西放入buffer,程序會自動在Lua棧中保存中間結果。所以,你不要認爲棧頂會保持你開始使用buffer的那個狀態。另外,雖然你可以在使用buffer的時候,將棧用作其他用途,但每次你訪問buffer的時候,這些其他用途的操作進行的push/pop操作必須保持平衡**(即有多少次push就要有多少次pop)**。有一種情況,即你打算將從Lua返回的字符串放入buffer時,這種情況下,這些限制有些過於嚴格。這種情況下,在將字符串放入buffer之前,不能將字符串出棧,因爲一旦你從棧中將來自於Lua的字符串移出,你就永遠不能使用這個字符串。同時,在將一個字符串出棧之前,你也不能夠將其放入buffer,因爲那樣會將棧置於錯誤的層次(because then the stack would be in the wrong level)。換句話說你不能做類似下面的事情:

luaL_addstring(&b, lua_tostring(L, 1)); /* BAD CODE */ 
//(上面正好構成了一對矛盾),由於這種情況是很常見的,auxlib 提供了特殊的函數來將位於棧頂的值放入 buffer
void luaL_addvalue (luaL_Buffer *B); 

當然,如果位於棧頂的值不是字符串或者數字的話,調用這個函數將會出錯。

在 C 函數中保存狀態

通常來說,C 函數需要保留一些非局部的數據,也就是指那些超過他們作用範圍的數據。C 語言中我們使用全局變量或者 static 變量來滿足這種需要。然而當你爲 Lua 設計一個程序庫的時候,全局變量和 static 變量不是一個好的方法。首先,不能將所有的(一般意義的,原文 generic)Lua 值保存到一個 C 變量中。第二,使用這種變量的庫不能在多個 Lua 狀態的情況下使用。

一個替代的解決方案是將這些值保存到一個 Lua 全局變兩種,這種方法解決了前面的兩個問題。Lua 全局變量可以存放任何類型的 Lua 值,並且每一個獨立的狀態都有他自己獨立的全局變量集。然而,並不是在所有情況下,這種方法都是令人滿意地解決方案,因爲 Lua 代碼可能會修改這些全局變量,危及 C 數據的完整性。爲了避免這個問題,Lua 提供了一個獨立的被稱爲 registry 的表,C 代碼可以自由使用,但 Lua 代碼不能訪問他。

The Registry

registry 一直位於一個由 LUA_REGISTRYINDEX 定義的值所對應的假索引(pseudo-index)的位置。一個假索引除了他對應的值不在棧中之外,其他都類似於棧中的索引。Lua API 中大部分接受索引作爲參數的函數,也都可以接受假索引作爲參數—除了那些操作棧本身的函數,比如 lua_remove,lua_insert。例如,爲了獲取以鍵值 “Key” 保存在 registry 中的值,使用下面的代碼:

lua_pushstring(L, "Key"); 
lua_gettable(L, LUA_REGISTRYINDEX); 

registry 就是普通的 Lua 表,因此,你可以使用任何非 nil 的 Lua 值來訪問她的元素。然而,由於所有的 C 庫共享相同的 registry ,你必須注意使用什麼樣的值作爲 key,否則會導致命名衝突。一個防止命名衝突的方法是使用 static 變量的地址作爲 key:C 鏈接器保證在所有的庫中這個 key 是唯一的。函數 lua_pushlightuserdata 將一個代表 C 指針的值放到棧內,下面的代碼展示了使用上面這個方法,如何從 registry 中獲取變量和向registry 存儲變量:

/* variable with an unique address */ 
static const char Key = 'k'; 
/* store a number */ 
lua_pushlightuserdata(L, (void *)&Key); /* push address */
lua_pushnumber(L, myNumber); /* push value */
/* registry[&Key] = myNumber */ 
lua_settable(L, LUA_REGISTRYINDEX); 
/* retrieve a number */ 
lua_pushlightuserdata(L, (void *)&Key); /* push address */
lua_gettable(L, LUA_REGISTRYINDEX); /* retrieve value */
myNumber = lua_tonumber(L, -1); /* convert to number */ 

我們會在 28.5 節中更詳細的討論 light userdata。

當然,你也可以使用字符串作爲 registry 的 key,只要你保證這些字符串唯一。當你打算允許其他的獨立庫房問你的數據的時候,字符串型的 key 是非常有用的,因爲他們需要知道 key 的名字。對這種情況,沒有什麼方法可以絕對防止名稱衝突,但有一些好的習慣可以採用,比如使用庫的名稱作爲字符串的前綴等類似的方法。類似lua 或者 lualib的前綴不是一個好的選擇。另一個可選的方法是使用 universal unique identifier(uuid),很多系統都有專門的程序來產生這種標示符(比如 linux 下的 uuidgen)。一個 uuid 是一個由本機 IP 地址、時間戳、和一個隨機內容組合起來的 128 位的數字(以 16 進制的方式書寫,用來形成一個字符串),因此它與其他的 uuid 不同是可以保證的。

References

你應該記住,永遠不要使用數字作爲 registry 的 key,因爲這種類型的 key 是保留給reference 系統使用。Reference 系統是由輔助庫中的一對函數組成,這對函數用來不需要擔心名稱衝突的將值保存到 registry 中去。(實際上,這些函數可以用於任何一個表,但他們典型的被用於 registry)

調用

int r = luaL_ref(L, LUA_REGISTRYINDEX); 

從棧中彈出一個值,以一個新的數字作爲 key 將其保存到 registry 中,並返回這個key。我們將這個 key 稱之爲 reference。

顧名思義,我們使用 references 主要用於:將一個指向 Lua 值的 reference 存儲到一個 C 結構體中。正如前面我們所見到的,我們永遠不要將一個指向 Lua 字符串的指針保存到獲取這個字符串的外部的 C 函數中。另外,Lua 甚至不提供指向其他對象的指針,比如 table 或者函數。因此,我們不能通過指針指向 Lua 對象。當我們需要這種指針的時候,我們創建一個 reference 並將其保存在 C 中。

要想將一個 reference 的對應的值入棧,只需要:

lua_rawgeti(L, LUA_REGISTRYINDEX, r); 

最後,我們調用下面的函數釋放值和 reference:

luaL_unref(L, LUA_REGISTRYINDEX, r); 

調用這個之後,luaL_ref 可以再次返回 r 作爲一個新的 reference。

reference 系統將 nil 作爲特殊情況對待,不管什麼時候,你以 nil 調用 luaL_ref 的話,不會創建一新的 reference ,而是返回一個常量 reference LUA_REFNIL。下面的調用沒有效果:

luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL); 

然而

lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL); 

像預期的一樣,將一個 nil 入棧。

reference 系統也定義了常量 LUA_NOREF,她是一個表示任何非有效的 reference 的整數值,用來標記無效的 reference。任何企圖獲取 LUA_NOREF 返回 nil,任何釋放他的操作都沒有效果。

註冊表與引用總結:

在這裏插入圖片描述
在這裏插入圖片描述

C函數環境:(儘量用C函數環境代替註冊表)

在這裏插入圖片描述

Upvalues

registry 實現了全局的值,upvalue 機制實現了與 C static 變量等價的東東,這種變量只能在特定的函數內可見。每當你在 Lua 中創建一個新的 C 函數,你可以將這個函數與任意多個 upvalues 聯繫起來,每一個 upvalue 可以持有一個單獨的 Lua 值。下面當函數被調用的時候,可以通過假索引自由的訪問任何一個 upvalues。

我們稱這種一個 C 函數和她的 upvalues 的組合爲閉包(closure)。記住:在 Lua 代碼中,一個閉包是一個從外部函數訪問局部變量的函數。一個 C 閉包與一個 Lua 閉包相近。關於閉包的一個有趣的事實是,你可以使用相同的函數代碼創建不同的閉包,帶有不同的upvalues。

看一個簡單的例子,我們在 C 中創建一個 newCounter 函數。(我們已經在 6.1 節部分在 Lua 中定義過同樣的函數)。這個函數是個函數工廠:每次調用他都返回一個新的counter 函數。儘管所有的 counters 共享相同的 C 代碼,但是每個都保留獨立的 counter變量,工廠函數如下:

/* forward declaration */ 
static int counter (lua_State *L); 
int newCounter (lua_State *L) { 
 lua_pushnumber(L, 0); 
 lua_pushcclosure(L, &counter, 1); 
return 1; 
} 

這裏的關鍵函數是 lua_pushcclosure,她的第二個參數是一個基本函數(例子中衛counter),第三個參數是 upvalues 的個數(例子中爲 1)。在創建新的閉包之前,我們必須將 upvalues 的初始值入棧,在我們的例子中,我們將數字 0 作爲唯一的 upvalue 的初始值入棧。如預期的一樣,lua_pushcclosure 將新的閉包放到棧內,因此閉包已經作爲newCounter 的結果被返回。

現在,我們看看 counter 的定義:

static int counter (lua_State *L) { 
double val = lua_tonumber(L, lua_upvalueindex(1)); 
 lua_pushnumber(L, ++val); /* new value */
 lua_pushvalue(L, -1); /* duplicate it */
 lua_replace(L, lua_upvalueindex(1)); /* update upvalue */
return 1; /* return new value */
} 

這裏的關鍵函數是 lua_upvalueindex(實際是一個宏),用來產生一個 upvalue 的假索引。這個假索引除了不在棧中之外,和其他的索引一樣。表達式 lua_upvalueindex(1)函數第一個 upvalue 的索引。因此,在函數 counter 中的 lua_tonumber 獲取第一個(僅有的)upvalue 的當前值,轉換爲數字型。然後,函數 counter 將新的值++val 入棧,並將這個值的一個拷貝使用新的值替換 upvalue。最後,返回其他的拷貝。

與 Lua 閉包不同的是,C 閉包不能共享 upvalues:每一個閉包都有自己獨立的變量集。然而,我們可以設置不同函數的 upvalues 指向同一個表,這樣這個表就變成了一個所有函數共享數據的地方。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

User-Defined Types in C

在面的一章,我們討論瞭如何使用 C 函數擴展 Lua 的功能,現在我們討論如何使用C 中新創建的類型來擴展 Lua。我們從一個小例子開始,本章後續部分將以這個小例子爲基礎逐步加入 metamethods 等其他內容來介紹如何使用 C 中新類型擴展 Lua。

我們的例子涉及的類型非常簡單,數字數組。這個例子的目的在於將目光集中到 API問題上,所以不涉及複雜的算法。儘管例子中的類型很簡單,但很多應用中都會用到這種類型。一般情況下,Lua 中並不需要外部的數組,因爲哈希表很好的實現了數組。但是對於非常大的數組而言,哈希表可能導致內存不足,因爲對於每一個元素必須保存一個範性的(generic)值,一個鏈接地址,加上一些以備將來增長的額外空間。在 C 中的直接存儲數字值不需要額外的空間,將比哈希表的實現方式節省 50%的內存空間。

我們使用下面的結構表示我們的數組:

typedef struct NumArray { 
int size; 
double values[1]; /* variable part */
} NumArray; 

我們使用大小 1 聲明數組的 values,由於 C 語言不允許大小爲 0 的數組,這個 1 只是一個佔位符;我們在後面定義數組分配空間的實際大小。對於一個有 n 個元素的數組來說,我們需要

sizeof(NumArray) + (n-1)*sizeof(double) bytes 

(由於原始的結構中已經包含了一個元素的空間,所以我們從 n 中減去 1)

Userdata

我們首先關心的是如何在 Lua 中表示數組的值。Lua 爲這種情況提供專門提供一個基本的類型:userdata。一個 userdatum 提供了一個在 Lua 中沒有預定義操作的 raw 內存區域。

Lua API 提供了下面的函數用來創建一個 userdatum:

void *lua_newuserdata (lua_State *L, size_t size); 

lua_newuserdata 函數按照指定的大小分配一塊內存,將對應的 userdatum 放到棧內,並返回內存塊的地址。如果出於某些原因你需要通過其他的方法分配內存的話,很容易創建一個指針大小的 userdatum,然後將指向實際內存塊的指針保存到 userdatum 裏。我們將在下一章看到這種技術的例子。

使用 lua_newuserdata 函數,創建新數組的函數實現如下:

static int newarray (lua_State *L) { 
int n = luaL_checkint(L, 1); 
 size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double); 
 NumArray *a = (NumArray *)lua_newuserdata(L, nbytes); 
 a->size = n; 
return 1; /* new userdatum is already on the stack */
} 

(函數 luaL_checkint 是用來檢查整數的 luaL_checknumber 的變體)一旦 newarray在 Lua 中被註冊之後,你就可以使用類似 a = array.new(1000)的語句創建一個新的數組了。

爲了存儲元素,我們使用類似 array.set(array, index, value)調用,後面我們將看到如何使用 metatables 來支持常規的寫法 array[index] = value。對於這兩種寫法,下面的函數是一樣的,數組下標從1開始:

static int setarray (lua_State *L) { 
 NumArray *a = (NumArray *)lua_touserdata(L, 1); 
int index = luaL_checkint(L, 2); 
double value = luaL_checknumber(L, 3); 
 luaL_argcheck(L, a != NULL, 1, "`array' expected"); 
 luaL_argcheck(L, 1 <= index && index <= a->size, 2, 
 "index out of range"); 
 a->values[index-1] = value; 
return 0; 
} 

luaL_argcheck 函數檢查給定的條件,如果有必要的話拋出錯誤。因此,如果我們使用錯誤的參數調用 setarray,我們將得到一個錯誤信息:

array.set(a, 11, 0) 
--> stdin:1: bad argument #1 to 'set' ('array' expected) 

下面的函數獲取一個數組元素:

static int getarray (lua_State *L) { 
 NumArray *a = (NumArray *)lua_touserdata(L, 1); 
int index = luaL_checkint(L, 2); 
 luaL_argcheck(L, a != NULL, 1, "'array' expected"); 
 luaL_argcheck(L, 1 <= index && index <= a->size, 2, 
 "index out of range"); 
 lua_pushnumber(L, a->values[index-1]); 
return 1; 
} 

我們定義另一個函數來獲取數組的大小:

static int getsize (lua_State *L) { 
 NumArray *a = (NumArray *)lua_touserdata(L, 1); 
 luaL_argcheck(L, a != NULL, 1, "`array' expected"); 
 lua_pushnumber(L, a->size); 
return 1; 
} 

最後,我們需要一些額外的代碼來初始化我們的庫:

static const struct luaL_reg arraylib [] = { 
 {"new", newarray}, 
 {"set", setarray}, 
 {"get", getarray}, 
 {"size", getsize}, 
 {NULL, NULL} 
}; 
int luaopen_array (lua_State *L) { 
 luaL_openlib(L, "array", arraylib, 0); 
return 1; 
} 

這兒我們再次使用了輔助庫的 luaL_openlib 函數,他根據給定的名字創建一個表,並使用 arraylib 數組中的 name-function 對填充這個表。

打開上面定義的庫之後,我們就可以在 Lua 中使用我們新定義的類型了:

a = array.new(1000) 
print(a) --> userdata: 0x8064d48 
print(array.size(a)) --> 1000 
for i=1,1000 do
 array.set(a, i, 1/i) 
end 
print(array.get(a, 10)) --> 0.1 

在一個 Pentium/Linux 環境中運行這個程序,一個有 100K 元素的數組大概佔用800KB 的內存,同樣的條件由 Lua 表實現的數組需要 1.5MB 的內存。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

Metatables

我們上面的實現有一個很大的安全漏洞。假如使用者寫了如下類似的代碼:array.set(io.stdin, 1, 0)。io.stdin 中的值是一個帶有指向流(FILE*)的指針的 userdatum。因爲它是一個 userdatum,所以 array.set 很樂意接受它作爲參數,程序運行的結果可能導致內存 core dump(如果你夠幸運的話,你可能得到一個訪問越界(index-out-of-range)錯誤)。這樣的錯誤對於任何一個 Lua 庫來說都是不能忍受的。不論你如何使用一個 C 庫,都不應該破壞 C 數據或者從 Lua 產生 core dump。

爲了區分數組和其他的userdata,我們單獨爲數組創建了一個metatable(記住userdata也可以擁有 metatables)。下面,我們每次創建一個新的數組的時候,我們將這個單獨的metatable 標記爲數組的 metatable。每次我們訪問數組的時候,我們都要檢查他是否有一個正確的 metatable。因爲 Lua 代碼不能改變 userdatum 的 metatable,所以他不會僞造我們的代碼。

我們還需要一個地方來保存這個新的 metatable,這樣我們才能夠當創建新數組和檢查一個給定的 userdatum 是否是一個數組的時候,可以訪問這個 metatable。正如我們前面介紹過的,有兩種方法可以保存 metatable:在 registry 中,或者在庫中作爲函數的upvalue。在 Lua 中一般習慣於在 registry 中註冊新的 C 類型,使用類型名作爲索引,metatable 作爲值。和其他的 registry 中的索引一樣,我們必須選擇一個唯一的類型名,避免衝突。我們將這個新的類型稱爲 “LuaBook.array”。

輔助庫提供了一些函數來幫助我們解決問題,我們這兒將用到的前面未提到的輔助函數有:

int luaL_newmetatable (lua_State *L, const char *tname); 
void luaL_getmetatable (lua_State *L, const char *tname); 
void *luaL_checkudata (lua_State *L, int index, 
 const char *tname); 

luaL_newmetatable 函數創建一個新表(將用作 metatable),將新表放到棧頂並建立表和 registry 中類型名的聯繫。這個關聯是雙向的:使用類型名作爲表的 key;同時使用表作爲類型名的 key(這種雙向的關聯,使得其他的兩個函數的實現效率更高)。luaL_getmetatable 函數獲取 registry 中的 tname 對應的 metatable。最後,luaL_checkudata檢查在棧中指定位置的對象是否爲帶有給定名字的 metatable 的 usertatum。如果對象不存在正確的 metatable,返回 NULL(或者它不是一個 userdata);否則,返回 userdata 的地址。

下面來看具體的實現。第一步修改打開庫的函數,新版本必須創建一個用作數組

metatable 的表:
int luaopen_array (lua_State *L) { 
 luaL_newmetatable(L, "LuaBook.array"); 
 luaL_openlib(L, "array", arraylib, 0); 
return 1; 
} 

第二步,修改 newarray,使得在創建數組的時候設置數組的 metatable:

static int newarray (lua_State *L) { 
int n = luaL_checkint(L, 1); 
 size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double); 
 NumArray *a = (NumArray *)lua_newuserdata(L, nbytes); 
 luaL_getmetatable(L, "LuaBook.array"); 
 lua_setmetatable(L, -2); 
 a->size = n; 
return 1; /* new userdatum is already on the stack */
} 

lua_setmetatable 函數將表出棧,並將其設置爲給定位置的對象的 metatable。在我們的例子中,這個對象就是新的 userdatum。

最後一步,setarray、getarray 和 getsize 檢查他們的第一個參數是否是一個有效的數組。因爲我們打算在參數錯誤的情況下拋出一個錯誤信息,我們定義了下面的輔助函數:

static NumArray *checkarray (lua_State *L) { 
void *ud = luaL_checkudata(L, 1, "LuaBook.array"); 
 luaL_argcheck(L, ud != NULL, 1, "`array' expected"); 
return (NumArray *)ud; 
} 

使用 checkarray,新定義的 getsize 是更直觀、更清楚:

static int getsize (lua_State *L) { 
 NumArray *a = checkarray(L); 
 lua_pushnumber(L, a->size); 
return 1; 
} 

由於 setarray 和 getarray 檢查第二個參數 index 的代碼相同,我們抽象出他們的共同部分,在一個單獨的函數中完成:

static double *getelem (lua_State *L) { 
 NumArray *a = checkarray(L); 
int index = luaL_checkint(L, 2); 
 luaL_argcheck(L, 1 <= index && index <= a->size, 2, 
 "index out of range"); 
/* return element address */ 
return &a->values[index - 1]; 
} 

使用這個 getelem,函數 setarray 和 getarray 更加直觀易懂:

static int setarray (lua_State *L) { 
double newvalue = luaL_checknumber(L, 3); 
 *getelem(L) = newvalue; 
return 0; 
} 
static int getarray (lua_State *L) { 
 lua_pushnumber(L, *getelem(L)); 
return 1; 
} 

現在,假如你嘗試類似 array.get(io.stdin, 10)的代碼,你將會得到正確的錯誤信息:

error: bad argument #1 to 'getarray' ('array' expected) 

訪問面向對象的數據

下面我們來看看如何定義類型爲對象的 userdata,以致我們就可以使用面向對象的語法來操作對象的實例,比如:

a = array.new(1000) 
print(a:size()) --> 1000 
a:set(10, 3.4) 
print(a:get(10)) --> 3.4 

記住 a:size()等價於 a.size(a)。所以,我們必須使得表達式 a.size 調用我們的 getsize函數。這兒的關鍵在於__index 元方法(metamethod)的使用。對於表來說,不管什麼時候只要找不到給定的 key,這個元方法就會被調用。對於 userdata 來講,每次被訪問的時候元方法都會被調用,因爲 userdata 根本就沒有任何 key。

假如我們運行下面的代碼:

local metaarray = getmetatable(array.new(1)) 
metaarray.__index = metaarray 
metaarray.set = array.set 
metaarray.get = array.get 
metaarray.size = array.size 

第一行,我們僅僅創建一個數組並獲取他的 metatable,metatable 被賦值給 metaarray(我們不能從 Lua 中設置 userdata 的 metatable,但是我們在 Lua 中無限制的訪問metatable)。接下來,我們設置 metaarray.__index 爲 metaarray。當我們計算 a.size 的時候,Lua 在對象 a 中找不到 size 這個鍵值,因爲對象是一個 userdatum。所以,Lua 試着從對象a 的metatable的__index域獲取這個值,正好__index就是metaarray。但是metaarray.size就是 array.size,因此 a.size(a)如我們預期的返回 array.size(a)。

當然,我們可以在 C 中完成同樣的事情,甚至可以做得更好:現在數組是對象,他有自己的操作,我們在表數組中不需要這些操作。我們實現的庫唯一需要對外提供的函數就是 new,用來創建一個新的數組。所有其他的操作作爲方法實現。C 代碼可以直接註冊他們。

getsize、getarray 和 setarray 與我們前面的實現一樣,不需要改變。我們需要改變的只是如何註冊他們。也就是說,我們必須改變打開庫的函數。首先,我們需要分離函數列表,一個作爲普通函數,一個作爲方法:

static const struct luaL_reg arraylib_f [] = { 
 {"new", newarray}, 
 {NULL, NULL} 
}; 
static const struct luaL_reg arraylib_m [] = { 
 {"set", setarray}, 
 {"get", getarray}, 
 {"size", getsize}, 
 {NULL, NULL} 
}; 

新版本打開庫的函數 luaopen_array,必須創建一個 metatable,並將其賦值給自己的__index 域,在那兒註冊所有的方法,創建並填充數組表:

int luaopen_array (lua_State *L) { 
 luaL_newmetatable(L, "LuaBook.array"); 
 lua_pushstring(L, "__index"); 
 lua_pushvalue(L, -2); /* pushes the metatable */
 lua_settable(L, -3); /* metatable.__index = metatable */
 luaL_openlib(L, NULL, arraylib_m, 0); 	//=luaL_register(L,NULL,arraylib_m)
 luaL_openlib(L, "array", arraylib_f, 0); 	//=luaL_register(L, "array", arraylib_f)
return 1; 
} 

這裏我們使用了 luaL_openlib 的另一個特徵,第一次調用,當我們傳遞一個 NULL作爲庫名時,luaL_openlib 並沒有創建任何包含函數的表;相反,他認爲封裝函數的表在棧內,位於臨時的 upvalues 的下面。在這個例子中,封裝函數的表是 metatable 本身,也就是 luaL_openlib 放置方法的地方。第二次調用 luaL_openlib 正常工作:根據給定的數組名創建一個新表,並在表中註冊指定的函數(例子中只有一個函數 new)。下面的代碼,我們爲我們的新類型添加一個__tostring 方法,這樣一來 print(a)將打印數組加上數組的大小,大小兩邊帶有圓括號(比如,array(1000)):

int array2string (lua_State *L) { 
 NumArray *a = checkarray(L); 
 lua_pushfstring(L, "array(%d)", a->size); 
return 1; 
} 

函數 lua_pushfstring 格式化字符串,並將其放到棧頂。爲了在數組對象的 metatable中包含 array2string,我們還必須在 arraylib_m 列表中添加 array2string:

static const struct luaL_reg arraylib_m [] = { 
 {"__tostring", array2string}, 
 {"set", setarray}, 
 ... 
}; 

訪問數組

除了上面介紹的使用面向對象的寫法來訪問數組以外,還可以使用傳統的寫法來訪問數組元素,不是 a:get(i),而是 a[i]。對於我們上面的例子,很容易實現這個,因爲我們的 setarray 和 getarray 函數已經依次接受了與他們的元方法對應的參數。一個快速的解決方法是在我們的 Lua 代碼中正確的定義這些元方法:

local metaarray = getmetatable(newarray(1)) 
metaarray.__index = array.get 
metaarray.__newindex = array.set 

(這段代碼必須運行在前面的最初的數組實現基礎上,不能使用爲了面向對象訪問的修改的那段代碼)

我們要做的只是使用傳統的語法:

a = array.new(1000) 
a[10] = 3.4 -- setarray 
print(a[10]) -- getarray --> 3.4 

如果我們喜歡的話,我們可以在我們的 C 代碼中註冊這些元方法。我們只需要修改我們的初始化函數:

int luaopen_array (lua_State *L) { 
 luaL_newmetatable(L, "LuaBook.array"); 
 luaL_openlib(L, "array", arraylib, 0); 
/* now the stack has the metatable at index 1 and 
 'array' at index 2 */
 lua_pushstring(L, "__index"); 
 lua_pushstring(L, "get"); 
 lua_gettable(L, 2); /* get array.get */
 lua_settable(L, 1); /* metatable.__index = array.get */
 lua_pushstring(L, "__newindex"); 
 lua_pushstring(L, "set"); 
 lua_gettable(L, 2); /* get array.set */
 lua_settable(L, 1); /* metatable.__newindex = array.set */
return 0; 
} 

Light Userdata

到目前爲止我們使用的 userdata 稱爲 full userdata。Lua 還提供了另一種 userdata: light userdata。

一個 light userdatum 是一個表示 C 指針的值(也就是一個 void *類型的值)。由於它是一個值,我們不能創建他們(同樣的,我們也不能創建一個數字)。可以使用函數lua_pushlightuserdata 將一個 light userdatum 入棧:

void lua_pushlightuserdata (lua_State *L, void *p); 

儘管都是 userdata,light userdata 和 full userdata 有很大不同。Light userdata 不是一個緩衝區,僅僅是一個指針,沒有 metatables。像數字一樣,light userdata 不需要垃圾收集器來管理她。

有些人把 light userdata 作爲一個低代價的替代實現,來代替 full userdata,但是這不是 light userdata 的典型應用。首先,使用 light userdata 你必須自己管理內存,因爲他們和垃圾收集器無關。第二,儘管從名字上看有輕重之分,但 full userdata 實現的代價也並不大,比較而言,他只是在分配給定大小的內存時候,有一點點額外的代價。

Light userdata 真正的用處在於可以表示不同類型的對象。當 full userdata 是一個對象的時候,它等於對象自身;另一方面,light userdata 表示的是一個指向對象的指針,同樣的,它等於指針指向的任何類型的 userdata。所以,我們在 Lua 中使用 light userdata表示 C 對象。

看一個典型的例子,假定我們要實現:Lua 和窗口系統的綁定。這種情況下,我們使用 full userdata 表示窗口(每一個 userdatum 可以包含整個窗口結構或者一個有系統創建的指向單個窗口的指針)。當在窗口有一個事件發生(比如按下鼠標),系統會根據窗口的地址調用專門的回調函數。爲了將這個回調函數傳遞給 Lua,我們必須找到表示指定窗口的 userdata。爲了找到這個 userdata,我們可以使用一個表:索引爲表示窗口地址的light userdata,值爲在 Lua 中表示窗口的 full userdata。一旦我們有了窗口的地址,我們將窗口地址作爲 light userdata 放到棧內,並且將 userdata 作爲表的索引存到表內。(注意這個表應該有一個 weak 值,否則,這些 full userdata 永遠不會被回收掉。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章