看Lua有一段时间了,说实话进展挺慢的。归根到底是因为“动机不纯”,我确实不是抱着学Lua的心态去看资料的。本来看Lua就是听说Lua的实现比较简单,可以借Lua理解解释性语言的一些细节实现。当然Lua也确实不简单的,闭包什么的概念以前都没听说过。没用过Lua却去硬啃Lua语言实现,我也是蛮拼的!
我主要的参考资料是云风大神的《Lua源码赏析》和高手翻译的《LUA中文教程》。这里我想记录一下函数调用的过程。按照函数调用这条线串联一下各各知识点。首先要从字节码开始了。Lua虚拟机的指令集中有两条用于函数调用的字节码。
OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */ OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */
后一个适用于尾调用的情况,第一个字节码适用于一般情况。我们知道Lua代码的执行其实就是一个大大的for循环里面套一个大大的switch语句。当虚拟机读到了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 */ if (luaD_precall(L, ra, nresults)) { /* C function? */ if (nresults >= 0) L->top = ci->top; /* adjust results */ base = ci->u.l.base; } else { /* Lua function */ ci = L->ci; ci->callstatus |= CIST_REENTRY; goto newframe; /* restart luaV_execute over new Lua function */ } )
Lua既可以调用Lua函数,也可以调用C函数。我先看看调用C函数吧!重点是luaD_precall(L, ra, nresults))。
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); switch (ttype(func)) { case LUA_TLCF: /* light C function */ f = fvalue(func); goto Cfunc; case LUA_TCCL: { /* C closure */ f = clCvalue(func)->f; Cfunc: luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */ ci = next_ci(L); /* now 'enter' new function */ ci->nresults = nresults; ci->func = restorestack(L, funcr); ci->top = L->top + LUA_MINSTACK; lua_assert(ci->top <= L->stack_last); ci->callstatus = 0; luaC_checkGC(L); /* stack grow uses memory */ if (L->hookmask & LUA_MASKCALL) luaD_hook(L, LUA_HOOKCALL, -1); lua_unlock(L); n = (*f)(L); /* do the actual call */ lua_lock(L); api_checknelems(L, n); luaD_poscall(L, L->top - n); return 1; } ......... } }
ptrdiff_t funcr = savestack(L, func); ci->func = restorestack(L, funcr); 这两句执行完之后,得到的结果就是ci->func = func。n = (*f)(L);通过这一句,实际的C函数就被调用了。 一个典型的lua调用的C函数如下:
static int l_sin (lua_State *L) { double d = lua_tonumber(L, 1); /* get argument */ lua_pushnumber(L, sin(d)); /* push result */ return 1; /* number of results */ }
这个函数的第一行的作用是获取参数,lua_tonumber(L, 1)是一个宏。它实际调用了lua_tonumberx()。而在这个函数中通过间接的方式调用TValue *o = ci->func + idx;
获得了第idx个参数变量。Call指令明确规定函数参数存放在R(A)后的B-1个栈内。而ci->func就是指向了R(A)。那么Lua是如何保证参数个数的正确的呢?玄机就在lua_tonumberx()所调用的index2addr()中。
static TValue *index2addr (lua_State *L, int idx) { CallInfo *ci = L->ci; if (idx > 0) { 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; /* ..... */ }
这个函数会比较o和L->top的位置,如果大于或等于,就会返回错误。而L->top是在OP_CALL的分支语句里面就改写成了ra+b,也就是最后一个参数的下一个位置。这样的机制就保证了C函数不会把参数个数弄错了。然后是参数返回的问题了,虚拟机指令规定返回值应该在R(A), ... ,R(A+C-2)。Lua是如何实现这种规定的呢?首先看函数lua_pushnumber(),其定义如下:
LUA_API void lua_pushnumber (lua_State *L, lua_Number n) { lua_lock(L); setnvalue(L->top, n); luai_checknum(L, L->top, luaG_runerror(L, "C API - attempt to push a signaling NaN")); api_incr_top(L); lua_unlock(L);
这个函数的工作就是把n写到L->top所指向的栈空间,然后把L->top向高位移动一个位置。很明显这个存储位置是不符合Lua虚拟机规定的。所以需要luaD_poscall(L, L->top - n)来完成这个任务。这个n是C函数的返回值个数,所以L->top其实是指向了C函数的第一个返回值。
int luaD_poscall (lua_State *L, StkId firstResult) { StkId res; int wanted, i; CallInfo *ci = L->ci; if (L->hookmask & (LUA_MASKRET | LUA_MASKLINE)) { if (L->hookmask & LUA_MASKRET) { ptrdiff_t fr = savestack(L, firstResult); /* hook may change stack */ luaD_hook(L, LUA_HOOKRET, -1); firstResult = restorestack(L, fr); } L->oldpc = ci->previous->u.l.savedpc; /* 'oldpc' for caller function */ } res = ci->func; /* res == final position of 1st result */ wanted = ci->nresults; L->ci = ci = ci->previous; /* back to caller */ /* move results to correct place */ for (i = wanted; i != 0 && firstResult < L->top; i--) setobjs2s(L, res++, firstResult++); while (i-- > 0) setnilvalue(res++); L->top = res; return (wanted - LUA_MULTRET); /* 0 iff wanted == LUA_MULTRET */ }
其实luaD_poscall也比较简单,它其实就是一个返回值的“搬运工”。res被赋值为ci->func,实际上就指向了前面的R(A)。然后一个for循环实现了拷贝的工作。对应C函数的ci结构也就功成身退了。到此,一个C函数的调用过程就完成了。
发表评论