天天看點

第二十五課 擴充應用程式

基礎 第一個任務是一個簡單的配置應用。假設C程式有一個視窗,并希望使用者指定視窗的初始大小。顯然,對于這種簡單的任務,有多種比Lua 更簡單的做法,例如使用環境變量或者使用記錄了名值對的檔案。不過就算使用一個簡單的文本檔案,也需要進行分析。是以使用Lua來作為配置檔案。下面是這種檔案最簡單的形式,它可以包含如下内容: --定義視窗大小 width = 200 height = 300 此時,必須用Lua API來指揮Lua分析這個檔案,并擷取全局變量width和height的值。下面這個 load函數完成了此項工作: void load (lua_State *L, const char *fname, int *w, int *h) { if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0)) { error(L, "cannot run config.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"); } *w = lua_tointeger(L, -2); *h = lua_tointeger(L, -1); }

假設已經建立了一個Lua狀态,這個函數調用 luaL_loadfile從檔案fname加載程式塊,然後調用lua_pcall運作編譯好的程式塊。若發生錯誤(例如配置檔案中的文法錯誤),這兩個函數就會把錯誤消息壓入棧,并傳回一個非0的錯誤代碼。此時,程式就調用 lua_tostring從棧頂擷取該消息。 當運作完程式塊後,程式需要擷取全局變量的值。程式調用了lua_getglobal兩次,這個函數除了第一個正常的lua_State參數外,還需要變量的名稱。每次 調用這個函數,它都會将相應的全局變量值壓入棧中。是以width處于索引-2上,height位于 索引-1上。另外,由于 棧事先是空的,也可以從棧底進行 索引,第一個值使用索引1,第二個值使用2。不過,自上向下的索引可以使代碼即使是在棧不為空的情況下依然可以工作。接下來程式調用lua_isnumber來檢查兩個值是否為數字。最後調用 lua_tointeger将這些值轉換為整數,并賦予對應的參數變量。 那麼是否值得用Lua來完成這類任務呢?其實對于這類簡單的任務,用一個簡單的檔案來記錄這兩個數字就足夠了,這比Lua更易于使用。但使用Lua卻可以帶來一些優勢。首先,Lua會處理所有的文法細節或文法錯誤,包括配置檔案中的注釋。其次,使用者可以實作一些更複雜的配置邏輯。例如,腳本可以提示使用者某些資訊,或者查詢一個環境變量來選擇合适的大小: --配置檔案 if getenv("DISPLAY") == ":0.0" then width = 300; height = 300 else width = 200; height = 200 end 即使是在這樣一個簡單的配置示例中,使用者也可能有很多實作需求。不過無論是何種實作,隻要腳本定義了這兩個變量,C程式則無須修改就能工作。 最後一個使用Lua的理由是,它更易于将新的配置機制添加到程式中。這種簡易性可以讓人形成一種态度,進而使程式變得更加靈活。

