看Lua有一段时间了,说实话进展挺慢的。归根到底是因为“动机不纯”,我确实不是抱着学Lua的心态去看资料的。本来看Lua就是听说Lua的实现比较简单,可以借Lua理解解释性语言的一些细节实现。当然Lua也确实不简单的,闭包什么的概念以前都没听说过。没用过Lua却去硬啃Lua语言实现,我也是蛮拼的!
我主要的参考资料是云风大神的《Lua源码赏析》和高手翻译的《LUA中文教程》。这里我想记录一下函数调用的过程。按照函数调用这条线串联一下各各知识点。首先要从字节码开始了。Lua虚拟机的指令集中有两条用于函数调用的字节码。
获得了第idx个参数变量。Call指令明确规定函数参数存放在R(A)后的B-1个栈内。而ci->func就是指向了R(A)。那么Lua是如何保证参数个数的正确的呢?玄机就在lua_tonumberx()所调用的index2addr()中。
我主要的参考资料是云风大神的《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函数的调用过程就完成了。