Lua_第24章 擴展你的程序

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

-- configuration filefor program 'pp'
-- define windowsize
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 包並加載了標準函數庫C雖然這是可選的,但通常包含這些 庫是比較好的編程思想)。然後程序使用 luaL_loadfile 方法根據參數 filename 加載此文件 中的信息塊並調用 lua_pcall 函數運行,這些函數運行時若發生錯誤(例如配置文件中有 語法錯誤),將返回非零的錯誤代碼並將此錯誤信息壓入棧中。通常,我們用帶參數 index 值爲-1 的 lua_tostring 函數取得棧頂元素。
         解析完 取得 的信息 塊後 ,程序會取得全局變量值。爲此,程序調用了兩次 lua_getglobal 函數,其中一參數爲變量名稱。每調用一次就把相應的變量值壓入棧頂,所以變量 width的 index 值是-2 而變量 height 的 index 值是-1(在棧頂)。(因爲先前的棧是空的,需要從棧底重新索引,1 表示第一個元素 2表示第二個元素。由於從棧頂索引, 不管棧是否爲空,你的代碼也能運行)。接着,程序用 lua_isnumber 函數判斷每個值是否 爲數字。lua_tonumber 函數將得到的數值轉換成 double 類型並用(int)強制轉換成整型。 最後,關閉數據流並返回值。
         Lua 是否值得一用?正如我前面提到的,在這個簡單的例子中,相比較於 lua用一個 只包含有兩個數字的文件會更簡單。即使如此,使用 lua 也帶來了一些優勢。首先,它 爲你處理所有的語法細節(包括錯誤);你的配置文件甚至可以包含註釋!其次,用可以 用 lua 做更多複雜的配置。例如,腳本可以向用戶提示相關信息,或者也可以查詢環境 變量以選擇合適的大小:
 

-- configuration filefor program 'pp'
if getenv("DISPLAY") == ":0.0" then
width = 300; height= 300
else
width = 200; height= 200
end
       在這樣簡單的配置情節中,很難預料用戶想要什麼;不過只要腳本定義了這兩個變 量,你的 C 程序無需改變就可運行。最後一個使用 lua  的理由:在你的程序中很容易的加入新的配置單元。方便的屬性 添加使程序更具有擴展性。

24.1表操作

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

-- configuration filefor 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}
表的使用給腳本的結構帶來很多靈活性,現在對於用戶C或者應用程序)很容易預 定義一些顏色,以便將來在配置中使用:
 
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 validcolor table");
 
red = getfield("r"); 
green = getfield("g"); 
blue =getfield("b");


一般來說,我們首先獲取全局變量 backgroud 的值,並保證它是一個 table。然後, 我們使用 getfield 函數獲取每一個顏色組件。這個函數不是 API 的一部分,我們需要自己定義他:
 
#define MAX_COLOR      255
/* assume thattable is on the stacktop */
int getfield (const char *key) {
   int result; 
   lua_pushstring(L, key);
   lua_gettable(L, -2); /* get 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); /* 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 thattable 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);
}
       與其他的 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 和全局變量),最終用戶可能只是用其中幾個,這會讓人覺得很怪異。在使用字符串表示的時候,應避免這種情況出現。
 
24.2調用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  函數:
 
<pre name="code" class="csharp">/* 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。 25.3 通用的函數調用        看一個稍微高級的例子,我們使用 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 ofarguments 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 thecall */
             nres =strlen(sig);  /* numberof expected results*/
             if (lua_pcall(L, narg, nres,0) != 0) /* do thecall */
                error(L, "error runningfunction `%s': %s",func, lua_tostring(L, -1));
 
             /* retrieve results*/
             nres =-nres;     /* stackindex of firstresult */
             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, constchar **) = lua_tostring(L, nres);
                break;

      default:
         error(L, "invalid option (%c)", *(sig- 1));
      }
    nres++;
   }
    va_end(vl);
}

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

發佈了456 篇原創文章 · 獲贊 223 · 訪問量 112萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章