Lua_第26章撰寫 C 函數的技巧

第 26章 撰寫 C 函數的技巧

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

26.1 數組操作
       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 是比較好的想法,如果有錯誤發生,把錯誤留給關心她的人去處理

26.2 字符串處理
        當 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, ...);
       這個函數某種程度上類似於 C 語言中的 sprintf,根據格式串 fmt 的要求創建一個新的字符串。與 sprintf 不同的是,你不需要提供一個字符串緩衝數組,Lua 爲你動態的創建新的字符串,按他實際需要的大小也不需要擔心緩衝區溢出等問題。這個函數會將結果字符串放到棧內,並返回一個指向這個結果串的指針。當前,這個函數只支持下列幾個指示符: %%(表示字符 '%')、%s(用來格式化字符串)、%d(格式化整數)、%f (格式化 Lua 數字,即 doubles)和 %c(接受一個數字並將其作爲字符),不支持寬度 和精度等選項。
       當我們打算連接少量的字符串的時候lua_concatlua_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);
當然,如果位於棧頂的值不是字符串或者數字的話,調用這個函數將會出錯。

26.3 在 C 函數中保存狀態
       通常來說,C 函數需要保留一些非局部的數據,也就是指那些超過他們作用範圍的數據。C 語言中我們使用全局變量或者 static 變量來滿足這種需要。然而當你爲 Lua 設 計一個程序庫的時候,全局變量和 static 變量不是一個好的方法首先,不能將所有的 (一般意義的,原文 generic)Lua 值保存到一個 C 變量中。第二,使用這種變量的庫不能在多個 Lua 狀態的情況下使用。
一個替代的解決方案是將這些值保存到一個 Lua 全局變兩種,這種方法解決了前面 的兩個問題。Lua 全局變量可以存放任何類型的 Lua 值,並且每一個獨立的狀態都有他 自己獨立的全局變量集。然而,並不是在所有情況下,這種方法都是令人滿意地解決方案,因爲 Lua 代碼可能會修改這些全局變量,危及 C 數據的完整性。爲了避免這個問題, Lua 提供了一個獨立的被稱爲 registry 的表,C 代碼可以自由使用,但 Lua 代碼不能訪問他

26.3.1The Registry

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

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

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

       你應該記住,永遠不要使用數字作爲 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,任何釋放他 的操作都沒有效果。

26.3.3Upvalues
       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 指向同一個表,這樣這個表就變成了一個 所有函數共享數據的地方。
發佈了456 篇原創文章 · 獲贊 223 · 訪問量 112萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章