日期:2014.8.8
PartⅣ The C API
28 Techniques for Writing C Functions
官方的API和輔助庫都提供了幾種幫助建立C函數的機制。在這章,将會介紹數組操控,字元串操控,和在C中存儲Lua變量。
28.1 Array Manipulation
Lua中的數組,就是table在特殊使用情況下的稱呼,我們可以使用管理table的方式來管理數組,名叫:lua_settable,lua_gettable.當時,API也為數組提供了一些特殊的函數。提供這些額外函數的一個原因是性能上的考慮:通常在一個算法内部都有一個周遊數組的操作,是以任何提升這個操作的努力都會對整個算法性能的提升産生非常大的影響。另一個原有是便捷性的考慮:跟字元串型key相比,數值key已經足夠滿足日常需求了。
API提供了兩個額外的函數:
void lua_rawgeti(lua_State *L,int index,int key);
void lua_rawseti(lua_State *L,int index,int key);
這兩個函數的意思大緻為:index表示table在棧中的位置;key表示table中元素的位置。調用lua_rawgeti(L,t,key) 相當于下面(當t為正值)的這個操作:
lua_pushnumber(L,key);
lua_rawget(L,t);
而調用lua_rawseti(L,t,key)(仍然t為正值)則相當于下面的操作:
lua_pushnumber(L,key);
lua_insert(L,-2);
lua_rawset(L,t);
兩個函數都使用了raw操作,快速。當以數組方式使用table的時候很少使用元方法。
28.2 String Manipulation
當C函數從Lua中接收一個字元串型參數的時候,隻需要遵循兩個規則:當得到該參數的時候不要從棧中推出該參數;不要修改該參數。
而當需要在C中建立号字元串再傳回給Lua的時候,則需要注意得更多了。需要C代碼注意緩存的配置與釋放,緩存溢出等。此時,Lua的API就提供了一些函數來幫助處理這些。
标準庫提供了兩個字元串的的基本操作:子串的取出和字元串的組合。取出一個子串,隻需要記住lua_pushlstring 需要字元串的長度作為額外的第三個參數。是以假如想傳遞s的位置i至j的子串給lua,需要寫的便是:
lua_pushlstring(L,s+i,j-i+1);
而Lua也在API中提供了特殊的用來做字元串組合的函數,叫做lua_concat。相當于lua中的 .. 操作符:将number轉換成string然後在需要的時候會觸發元方法。不僅如此,也可以一次組合兩個以上的字元串。調用lua_concat(L,n) 将會組合(且推出)n個棧中頂部的資料,然後将結果push值棧頂。
而另一個函數是lua_pushfstring:
const char* lua_pushfstring(lua_State *L,const char *fmt,…);
這個函數類似于C中的sprintf函數,這個函數根據特定的格式和一些額外的參數來建立一個新的字元串。當然不像sprintf函數,這個函數是不需要提供一個buffer的,Lua會動态的建立一個字元串,其大小按需要調整。函數将組合結果字元串push至棧中,然後傳回一個指向該結果的指針。也不需要擔心buff的溢出。至Lua5.2,該函數接受以下格式串:
%s 插入一個以0為結尾的字元串
%d 插入一個整數
%f 插入一個Lua的number,即double
%p 插入一個指針
%c 以字元串形式插入一個整數
%% 插入字元%
當我們隻需要處理少量的字元串組合的時候,使用lua_concat 和 lua_pushfstring 是非常有效的。但是當我們需要處理多個字元串的時候,又顯得不高效了,此時借助輔助庫來使用buff特性能顯著提升效率。
28.3 Storing State in C Functions
通常,C函數需要存儲一些非局部資料,即這些資料 outlive their invocation(生存期長于其調用期?)。在C中,通常使用全局或者靜态變量來實作這個需求。但是當你使用标準庫函數的時候,使用全局和靜态變量是不太可取的。首先,不能使用C的變量來存儲一個泛型的Lua變量;第二,使用這些變量的庫将會在多種Lua state的時候無效。
Lua的函數有兩個地方用來存儲非局部資料:全局變量和非局部變量。C API也提供了兩個地方來存儲非局部資料:registry 和 upvalues。
registry是一個全局table,隻能被C代碼通路。特别的是,可以使用這個table來存儲資料共享給多個子產品,而如果想隻為一個子產品或為單個函數提供資料,那麼需要使用upvalues。
The registry
registry總是位于pseudo-index,其值是由LUA_REGISTRYINDEX定義的。pseudo-index和棧中的index類似,隻不過與之相對應的value并不在棧中。Lua API中接受indices作為參數的函數同樣接受pseudo-indices,如lua_remove 、lua_insert 函數。例如從registry中得到一個key為“Key”的值,可以如下操作:
e.g.
lua_getfield(L,LUA_REGISTRYINDEX,"Key");
registry也就是Lua中的一個table,可以使用Lua中除nil外的所有類型變量來做它的index值。然而,因為C子產品中所有的函數公用同一個registry,是以為了避免沖突,需要認真考慮選擇合适的作為key的變量類型。字元串類型是作者推薦的。
而使用數值作為key是不可取的,因為這些key是保留給reference system的。這個系統包含了一組輔助庫中的一些函數,這些函數用來允許你存儲變量到table中而不需要關心如何建立獨特的名字。函數luaL_ref 建立一個新的reference:
e.g.
int r = luaL_ref(L,LUA_REGISTERINDEX);
調用這個函數将會從棧中推出一個值,然後以一個數值key存儲至registry中。稱這個key為reference。
我們使用references主要是當我們需要在一個C的資料結構中存儲一個Lua變量的reference。之前我們已經說過,我們不應該在C函數(包含指向Lua字元串的指針)之外存儲這些指針。不僅如此,Lua也不提供指向其餘對象的指針,如table和函數。是以,我們不能通過指針指向Lua的對象,而隻能通過建立reference指向這些然後存儲在C中。
将指向某個值的reference r推進至棧中,執行如下操作:
e.g.
lua_rawgeti(L,LUA_REGISTRYINDEX,r);
最後,要釋放值和reference,需要調用函數luaL_unref:
e.g.
lua_unref(L,LUA_REGISTRYINDEX,r);
執行上述調用之後,重新調用luaL_ref 可能會再次傳回這個reference。
reference system将nil視為一個特殊情況,無論何時我們以luaL_ref調用一個nil的值,将不會建立一個新的reference,而是傳回常量LUA_REFNIL,而對這個操作執行釋放動作是無效的:
luaL_unref(L,,LUA_REGISTRYINDEX,LUA_REFNIL);
而以下操作:
lua_rawgeti(L,LUA_REGISTRYINDEX,LUA_REFNIL)将會push一個nil至棧中。
也定義了另一個常量LUA_NOREF,這是一個與任何有用的reference不同的數值,用來标記一個reference為不可用。
而另一個建立key的方式是使用靜态變量的位址:C連結器将會保證這些key是獨一無二的。這裡就需要使用lua_pushlightuserdata來實作這個操作了,使用一個C指針來push一個值至Lua的棧中。下面的例子展現了用這個方法如何向registry存儲和使用一個字元串:
e.g.
/* variable with a unique address*/
static char Key = "k";
/* store a string*/
lua_pushlightuserdata(L,(void*)&Key);
lua_pushstring(L,myStr);
lua_settable(L,LUA_REGISTRYINDEX);
/* retrieve a string*/
lua_pushlightuserdata(L,(void*)&Key);
lua_gettable(L,LUA_REGISTRYINDEX);
myStr = lua_tostring(L,-1);
而lua5.2 為了簡化使用變量的位址作為一個唯一key,提供了兩個新的函數:lua_rawgeti和lua_reaseti ,隻是使用C指針作為key,而不是整數。使用這兩個函數可以重寫之前的代碼:
e.g.
static char Key = "k";
/* store a string*/
lua_pushstring(L,myStr);
lua_rawsetp(L,LUA_REGISTRYINDEX,(void*)&Key);
/* retrieve a string*/
lua_rawgetp(L,LUA_REGISTRYINDEX,(void*)&Key);
myStr = lua_tostring(L,-1);
Upvalues
與registry存儲方式不同,upvalue實作的是類似于C中的靜态變量,隻在特定函數中可見。每次在Lua中建立一個新的C函數,都可以與之關聯任意數量的upvalues;每個upvalue可以存儲一個Lua的變量值。之後,當這個函數被調用了,可以使用 pseudo-indices 自由通路這些upvalues。
稱這種帶upvalues的C函數為closure,類似于Lua中的closure。但我們可以用同一套C函數代碼,不同的upvalues,來建立不同的closure。
以一個例子做解釋:用C建立一個newCounter函數,這個函數是一個工廠,每次通路這個函數都傳回一個新的counter函數。盡管是使用同一套代碼,但是每次都是一個獨立的counter:
e.g.
static int counter(lua_State *L); /* forward declaration*/
int newCounter(lua_State *L)
{
lua_pushinteger(L,0);
lua_pushcclosure(L,&counter,1);
return 1;
}
這裡的關鍵函數是lua_pushcclosure ,這個函數用來建立一個新的closure。函數的第二個參數是base function(什麼意思?),第三個參數是upvalues的數量。而在建立一個新的closure之前,需要将初始化的upvalues 推進至棧中。在這裡的例子中,初始化了一個0 作為closure的upvalues。
在看一下counter函數的定義:
e.g.
static int counter(lua_State *L)
{
int val = lua_tointger(L,lua_upvalueindex(1));
lua_pushinteger(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 的 pseudo-index。特别的,表達式lua_upvalueindex(1) 産生正在運作的函數的第一個upvalue的pseudo-index。這個index和棧中的index類似,隻是這些index不是在棧中而已。是以調用lua_tointeger 将會得到第一個upvalue且将其轉化為一個number。函數counter 推進一個新的值:++val,得到之前那個number的複本。
Shared upvalues
通常我們需要在一個庫中給所有的函數共享一些變量。之前我們已經可以通過registry來實作,現在我們也可以通過upvalues來實作。
不像Lua的closure,C的closure不能共享upvalues的。每個closure都有自己私有的upvalues。但是可以通過設定不同的函數來使得upvalues代表同樣的一個table,這樣這個table就成為了庫中所有函數都能共享資料的機制。
Lua5.2提供了一個函數用來實作上面的這個機制,我們已經使用luaL_newlib 來打開了C的庫,而Lua用下面的這個宏來實作剛提到的這個函數:
e.g.
#define luaL_newlib(L,l) \
(luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
這個宏就是為庫建立了一個新的table,而函數luaL_setfuncs 則将清單l中的函數加入到新的table中。
而luaL_setfuncs 的第三個參數則代表了庫中新函數有多少個upvalues,這些初始化upvalues的值也必須在棧中,即lua_pushcclosure中出現的那些。是以,建立一個庫中所有函數共享一個table作為其單一的upvalue的庫,可以使用以下代碼:
e.g.
/* create library table ('lib' is its list of function) */
luaL_newlibtable(L,lib);
/* create shared upvalue */
lua_newtable(L);
/* add functions in list 'lib' to the new library,sharing previous table as upvalue*/
luaL_setfuncs(L,lib,1);
最後的這個函數調用:luaL_setfuncs(L,lib,1) 也會将這個共享的table從棧中移除掉,而隻将該table應用于新的庫中。