前言
在《Lua“控制”C》中對Lua調(diào)用C函數(shù)做了初步的學(xué)習(xí),而這篇才是重中之重,這篇文章會重點(diǎn)的總結(jié)C模塊編寫過程中遇到的一些問題,比如數(shù)組操作、字符串操作和C函數(shù)的狀態(tài)保存等問題?,F(xiàn)在就開始吧。
數(shù)組操作
在Lua中應(yīng)該不能叫數(shù)組,而是一種table的東西;而在C語言中,沒有table這種東西,只有數(shù)組。Lua中的table可以使關(guān)聯(lián)的,也就是key=>value鍵值對,而C中,數(shù)組不是關(guān)聯(lián)的,下標(biāo)是從0開始的。當(dāng)然了,Lua中的數(shù)組表示,只是table的一個子集,就是因?yàn)檫@種關(guān)系,就有了C數(shù)組和Lua table的交互關(guān)系了。
比如lua_settable和lua_gettable這種操作table的API(其實(shí)之前我一直用的都是lua_setfield和lua_getfield),也可以操作數(shù)組。然而,API為數(shù)組操作提供了專門的函數(shù),出于以下兩個原因:
- 性能;我們一般使用C語言來擴(kuò)展Lua,都是用來做一些Lua難以做到,而C卻非常容易做到的事情,比如一些追求效率的算法;如果提高了訪問數(shù)組的效率,那就能提高整個算法的性能了;
- 便利;整數(shù)key是非常常用的,所以提供專門的API也會非常便利的。
API為數(shù)組操作提供了兩個函數(shù):
void lua_rawgeti(lua_State *L, int index, int key); void lua_rawseti(lua_State *L, int index, int key);
lua_rawgeti和lua_rawseti的參數(shù)中涉及到兩個索引,index表示table在棧中的位置,key表示元素在table中的元素。這兩個函數(shù)都是原始操作,比涉及元表的table訪問更快。通常,作為數(shù)組使用的table很少會用到元表。
下面就來一個實(shí)例,看看如何使用上面的兩個API函數(shù),不知道你會不會PHP,在PHP中,有一個array_walk函數(shù),這個函數(shù)允許用戶定義一個函數(shù),然后對數(shù)組中的每個函數(shù)都應(yīng)用這個函數(shù)。我現(xiàn)在就來實(shí)現(xiàn)這個功能。把重點(diǎn)代碼貼上來:
static int array_walk(lua_State *L) { // 和寫別的函數(shù)一行,先檢查參數(shù)的合法性 // 第一個參數(shù)必須是一個table luaL_checktype(L, 1, LUA_TTABLE); // 第二個參數(shù)必須是一個用戶定義的函數(shù) luaL_checktype(L, 2, LUA_TFUNCTION); // 獲取table的大小 int iLen = lua_objlen(L, 1); for (int i = 1; i <= iLen; ++i) { // 將用戶定義的函數(shù)壓入棧 lua_pushvalue(L, 2); // 將參數(shù)table的所以i對應(yīng)的值壓入棧 lua_rawgeti(L, 1, i); // 調(diào)用用戶定義的函數(shù) lua_call(L, 1, 1); lua_rawseti(L, 1, i); } // 沒有返回值壓入棧中 return 0; }
代碼比較簡單,不多說,哪里不懂的地方,可以留言。對于代碼中出現(xiàn)的luaL_checktype和lua_call函數(shù),這里說一下。luaL_checktype用來檢查給定的參數(shù)符合特定的類型,從而防止由于參數(shù)類型錯誤而引起的后續(xù)錯誤;如果參數(shù)不正確,這個函數(shù)就會引發(fā)一個錯誤。
lua_call運(yùn)行在無保護(hù)的模式下,這個是它和lua_pcall最大的區(qū)別,所以它在發(fā)生錯誤時,會傳播錯誤,而不是簡單的返回一個錯誤代碼。在我們的實(shí)際編程開發(fā)中,在一個應(yīng)用程序中編寫主函數(shù)時,不應(yīng)該使用lua_call,因?yàn)檫@樣需要捕獲所有的錯誤;而編寫C函數(shù)時,通??梢杂胠ua_call,當(dāng)錯誤發(fā)生時,就應(yīng)該讓錯誤顯示出來。
上面只是貼出了關(guān)鍵代碼,可以點(diǎn)擊這里下載完整工程。
字符串操作
實(shí)際開發(fā)中,我們都是在和各種字符串打交道,現(xiàn)在我們就來完成這個功能,Lua傳進(jìn)一個字符串到C模塊中,C模塊進(jìn)行字符串處理。
當(dāng)一個C函數(shù)從Lua接收到一個字符串參數(shù)時,必須遵守兩條規(guī)則:
- 不要在訪問字符串時,從棧中彈出它;
- 不要修改字符串。
當(dāng)一個C函數(shù)需要創(chuàng)建一個字符串返回給Lua時,C代碼還必須處理字符串緩沖的分配和釋放等問題。Lua API也提供了一些函數(shù)來幫助完成這些任務(wù)。
標(biāo)準(zhǔn)API為兩種常用的字符串操作提供了支持:提取子串和字符串連接。lua_pushlstring支持提取子串,它接受一個額外的字符串長度參數(shù),這就好比我們在壓入棧時,對字符串進(jìn)行了一個截取操作。下面我先來完成一個簡單的功能,根據(jù)指定的切割符號來切割字符串,將子串保存在一個table中,然后向Lua返回這個table。來吧?。。?/p>
static int split(lua_State *L) { // 傳進(jìn)來兩個參數(shù),先檢查參數(shù)的合法性 const char *pSrc = luaL_checkstring(L, 1); const char *pSep = luaL_checkstring(L, 2); lua_newtable(L); int index = 1; char *pLocation = NULL; while ((pLocation = strchr(pSrc, *pSep)) != NULL) { // 壓入字符串 lua_pushlstring(L, pSrc, pLocation - pSrc); // 設(shè)置結(jié)果表 lua_rawseti(L, -2, index++); // 跳過分隔符 pSrc = pLocation + 1; } // 把最后一部分壓入table中 // eg.abc,def,cg // 現(xiàn)在把cg放到結(jié)果表中 lua_pushstring(L, pSrc); lua_rawseti(L, -2, index); return 1; }
把重點(diǎn)代碼貼上來了。無需多解釋,慢慢看,能看懂的。Lua測試代碼如下:
require "split" local str = "abc,de,fg" local strsep = "," local tbRet = MySplit.split(str, strsep) for _, v in pairs(tbRet) do print(v) end
單擊這里下載完整項(xiàng)目代碼。
為了連接字符串,Lua API提供了一個叫l(wèi)ua_concat的函數(shù)。它類似于Lua中的“..”操作符。不過,它可以同時連接多個字符串,調(diào)用lua_concat(L, n)連接(并彈出)棧頂?shù)膎個值,然后壓入結(jié)果。此外,這個函數(shù)會將數(shù)字轉(zhuǎn)換為字符串,并在需要的時候調(diào)用元方法(__tostring)。還有另外一個有用的函數(shù)是lua_pushfstring,這個函數(shù)和C中的sprintf有點(diǎn)類似,它們都會根據(jù)一個格式字符串和一些額外的參數(shù)來創(chuàng)建一個新字符串;但是與sprintf不同的是,無需提供這個新字符串的緩沖。Lua會動態(tài)的創(chuàng)建一個足夠大的緩沖區(qū)來存放字符串,確保不會有緩沖溢出的問題。這個函數(shù)會將結(jié)果字符串壓入棧中,并返回一個指向它的指針,當(dāng)前這個函數(shù)接受的指示符只有以下幾種:
- %%,表示字符%;
- %s,表示字符串;
- %d,表示整數(shù);
- %f,表示Lua中的數(shù)字, 即雙精度浮點(diǎn)數(shù);
- %c,接受一個整數(shù),并將它格式化為一個字符,和string.char功能類似。
除了上述列出的指示符以外,它不接受任何其它選項(xiàng)。
如果只是連接一些字符串的話,這樣簡單的工作,lua_concat和lua_pushfstring就能夠很簡單的完成;但是,如果要連接很多字符串的話,為了提高效率,我們可以使用輔助庫,也就是lauxlib.h中定義的API函數(shù)來完成這項(xiàng)工作。輔助庫提供了什么呢?它提供了一種緩沖機(jī)制,包含了兩個層面的緩沖:
- 在本地緩沖區(qū)中收集較小的字符串,并在本地緩沖區(qū)滿了以后,將結(jié)果傳遞給Lua(通過lua_pushlstring);
- 使用lua_concat或其它算法來連接多次緩沖區(qū)填滿后的結(jié)果。
為了更好的描述輔助庫的緩沖機(jī)制,來看一段string.upper的源代碼,可以去Lua源代碼中的lstrlib.c文中查看。
static int str_upper (lua_State *L) { size_t l; size_t i; luaL_Buffer b; const char *s = luaL_checklstring(L, 1, &l); luaL_buffinit(L, &b); for (i=0; i<l; i++) luaL_addchar(&b, toupper(uchar(s[i]))); luaL_pushresult(&b); return 1; }
不要驚訝,Lua的代碼你可以隨心所欲的閱讀,偉大的開源,分享的力量。使用緩沖區(qū)分為以下幾步:
- 聲明一個luaL_Buffer變量;
- 使用luaL_buffinit來初始化它;
- 調(diào)用luaL_add*系列函數(shù)向緩沖區(qū)添加字符或字符串;
- 調(diào)用luaL_pushresult更新緩沖區(qū),將最終的結(jié)果字符串留在棧頂。
在調(diào)用luaL_buffinit初始化以后,這個變量中就會保留一份狀態(tài)L的副本,所以在后續(xù)調(diào)用luaL_add*系列函數(shù)時,就不用傳遞lua_State參數(shù)了。
通過使用這些函數(shù),就可以使用緩沖機(jī)制,我們也不用再去關(guān)心緩沖的分配、溢出等細(xì)節(jié)了。另外,這種連接算法也非常高效。用str_upper函數(shù)處理大型的字符串也不會有什么問題。
總結(jié)
這篇《再說C模塊的編寫(1)》到這里就結(jié)束了。由于篇幅不夠,還有一部分內(nèi)容,且非常重要的內(nèi)容沒有總結(jié),在下一篇《再說C模塊的編寫(2)》中,就對剩下的那部分非常重要的內(nèi)容,單獨(dú)進(jìn)行總結(jié)。希望我這里總結(jié)的內(nèi)容,大家能看懂,能明白;同時,我也希望大家能和我進(jìn)行更多的交流,大家互相提升。
2014年8月26日 于深圳。