从lua的c源码了解lua栈结构和函数调用流程
Posted winsons
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从lua的c源码了解lua栈结构和函数调用流程相关的知识,希望对你有一定的参考价值。
因为实习需要用到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
以上是关于从lua的c源码了解lua栈结构和函数调用流程的主要内容,如果未能解决你的问题,请参考以下文章