因為實習需要用到lua所以最近在學習lua,在學習過程中我使用C++與lua做交互。正常來說,如果lua要調用C++的函數,C++需要返回一個整數,這個整數的值是這個C++函數需要返回給lua調用的值的個數。這樣的做法才是正確的,只是我突然間想了下,如果我返回一個不正確的值會怎樣呢?於是我這么做了,然后數據如預料之中變得很不正常。然后我又在想,為什么我返回不正確的值lua會得到這樣的數據呢。於是我開始了lua的C源碼分析。其實就是給自己挖了個大坑23333,然后我又是屬於那種有問題沒解決心里仿佛有塊石頭壓着的那種人。沒辦法,只好硬着頭皮上了。
一、問題建立
實驗代碼:
main.cpp
#include <iostream> #include <string> #include "lua.hpp" #include "Utils.hpp" int CGetPow(lua_State *l) { lua_pushstring(l, "hello"); lua_pushstring(l, "world"); StackDump(l); return 2; } int main(int argc, const char *argv[]) { using namespace std; int error,error1,error2; string fname; fname = argv[1]; lua_State *L = luaL_newstate(); luaL_openlibs(L); lua_pushstring(L, "fuck"); lua_pushcfunction(L, CGetPow); lua_setglobal(L, "pow"); error1 = luaL_loadfile(L, fname.c_str()); error2 = lua_pcall(L, 0, 0, 0); error = error1 || error2; if (error) { std::cout << fname << std::endl; fprintf(stderr, "%s\n", lua_tostring(L, -1)); lua_pop(L, 1); } lua_close(L); return 0; }
StackDump的作用是打印棧上的內容:
void StackDump(lua_State *l) { int top = lua_gettop(l); std::cout << "Stack: "; for (int i = 1; i <= top; i++) { int t = lua_type(l, i); switch (t) { case LUA_TSTRING: std::cout << "'" << lua_tostring(l, i) << "'"; break; case LUA_TBOOLEAN: std::cout << lua_toboolean(l, i) ? "true" : "false"; break; case LUA_TNUMBER: std::cout << lua_tonumber(l, i); break; default: std::cout << lua_typename(l, t); break; } std::cout << "\t"; } std::cout << std::endl; }
test.lua
print(pow(2,3))--這句是最主要的,下面三個輸出只是為了對比函數而已 print("print",print) print("pow",pow) print("test.lua",debug.getinfo(1, "f").func)
現在,CGetPow返回的整數是2,而函數中push的內容也的確只有兩個,所以,它會很正常的返回"hellow"和"world"內容,輸出:
如上圖所示,print(pow(2,3))的輸出是 hello world(下面三行還沒用到,可以先無視)。
然后第一次,把CGetPow的返回改為5,它會輸出:
這時候輸出變成了function:xxxxx 2 3 hello world,第一個function:xxxxxx可以由下面的函數對照發現是pow函數,也就是CGetPow,從這時候看好像看起來還是棧結構,雖然它返回來我們不需要的參數2和3和正在CGetPow函數.
第二次,把CGetPow的返回改為8,輸出為:
由函數對照發現,輸出在重復:fuck,加載test.lua生成的函數,print函數。這時候它的輸出又不像一個棧結構了,因為2,3,hello,world不見了。為什么會這樣呢?lua是怎么push值到棧中的,怎么從棧中取值的,函數調用到底干了什么?我決定通過分析lua的c源碼來弄明白這些問題。
二、源碼分析
1、lua_State和棧的初始化
lua_State由lua_newstate創建:
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { int i; lua_State *L; global_State *g; //LG由傳進來的f進行分配,其中就已經包含了lua_State LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG))); if (l == NULL) return NULL; //從LG中取出要返回的lua_State L = &l->l.l; g = &l->g; L->next = NULL; L->tt = LUA_TTHREAD; g->currentwhite = bitmask(WHITE0BIT); L->marked = luaC_white(g); preinit_thread(L, g); g->frealloc = f; g->ud = ud; g->mainthread = L; g->seed = makeseed(L); g->gcrunning = 0; /* no GC while building state */ g->GCestimate = 0; g->strt.size = g->strt.nuse = 0; g->strt.hash = NULL; setnilvalue(&g->l_registry); g->panic = NULL; g->version = NULL; g->gcstate = GCSpause; g->gckind = KGC_NORMAL; g->allgc = g->finobj = g->tobefnz = g->fixedgc = NULL; g->sweepgc = NULL; g->gray = g->grayagain = NULL; g->weak = g->ephemeron = g->allweak = NULL; g->twups = NULL; g->totalbytes = sizeof(LG); g->GCdebt = 0; g->gcfinnum = 0; g->gcpause = LUAI_GCPAUSE; g->gcstepmul = LUAI_GCMUL; for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL; if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) { /* memory allocation error: free partial state */ close_state(L); L = NULL; } return L; }
展開lua_rawrunprotected
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) { ...... LUAI_TRY(L, &lj, (*f)(L, ud); ); ...... }
發現調用了傳進來的f_luaopen函數,追蹤這個函數
static void f_luaopen (lua_State *L, void *ud) { ...... stack_init(L, L); /* init stack */ ...... }
由作者注釋可知stack_init是初始化棧的函數,找到這個函數的實現
static void stack_init (lua_State *L1, lua_State *L) { int i; CallInfo *ci; /* initialize stack array */ L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, TValue); L1->stacksize = BASIC_STACK_SIZE; for (i = 0; i < BASIC_STACK_SIZE; i++) setnilvalue(L1->stack + i); /* erase new stack */ L1->top = L1->stack; L1->stack_last = L1->stack + L1->stacksize - EXTRA_STACK; /* initialize first ci */ ci = &L1->base_ci; ci->next = ci->previous = NULL; ci->callstatus = 0; ci->func = L1->top; setnilvalue(L1->top++); /* 'function' entry for this 'ci' */ ci->top = L1->top + LUA_MINSTACK; L1->ci = ci; }
分析得知:lua_State由luaM_newvector初始化並返回一個基地址給L1->stack,棧的大小為40(由BASIC_STACK_SIZE的宏定義得到),然后初始化把棧上的值全設置為nil,設置棧最后一個元素的地址L1->stack_last,初始化當前調用信息L1->ci,把lua_State的top設置為第一個棧上的第二個空元素(第一個是空元素已經被用作ci了,所以不能使用),設置ci的top(其實相當於ci作為棧基,然后ci這個棧的長度為LUA_MINSTACK,也就是20,在這個棧中push時不能超過這個長度,除非重新設置棧長度)。
2、lua從棧中取數據和往棧中push數據
首先是取數據,這里用lua_tointegerx作例子
LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum) { lua_Integer res; //重點是這句,TValue是lua中用的最多的數據結構,index2addr則是根據傳入的棧索引從棧取出數據 const TValue *o = index2addr(L, idx); int isnum = tointeger(o, &res); if (!isnum) res = 0; /* call to 'tointeger' may change 'n' even if it fails */ if (pisnum) *pisnum = isnum; return res; }
查看TValue的數據結構
//Value是具體的值,tt_則是定義好的數據類型,數據類型在lua中也是使用宏定義設置的 #define TValuefields Value value_; int tt_ typedef struct lua_TValue { TValuefields; } TValue;
展開index2addr的實現:
static TValue *index2addr (lua_State *L, int idx) { //當前調用函數的信息 CallInfo *ci = L->ci; if (idx > 0) { //在棧基(這里的棧基不是指整個lua_State的棧基,而是當前調用函數信息的棧基,也可以簡單理解成就是當前函數在棧中的地址)基礎上加上傳進來的索引獲取到正確的數據 TValue *o = ci->func + idx; //下面是檢測數據正確性 api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index"); if (o >= L->top) return NONVALIDVALUE; //返回獲取到的數據 else return o; } ...... }
通過index2addr從棧中獲取到數據,然后在根據tt_獲取到數據類型,得到對應的值。lua_tolstring,lua_toboolean等基本都是這樣從棧中取數據。
然后是向棧中push數據。這里以lua_pushinteger做例子:
#define api_incr_top(L) {L->top++; api_check(L, L->top <= L->ci->top, \ "stack overflow");} #define setivalue(obj,x) \ { TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_TNUMINT); } LUA_API void lua_pushinteger (lua_State *L, lua_Integer n) { lua_lock(L); setivalue(L->top, n); api_incr_top(L); lua_unlock(L); }
這里所做的操作顯示把L->top指向的空元素設置為想要設置的元素,然后再把L->top只向下一個空元素,期間同樣會涉及到向棧推元素需要做的安全性檢查。push其他類型元素的操作也和這個操作原理一樣。
3、函數的調用
通常我們在C中調用函數,會先push要調用的函數,然后再按順序push參數,最后使用lua_pcall指定lua_state,參數個數以及返回個數。先來分析源碼
//lua_pcall是個宏定義,展開后是lua_pcallk() #define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL) //看看lua_pcallk的實現,代碼太多,只展示要說明的 LUA_API int lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k) { struct CallS c; int status; ptrdiff_t func; lua_lock(L); //檢查正確性 api_check(L, k == NULL || !isLua(L->ci), "cannot use continuations inside hooks"); api_checknelems(L, nargs+1); api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread"); checkresults(L, nargs, nresults); //errfunc為0代表沒指定錯誤處理函數 if (errfunc == 0) func = 0; else { ...... } //c.func通過棧的運算獲得,是要調用的函數在棧上的位置 c.func = L->top - (nargs+1); /* function to be called */ //k為空代表沒有延續 if (k == NULL || L->nny > 0) { /* no continuation or no yieldable? */ //設置返回結果個數 c.nresults = nresults; /* do a 'conventional' protected call */ //luaD_pcall才是重點,在這里傳進去了f_call函數 status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); } else { /* prepare continuation (call is already protected by 'resume') */ ...... } //調整棧頂位置 adjustresults(L, nresults); lua_unlock(L); return status; } //展開luaD_pcall int luaD_pcall (lua_State *L, Pfunc func, void *u, ptrdiff_t old_top, ptrdiff_t ef) { ...... //在這里調用了上一步傳進來的f_call函數 status = luaD_rawrunprotected(L, func, u); ...... } //繼續展開f_call函數 static void f_call (lua_State *L, void *ud) { struct CallS *c = cast(struct CallS *, ud); //f_call里有調用了luaD_callnoyield函數 luaD_callnoyield(L, c->func, c->nresults); } //展開luaD_callnoyield函數 void luaD_callnoyield (lua_State *L, StkId func, int nResults) { L->nny++; //好吧,繼續看看這個 luaD_call(L, func, nResults); L->nny--; } void luaD_call (lua_State *L, StkId func, int nResults) { if (++L->nCcalls >= LUAI_MAXCCALLS) stackerror(L); //重點函數:luaD_precall,展開之 if (!luaD_precall(L, func, nResults)) /* is a Lua function? */ luaV_execute(L); /* call it */ L->nCcalls--; } int luaD_precall (lua_State *L, StkId func, int nresults) { lua_CFunction f; CallInfo *ci; switch (ttype(func)) { case LUA_TCCL: /* C closure */ f = clCvalue(func)->f; goto Cfunc; case LUA_TLCF: /* light C function */ f = fvalue(func); Cfunc: { //n是c函數中返回的值 int n; /* number of returns */ //棧檢查,這個沒細看,都是些檢查安全性的東西 checkstackp(L, LUA_MINSTACK, func); /* ensure minimum stack size */ //創建並初始化新的調用函數信息,並進入新的函數 ci = next_ci(L); /* now 'enter' new function */ ci->nresults = nresults; ci->func = func; ci->top = L->top + LUA_MINSTACK; lua_assert(ci->top <= L->stack_last); ci->callstatus = 0; if (L->hookmask & LUA_MASKCALL) luaD_hook(L, LUA_HOOKCALL, -1); lua_unlock(L); //終於來了!!媽的調用來調用去的你終於執行到了真正要執行的函數了啊!C函數的返回值賦給了n。 n = (*f)(L); /* do the actual call */ lua_lock(L); api_checknelems(L, n); //這個函數也很重要,就是把執行函數后push進來的值重新放置到正確的位置,L->top-n是指第一個元素,n是指有多少個元素 luaD_poscall(L, ci, L->top - n, n); return 1; } //這里只說明c函數部分,其他lua函數等部分的原理都是差不多的。 ...... } } //展開luaD_poscall int luaD_poscall (lua_State *L, CallInfo *ci, StkId firstResult, int nres) { StkId res; int wanted = ci->nresults; ...... //res指向當前調用函數在棧中的位置 res = ci->func; /* res == final position of 1st result */ //通過調用函數信息鏈回到上一個調用函數信息中 L->ci = ci->previous; /* back to caller */ /* move results to proper place */ //moveresults真正執行移動元素的操作 return moveresults(L, firstResult, res, nres, wanted); } //展開moveresults static int moveresults (lua_State *L, const TValue *firstResult, StkId res, int nres, int wanted) { switch (wanted) { /* handle typical cases separately */ //不需要返回則不需要轉移 case 0: break; /* nothing to move */ //需要一個元素,只轉移第一個元素 case 1: { /* one result needed */ if (nres == 0) /* no results? */ firstResult = luaO_nilobject; /* adjust with nil */ setobjs2s(L, res, firstResult); /* move it to proper place */ break; } //如同print函數這些需要所有返回值的則調用這個,轉移所有元素 case LUA_MULTRET: { int i; //遍歷nres個元素並轉移到新位置,可以發現轉移元素實際上使用到了setobjs2s() for (i = 0; i < nres; i++) /* move all results to correct place */ setobjs2s(L, res + i, firstResult + i); //重新設置棧頂 L->top = res + nres; return 0; /* wanted == LUA_MULTRET */ } default: { int i; if (wanted <= nres) { /* enough results? */ for (i = 0; i < wanted; i++) /* move wanted results to correct place */ setobjs2s(L, res + i, firstResult + i); } else { /* not enough results; use all of them plus nils */ for (i = 0; i < nres; i++) /* move all results to correct place */ setobjs2s(L, res + i, firstResult + i); for (; i < wanted; i++) /* complete wanted number of results */ setnilvalue(res + i); } break; } } //重設置棧頂 L->top = res + wanted; /* top points after the last result */ return 1; } //展開setobjs2s //發現原來又是宏定義 #define setobjs2s setob //還是宏定義,原來就是直接把obj2的值賦給了obj1而已 #define setobj(L,obj1,obj2) \ { TValue *io1=(obj1); *io1 = *(obj2); \ (void)L; checkliveness(L,io1); } OK~到這里,就基本上明白了整個函數調用流程以及在流程中的棧變化了。
三、得出結論
為什么當CGetPow的返回值為5的時候它看起來還像是個棧結構,但是返回值為8的時候卻會重復fuck 編譯test.lua得出的函數 print這三個呢?
解:
因為在調用print(pow(2,3))中,棧中的結構為1:nil(已通過實驗確定這是最初的lua_state的ci->func),2:fuck, 3:function(加載test.lua得到的函數),4:print函數,5:pow函數,6:參數2,7:參數3.
當push了"hello"和"world“后,棧結構多了8:”hello“,9:”world“。
然后如果這時候返回值為5,移動棧元素時會從L->top-5(當前的top在索引10上)的元素開始,移動5個元素到從當前函數(pow)在棧中的位置開始的位置上。也就是說把從第5個索引開始的5個元素轉移到從pow函數所在位置開始的新位置,所以實際上棧的元素是沒有變動過的。所以當調用第4個索引位置上的print輸出他們的時候,輸出結果為:pow 2 3 hello world。
然后當返回值為8時,移動棧元素時會從L->top-8,也就是索引2開始把8個元素轉移到pow函數所在位置開始的新位置后。這里就會出現一個現象,因為2->5,3->6,4->7,所以實際上現在第5索引上的元素為最開始第2個索引上的元素,所以5->8后,第8索引上的元素為fuck,同理6->9實際上是3->9,7->10實際上是4->10,這就導致了print輸出時一直重復fuck test.lua編譯出來的函數 print這3個元素。
至此問題解決,並且還對lua的認識增加了許多。就是看lua的宏定義看得我腦殼疼了一天。233333