table操作 接下來要完成的任務是配置一個視窗的背景顔色。假設,顔色的格式是由3個數字組成的,每個數字都是RGB的一個顔色分量。在C語言中,這些數字通常是在區間[0,255]中的整型。但在Lua中,由于所有的數字都是實數,是以可以使用區間[0,1]。 一種基本的做法是要求使用者将每個分量設定在不同的全局變量中: --配置檔案 width = 200 height = 300 background_red = 0.30 background_green = 0.10 background_blue = 0 但是這種做法有兩個缺點:第一,它太冗長了;第二,無法預定義常用顔色。如果能定義常用顔色,使用者就可以簡單地寫出background = WHITE。為了避免這些缺點,使用table來表示顔色: background = {r=0.30, g=0.10, b=0} 使用table可以讓腳本變得更加結構化。現在,使用者就可以很容易地在配置檔案中預定義常用顔色了: 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 table"); } red = getfield(L, ""r); green = getfield(L, "g"); blue = getfield(L, "b"); 現擷取全局變量background的值,并确認其是一個table。然後,使用getfield擷取顔色中的各個分量。不過這個函數不是API函數,是以必須定義它。然而由于在這裡又遇到了多态的問題,是以需要更多版本的getfield函數,以針對不同的key類型、value類型和錯誤處理等。Lua API隻提供了一個函數lua_gettable,它能處理所有的類型。但它需要知道table在棧中的位置,然後才會從棧中彈出key,并壓入相應的value。getfield的定義如下: #define MAX_COLOR 255 int getfield(lua_State *L, const char *key) { int result; lua_pushstring(L, key); lua_gettable(L, -2); //擷取background[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); //删除數字 return result; } 這個函數假設table位于棧頂。當用 lua_pushstring壓入key後,table就位于索引-2上。在傳回前,getfield彈出從棧中檢索到的值,并使棧保持為調用前的樣子。 由于經常需要用字元串來索引table,為此Lua 5.1提供了一個 lua_gettable的特化版本lua_getfield。通過這個函數,可以将如下兩行: lua_pushstring(L, key); lua_gettable(L, -2); 重寫為: lua_getfield(L, -1, key); 由于沒有向棧中壓入字元串,是以當調用 lua_getfield時,table的索引仍為-1。 接下來繼續擴充這個示例。現在就為使用者定義 各種常用顔色。使用者除了可以使用自己建立的顔色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}, <other colors> {NULL, 0, 0, 0} }; 接下來要根據這些顔色名來建立相應的全局變量,然後用顔色table來初始化這些變量。最終結果應等價于使用者在其腳本中寫入如下内容: WHITE = {r = 1, g = 1, b = 1} RED = {r = 1, g = 0, b = 0} <其他顔色> 定義一個輔助函數setfield來設定table字段。它會将字段名和字段值壓入棧中。然後調用lua_settable: void setfield (lua_State *L, const char *index, int value) { lua_pushstring(L, index); lua_pushnumber(L, (double)value / MAX_COLOR); lua_settable(L, -3); } 就像其他API函數一樣,lua_settable能處理各種類型,它會從棧中擷取所需的操作數。lua_settable要求傳入一個table索引參數,然後它會設定這個table,并彈出key和value。setfield函數假設在調用前table已經在棧頂(索引為-1)。當壓入key和value後,table就位于索引-3。 Lua 5.1同樣為字元串key提供了一個lua_settable的特化版本,名為 lua_setfield。通過這個函數,可以将上述的setfield的定義重寫為: void setfield (lua_State *L, const char *index, int value) { lua_pushnumber(L, (double)value / MAX_COLOR); lua_setfield(L, -2, index); } 接下來的一個函數是setcolor,它用于定義一個顔色。它會建立一個table,并設定相應的字段,最後将這個table賦予相應的全局變量: void setcolor (lua_State *L, struct ColorTable *ct) { lua_newtable(L); setfield(L, "r", ct->red); setfield(L, "g", ct->green); setfield(L, "b", ct->blue); lua_setglobal(L, ct->name); } setcolor先調用lua_newtable,這個函數會建立一個空的table,并将其壓入棧中。然後,setcolor調用setfield來設定table的各個字段。最後,lua_setglobal彈出table,并根據名稱将其賦予全局變量。 通過上述函數,下面這個循環便會為配置腳本注冊所有的顔色: int i = 0; while (colortable[i].name != NULL) { setcolor(L, &colortable[i++]; } 記住,應用程式必須在運作腳本前,執行這個循環。 下面是另一種實作“具名(Named)”顔色的做法。 lua_getglobal(L, "background"); if (lua_isstring(L, -1)) { const char *colorname = lua_tostring(L, -1); int i; for (i = 0; colortable[i].name != NULL; ++i) { if (strcmp(colorname, colortable[i].name) == 0) { break; } if (colortable[i].name == NULL) { error(L, "invalid color name (%s)", colorname); } else { red = colortable[i].red; green = colortable[i].green; blue = colortable[i].blue; } } } else if (lua_istable(L, -1)) { red = getfield(L, "r"); green = getfield(L, "g"); blue = getfield(L, "b"); } else { error(L, "invalid value for 'background'"); }

這裡沒有用到全局變量,而是讓使用者用字元串來表示顔色名稱。例如,background = "BLUE"。現在,background既可以是table又可以是字元串。若以這種方式來實作,應用程式則無須在運作使用者腳本前做任何事情。不過,它需要在擷取顔色時做更多的事情。當它擷取變量background的值時,必須測試該值是否為合法的字元串,這需要在顔色表中查找該字元串。 在C程式中,用字元串來表示選項并不是一個好方法,因為編譯器無法檢測到 拼寫錯誤。在Lua中,全局變量無須聲明,是以若使用者錯誤地拼寫了一個顔色,Lua也不會報錯誤。如果使用者寫了WITE,而非WHITE, background變量會變成 nil。而應用程式卻隻知道background是nil,除此之外沒有其他資訊可以說明錯誤的原因。另一方面,使用字元串時,若background的值拼寫錯誤,則應用程式可以将這個資訊附加到錯誤消息中,還可以用大小寫無關的方式來比較字元串, 如使用者可以寫“ white”、“WHITE”或“White”。此外,如果使用者腳本很小,而顔色很多,那麼就需要注冊許多顔色,但隻有其中一些會被使用者用到。在這種情況下,使用字元串方式可以避免 這種開銷。

