本文共 3162 字,大约阅读时间需要 10 分钟。
本文介绍lua虚拟机中是如何实现lua函数调用的,不涉及C函数的调用。下面通过栈帧结构以及lua调用栈组织形式来了解lua虚拟机中函数调用的过程。
对以下的lua函数调用代码:
function wen(a,b) print(a,b)enda = 1b = 2wen(a,b)
lua编译器会生成如下形式字节码:
CLOSURE 0 0 ; 0xb3c630 SETTABUP 0 -1 0 ; _ENV "wen" SETTABUP 0 -2 -3 ; _ENV "a" 1 SETTABUP 0 -4 -5 ; _ENV "b" 2 GETTABUP 0 0 -1 ; _ENV "wen" GETTABUP 1 0 -2 ; _ENV "a" GETTABUP 2 0 -4 ; _ENV "b" CALL 0 3 1 RETURN 0 1
这里忽略了wen函数对应的字节码。
可以看到,在执行CALL指令之前,lua会执行以下的三条指令做准备:
GETTABUP 0 0 -1 ; _ENV "wen" GETTABUP 1 0 -2 ; _ENV "a" GETTABUP 2 0 -4 ; _ENV "b"
这三条指令分别是将wen对应的闭包结构放入R(0)中,然后将全局变量a和b的值分别放入R(1),R(2)中。其中R(K)表示base+K,亦即当前栈帧中索引为K的内存位置。
下面我们看一下调用CALL指令后,lua是如何构建新函数的栈帧的。
CALL指令具有如下形式:
CALL A B C
可以用伪代码表示为:
R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))
也就是,R(A)存储函数的Closure结构,R(A)以上存储函数参数,参数个数为B-1。函数的返回结果放在R(A)以及R(A)往上的位置,返回个数为C-1。这里为了节约内存,返回参数覆盖了原来CLosure以及函数参数的位置。我们这里先不关心函数的返回。
下面看一下虚拟机中的实现。
CALL指令的实现位于OP_CALL分支:
vmcase(OP_CALL,
虚拟机首先根据指令内容获取函数的返回参数个数,以及根据函数参数调整当前栈结构:
int b = GETARG_B(i);int nresults = GETARG_C(i) - 1;if (b != 0) L->top = ra+b; /* else previous instruction set top */
这里L是一个lua_state变量,它维护一个lua执行流的所有状态,其中L->stack维护该执行流的栈基地址,L->top指向栈中第一个可用的位置。这里根据已经放入栈中的函数参数调整L->top指针,使他始终指向栈的可用位置,避免覆盖掉函数参数。
下面调用luaD_precall函数:
if (luaD_precall(L, ra, nresults)) { /* C function? */ if (nresults >= 0) L->top = ci->top; /* adjust results */ base = ci->u.l.base;}
该函数将会构建栈帧,同时判断所调用函数类型。这里我们只关注lua函数栈帧的构建过程。
下面进入该函数:
int luaD_precall (lua_State *L, StkId func, int nresults) { lua_CFunction f; CallInfo *ci; int n; /* number of arguments (Lua) or returns (C) */ ptrdiff_t funcr = savestack(L, func);
参数中,func指向栈中存放要调用的函数闭包的位置。
CallInfo是一个维护被调用闭包的相关状态的结构,包括被调用闭包的栈帧在L->stack中的位置区间,闭包的执行字节码等等。下面是lua函数栈帧构建的核心逻辑:
case LUA_TLCL: { /* Lua function: prepare its call */
首先是根据函数闭包获取函数的元信息:
Proto *p = clLvalue(func)->p;
Proto 中包含了在编译期得到的函数信息,包括,该函数最大需要使用的栈空间(maxstacksize),函数的形式参数个数等等(numparams)。lua函数支持实参数目小于形参数目的调用,其他没有赋值的形参将默认为nil:
for (; n < p->numparams; n++) setnilvalue(L->top++);
在定长参数情况下,lua将根据已经完整填入函数参数后的栈指针来设置栈帧位置:
if (!p->is_vararg) { func = restorestack(L, funcr); base = func + 1;}
base就是这个将被调用的函数的栈帧的基地址。当然,函数执行过程中除了参数需要用到栈空间外,局部变量也需要存储在栈中,因此L->top还需要进行进一步的调节。主要是根据proto的maxstacksize来调整,保证该函数只使用L->top以下,L->base以上的栈空间。
以下就是构建之后的栈帧结构示意图:
CallInfo维护了这个即将调用的函数的所有信息。
在C函数调用过程中,会在执行被调用的函数前将调用函数的执行地址放入栈中,这样被调用的函数返回时才可以继续原来的位置执行。
但是从上面分析lua函数调用过程中发现,lua栈中并没有任何涉及调用的主函数的信息,存储的都是参数,局部变量等信息。这样的话,在被调用函数结束时,如何回到原来的函数执行位置呢?
lua实现机制是将数据栈和调用栈进行分离。前面说过CallInfo维护了被调用函数的所有信息,因此借助这个结构就可以返回到原来的函数中。基于此,lua执行流状态机lua_state维护了一个CallInfo链表,其中L->ci始终指向当前执行的函数对应的CallInfo结构。
每次调用一个函数都会在执行链表中添加一个CallInfo项:
ci = next_ci(L);
这个宏会将新CallInfo放到链表末端,因此当下次执行到这个函数的RETURN返回语句时,只需要执行L->ci = L->ci->previous就可以返回到调用函数中。
以上所有预备工作完成之后,就可以执行这个新的CallInfo了:
else { /* Lua function */ ci = L->ci; ci->callstatus |= CIST_REENTRY;//调用的是lua函数 goto newframe; /* restart luaV_execute over new Lua function */ }
可以看到,只需要将ci指向L->ci即可(L->ci在luaD_precall中已经被更新过了)。
阿根廷队惊险晋级真是太让人开心了,煤球王终于还是有机会用自己的天才成为国家英雄获得无上荣耀。所以说,什么时候都不能放弃,即使只剩下十分钟,也要拼尽全力。愿你的明天充满光辉。