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



