lua是一種嵌入式的語言,意味著lua可以用來嵌入其他應用的程序。lua API是lua與宿主語言(通常為C/C++)通訊交互的橋梁,是一個C/C++代碼與lua進行交互的函數集。lua共約有一百多個API(如lua5.3有129個),主要分為stack操作、函數導入與調用、table操作、協程、調試5類,本文分析stack操作、函數調用、table操作3類的功能與使用要點。
準備知識:1.lua_State在lua API中,幾乎所有函數的都有一個lua_State類型的參數,lua_State數據包括當前腳本環境的運行狀態信息,還會有gc相關的信息。luaL_newstate用於創建一個新的lua_State,lua API調用時操作會在第一個參數傳入的lua_State上進行,lua_close用於關閉釋放lua_State。
2.lua數據類型lua語言中變量沒有強制類型,但是在執行過程中如果類型不匹配則會產生運行錯誤,而在C/C++中用lua API對lua變量的操作具有強制類型。lua內置類型有LUA_TNIL、LUA_TNUMBER、LUA_TBOOLEAN、LUA_TSTRING、LUA_TTABLE、LUA_TFUNCTION、LUA_TUSERDATA、LUA_TTHREAD、LUA_TLIGHTUSERDATA 9種。lua_type、lua_typename兩個函數可以用來識別一個變量數據類型,lua_type返回一個指示類型的整數,lua_typename能夠依據該整數返回字符串,用lua_typename(L, lua_type(L, index))可以獲取指定棧元素的類型字符串表示。
3.userdatalua與C/C++交互時可以用userdata類型變量傳遞數據,userdata允許將C/C++中的數據保存在lua變量中。 userdata類型的值是一個內存塊, 有兩種: 完全用戶數據(full user data),指一塊由lua管理的內存對應的對象;輕量用戶數據(light user data),則指一個簡單的C指針。 用戶數據在lua中除了賦值與相等性判斷之外沒有其他預定義的操作,使用元表可以給完全用戶數據定義一系列的操作。userdata只能通過C API而無法在lua代碼中創建或者修改,數據僅被宿主程序所控制。userdata操作API有lua_newuserdata、lua_pushlightuserdata、lua_touserdata、lua_isuserdata、lua_islightuserdata,在使用lua_newuserdata函數創建userdata空間時,一種常見的寫法是採用replacement new:
new(lua_newuserdata(L,sizeof(int))) int(10);
寫法等效於
int* udptr=(int*)lua_newuserdata(L,sizeof(int);*udptr=10;
創建的userdata可以形成命名變量或者關聯到table欄位或者作為函數upvalue使用。
stack操作類API:lua API裡一類重要操作是對棧(stack)的操作,lua使用一個虛擬棧來和C互傳數據。棧上的的每個元素都是一個lua數據,對棧的API查詢操作都不嚴格遵循棧的操作規則。statck對lua語言層是透明的。
棧索引API中的函數若需要傳入棧索引,這個索引必須是有效索引或是可接受索引。有效索引指引用棧內真實位置的索引; 即在1到棧頂之間的位置 (1 ≤ abs(index) ≤ top)。正索引處棧底的1往棧頂方向增長,負索引為從棧頂第一個元素的-1絕對值依次增大,例如棧頂元素的負索引表示為-1,次棧頂元素為-2。棧中元素即可以使用正索引也可以使用負所以進行訪問。
對棧操作的API主要有三類:入棧、出棧、取值。
入棧入棧類函數用於將指定數據放到lua棧頂,對不同數據類型有不同的API函數, lua_pushboolean、lua_pushcclosure、lua_pushcfunction、lua_pushfstring、lua_pushglobaltable、lua_pushinteger、lua_pushlightuserdata、lua_pushlstring、lua_pushnil、lua_pushnumber 這些函數作用相同但是操作的數據類型不統一,在實際中常常將其採用模板形式的形式將其封裝。 還有兩個棧操作函數lua_pushvalue、lua_insert,將棧頂元素移動到其它位置。
出棧出棧函數從lua棧頂彈出一定數量元素,函數只有lua_pop。
取值函數可以從lua棧中取處元素值,函數有 lua_toboolean、lua_tocfunction、lua_tointeger、lua_tointegerx、lua_tolstring、lua_tolstring、lua_tonumber、lua_tonumberx、lua_topointer、lua_tostring,這些函數從lua棧指定位置處取得數據,不影響棧中數據。
除了上述API顯性主動地從lua棧中插入或彈出元素,其它API在執行過程中也會附帶從棧中彈出或插入數據。
其它棧函數另外對棧上元素還有的操作函數有: lua_copy 棧元素複製 lua_concat 棧元素連接 lua_compare 棧元素比較 lua_arith 對棧元素執行運算 lua_replace 元素賦值替換
棧元素命名與命名元素壓棧對棧中的數據,可以對其命名形成lua符號使該數據可以在lua語言中訪問或通過接口訪問,函數為lua_setglobal。原型為 void lua_setglobal (lua_State *L, const char *name); 函數會將棧頂的元素命名為以第二個參數命名的lua符號,並且將該棧頂元素出棧。
cpp:lua_pushstring(L, "lua");lua_setglobal("var_a");lua_pushnumber(L, 123);lua_setglobal("var_b");luaL_dofile("1.lua")1.lua:print(var_a)print(var_b)
上邊代碼等效於lua代碼:
var_a="lua"var_b=123print(var_a)print(var_b)
由lua_setglobal形成的lua語言符號與在lua語言中定義的符號在後續使用中是一致的。 對於已有的lua符號,可以使用lua_getglobal對該變量訪問,該變量會被放置到棧頂。
2.lua:var_c=123cpp:luaL_dofile("2.lua")lua_getglobal(L, "var_c")int c=lua_tonumber(L,-1)std::out<<c;
上述代碼中c++從lua語言中定義的變量符號取得數據。
函數導入與調用:C/C++函數導入到lua C/C++中的函數可通過lua_pushcfunction函數導入到lua中。函數原型為
void lua\_pushcfunction (lua\_State *L, lua\_CFunction f);
其中lua_CFunction定義為
typedef int (*lua\_CFunction) (lua\_State *L);
是一個函數指針,函數會將一個C函數壓棧。 這個函數接收一個C函數指針,並將一個類型為LUA_TFUNCTION的lua值壓棧。
3.lua:print(csum(2,3))cpp:int csum_for_lua(lua_State* l){int a = lua_tointeger(l,1);int b = lua_tointeger(l,2);lua_pushinteger(l,a+b);return 1;}lua_pushcfunction(lua_state,csum_for_lua); lua_setglobal(lua_state,"csum");luaL_dofile(lua_state,"3.lua");
當C函數被創建出來,我們有可能會把一些值關聯在一起,也就是創建一個C閉包;這些被關聯起來的值被叫做上值,它們可以在函數被調用的時候訪問的到。無論何時去調用C函數,函數的上值都可以用偽索引定位。 我們可以用lua_upvalueindex這個宏來生成這些偽索引。 第一個關聯到函數的值放在lua_upvalueindex(1) 位置處,依此類推。 lua_pushcclosure創建新的C閉包並且壓到棧頂。函數原型為
void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n)
第二個參數為c函數指針,第三個參數指定有多少個值需要關聯到該函數上,lua_pushcclosure 也會把這些值從棧上彈出。 代碼段:
4.luaprint(ccnt())print(ccnt())print(ccnt())cpp:int cnt(lua_State* L) { int val = lua_tointeger(L,lua_upvalueindex(1));//pesudo-index access lua_pushinteger(L,++val); lua_pushvalue(L,-1); lua_replace(L,lua_upvalueindex(1));//assign element value return 1;}int newcnt(lua_State* L){ lua_pushinteger(L,0); lua_pushcclosure(L,cnt,1); lua_setglobal(L,"ccnt"); return 1;} newcnt(lua_state); luaL_dofile(lua_state,"4.lua");
輸出1 2 3。C閉包裡的upvalue作用與C函數裡的靜態變量類似。
void lua_call (lua_State *L, int nargs, int nresults); C/C++中調用lua函數時,要調用的函數應該被壓入棧; 接著,把需要傳遞給這個函數的參數按正序壓棧; 這是指第一個參數首先壓棧。 最後調用一下 lua_call; nargs是你壓入棧的參數個數。 當函數調用完畢後,所有的參數以及函數本身都會出棧。 而函數的返回值這時則被壓棧。 返回值的個數將被調整為nresults個,除非 nresults 被設置成LUA_MULTRET。 在這種情況下,所有的返回值都被壓入堆棧中。 lua會保證返回值都放入棧空間中。 函數返回值將按正序壓棧(第一個返回值首先壓棧),因此在調用結束後,最後一個返回值將被放在棧頂。這裡的lua函數既可以是lua語言中的函數,也可以是通過在C/C++中通過lua_pushcfunction、lua_pushcclosure形成的函數。與lua_call類似的還有xpcall、lua_pcall、lua_callk、lua_pcallk等函數,這些函數還有錯誤處理等功能稱之為lua_call系列函數。通過lua_call類的函數可以在c/c++中執行lua函數。 如代碼:
5.lua:function lua_add (x, y) return x+yendcpp:int c_add (lua_State* L,int x, int y ) { int sum; lua_getglobal(L, "lua_add"); lua_pushnumber(L, x); lua_pushnumber(L, y); lua_call(L, 2, 1); sum = (int)lua_tonumber(L, -1); lua_pop(L, 1); return sum; }
函數c_add裡面調用了lua函數lua_add求和。調用時先取出lua_add函數的符號放到棧中,然後將lua函數的參數依次入棧,然後執行luacall(L, 2, 1),其中2即為lua函數參數個數,1為返回參數個數。調用完lua函數後,結果放在棧頂,取值後使用luapop清除在棧頂的結果。
table操作類:table的操作有創建、欄位訪問、欄位賦值、遍歷、元表存取幾類。 table創建函數為 void lua_newtable (lua_State *L); 創建一個table並將其壓棧。 table欄位訪問函數有
int lua_rawget (lua_State *L, int index);void lua_rawgeti (lua_State *L, int index, lua_Integer i);int lua_gettable (lua_State *L, int index);int lua_getfield (lua_State *L, int index, const char *k);
lua_rawget調用時第二個參數指定table,並以棧頂元素作為key,獲取table[key]後放到棧頂。lua_rawseti與lua_rawget類似,但是key是從第三個參數使用字符串傳入的。lua_rawget和lua_rawgeti在獲取table的欄位時不會觸發"index"元方法(如果設定)。lua_gettable與lua_rawget類似,但是會觸發"index"元方法(如果設定),lua_getfield與lua_gettable一樣會觸發"index"元方法,不同在於lua_getfield的key是從第三個參數傳入的。
table欄位賦值函數有
void lua_rawset (lua_State *L, int index);void lua_rawseti (lua_State *L, int index, lua_Integer i);void lua_settable (lua_State *L, int index);void lua_setfield (lua_State *L, int index, const char *k);
lua_rawset調用時第二個參數指定table,並以棧頂元素作為value,次棧頂元素為key,執行table[key]=value,設置完成後從棧中移除key與value。lua_rawseti與lua_rawset類似,但是key是從第三個參數傳入的。lua_rawset和lua_rawseti在獲取table的欄位時不會觸發"newindex"元方法(如果已設定)。lua_settable與lua_rawset類似,但是會觸發"newindex"元方法(如果已設定),lua_setfield與lua_settable一樣會觸發"newindex"元方法,不同在於lua_setfield的key是從第三個參數傳入的。
table欄位賦值函數使用示例代碼:
cpp:lua_newtable(L);lua_pushstring(L, "a");lua_pushstring(L, "lua");lua_rawset(L, -3);lua_pushstring(L, "b");lua_pushinteger(L,123);lua_rawset(L, -3);lua_setglobal(L, "c");
等價lua代碼為:
c ={["a"]="lua",["b"]=123}
table遍歷遍歷table主要函數為lua_next與lua_len。lua_next原型為 int lua_next (lua_State *L, int index); 從棧頂彈出一個鍵, 然後把索引指定的表中的一個鍵值對壓棧 (彈出的鍵之後的 「下一」 對)。 如果表中已無待遍曆元素, 那麼lua_next 將返回 0 (什麼也不壓棧)。 如下代碼可對棧頂table進行遍歷訪問:
6.lua:t={1,true,"1"}cpp:void traveltable(lua_State *L){ int nIndex = lua_gettop(L); lua_pushnil(L); while(0!= lua_next(L,nIndex)) { //here stack element index -2 is key,-1 is value lua_pop( L, 1 ); }}luaL_dofile(lua_state,"6.lua");lua_getglobal(L, "t");traveltable(L);
與table遍歷相關lua_len函數可以獲取原型為 void lua_len (lua_State *L, int index); 返回給定索引的值的長度,函數與lua語言中「#」操作等價。
元表操作通常lua中的每個值都有一套預定義的操作集合,可以通過元表來修改一個值得行為,使其在面對一個非預定義的操作時執行一個指定的操作。這些操作稱為元方法,定義這些操作的集合叫做元表。lua中的table與userdata類型的數據可以設置元表,元表操作API有lua_getmetatable與lua_setmetatable,分別用戶獲取和設置元表。 int lua_getmetatable (lua_State *L, int index); 如果該索引處的值有元表,則將其元表壓棧,返回1 。 否則不會將任何東西入棧,返回0。 void lua_setmetatable (lua_State *L, int index); 把一張表彈出棧,並將其設為給定索引處的值的元表。
總結stack操作、函數導入與調用、table操作是lua API中的主要部分,理解了這幾類lua API的函數工作方式後,可以理解常見的lua/C++綁定通信庫工作原理,甚至可以修改或封裝出我們自己的lua/C++綁定通信庫。
長按關注黑斑馬微信公眾號
歡迎訪問「百納遊戲團隊」技術博客:http://blog.dolphin-game.com