調用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函數來調用這個Lua函數: double f (double x, double y) { double z; lua_getglobal(L, "f"); //待調用的函數 lua_pushnumber(L, x); //壓入第一個參數 lua_pushnumber(L, y); //壓入第二個參數

if (lua_pcall(L, 2, 1, 0) != 0) { error(L, "error running function 'f' : %s", lua_tostring(L, -1)); } if (!lua_isnumber(L, -1)) { error(L, "function 'f' must return a number"); } z = lua_tonumber(L, -1); lua_pop(L, 1); return z; } 在調用lua_pcall時,第二個參數是傳給待調用函數的參數數量,第三個參數是期望的結果數量,第四個參數是一個錯誤處理函數的索引。就像Lua的指派一樣,lua_pcall會根據要求的數量來調整實際結果的數量,即壓入nil或丢棄多餘的結果。在壓入結果前, lua_pcall會先 删除棧中的函數以及其參數。如果一個函數會傳回多個結果,那麼第一個結果最先壓入。例如,函數傳回了3個結果,第一個的索引就是-3,最後一個的索引是-1。 如果在 lua_pcall的運作過程中有任何錯誤,lua_pcall會傳回一個非零值,并在棧中壓入一條錯誤消息。不過即使如此,它仍會彈出函數以及其參數。然而,在壓入錯誤消息前,如果存在一個錯誤處理函數,lua_pcall就會先調用它。通過lua_pcall的最後一個參數可以指定這個錯誤處理函數。零表示沒有錯誤處理函數,那麼 最終的錯誤消息就是原來的消息。若傳入非零參數,那麼這個參數就應該是一個錯誤處理函數在棧中索引。是以,錯誤處理函數必須先壓入棧中,也就是必須位于 待調用函數以及其參數的下面。 對于普通的錯誤,lua_pcall會傳回錯誤代碼LUA_ERRRUN。但有兩種特殊的錯誤情況,不會運作錯誤處理函數。第一種是記憶體配置設定錯誤,對于這種錯誤,lua_pcall總是傳回 LUA_ERRMEM。第二類錯誤則發生在Lua運作錯誤處理函數時,在這種情況中,是沒有必要再次調用錯誤處理函數的,是以lua_pcall會立即傳回錯誤代碼LUA_ERRERR。

一個通用的調用函數 本例作為一個更進階的示例,将編寫一個調用Lua函數的輔助函數,其中用到了C語言的可變參數機制。這個輔助函數稱為call_va,它接受一個待調用函數的名字、一個描述參數類型和結果類型的字元串,以及所有的參數變量和所有存放結果的指針。call_va會處理所有的API細節。通過這個函數,可以将前例寫為: call_va("f", "dd>d", x, y, &z); 其中字元串“dd>d"表示”兩個雙精度類型的參數和一個雙精度類型的結果“。在這段描述字元串中,可以用字母‘d'表示雙精度浮點數、’i'表示整數、‘s'表示字元串,而’>‘表示參數與結果的分隔符。如果函數沒有結果,’>'便是可選的。 以下是call_va的實作,這個函數執行了與第一示例中相同的步驟:壓入函數、壓入參數、完成調用和擷取結果。 #include <stdarg.h> void call_va (const char *func, const char *sig, ...) { va_list vl; int narg, nres; //參數和結果的數量 va_start(vl, sig); lua_getglobal(L, func); //壓入函數 //壓入參數 for (narg = 0; *sig; ++narg) { //周遊所有參數 //檢查棧中空間 luaL_checkstack(L, 1, "too many arguments"); switch (*sig++) { case 'd': { lua_pushnumber(L, va_arg(vl, double)); break; } case 'i': { lua_pushinteger(L, va_arg(vl, int)); break; } case 's': { lua_pushstring(L, va_arg(vl, char *)); break; } case '>': { goto endargs; } default: { error(L, "invalid option (%c)", *(sig - 1)); } } } endargs: nres = strlen(sig); //期望的結果數量 //函數調用 if (lua_pcall(L, narg, nres, 0) != 0) { error(L, "error calling '%s' : %s", func, lua_tostring(L, -1)); } //檢索結果 nres = -nres; //第一個結果的棧索引 while (*sig) { //周遊所有結果 switch (*sig++) { case 'd': { if (!lua_isnumber(L, nres)) { error(L, "wrong result type"); } *va_arg(vl, double *) = lua_tonumber(L, nres); break; } case 'i': { if (!lua_isnumber(L, nres)) { error(L, "wrong result type"); } *va_arg(vl, int *) = lua_tointeger(L, nres); break; } case 's': { if (!lua_isstring(L, nres)) { error(L, "wrong result type"); } *va_arg(vl, const car **) = lua_tostring(L, nres); break; } default: { error(L, "invalid option (%c)", *(sig - 1)); } } ++nres; } va_end(vl); }

以上大部分代碼都很直覺,不過有些 地方需要說明一下。首先,無須檢查func是否為一個函數,因為lua_pcall會發現這類錯誤。其次,由于它要壓入任意數量的參數,是以必須確定棧中有足夠的空間。第三,由于函數可能會傳回字元串,是以call_va不能将結果彈出棧。調用者必須在使用完字元串結果(或将字元串複制到其他緩沖)後彈出所有結果。

繼續閱讀