目录
前面几章我们介绍了Lua常用的最重要的几个数据结构。这章节开始,我们开始讲解主流程篇。
主流程,一般都是从lua.c的main方法开始。那我们就从main方法开始看整个链路和流程。
一、函数调用栈 - 从main方法看函数调用栈
从main方法中,创建完基础的lua_State *L结构后,我们就能看到Lua向数据栈上push了一个c语言的闭包方法。该方法:pmain。
- pmain方法是整个Lua执行流最核心的方法。主要负责:命令行参数的解析、Lua语言默认库的加载、Lua脚本语言的解析和调用等。
- Lua的栈操作主要靠lua_push*系列函数,往栈上不同压入不同的数据。
- main函数中首先压入pmain方法,然后把命令行参数个数和命令行参数数据也压入到了Lua的栈上。然后通过lua_pcall方法,开始执行pmain函数。
- lua_pcall方法为何调用的是pmain函数,而不是其他函数。其实调用的函数就是我们Lua栈执行函数= L->top - (nargs+1); 在lua_pcall中就能详细看到。
- 当调用pmain函数的时候,pmain首先会去解析命令行参数,然后加载默认的Lua语言的API函数库,再然后加载lua脚本文件以及解析lua语言,最后如果pmain方法执行成功,需要将status状态push放入栈顶(lua_pushboolean)。
- main方法中,通过lua_toboolean从当前调用栈CallInfo上获取结果值。
看一下具体实现:
/**
* argc:行参数的个数
* argv:每个参数的值,指针结构
*/
int main (int argc, char **argv) {
int status, result;
/* 第一步:创建一个主线程栈数据结构 */
lua_State *L = luaL_newstate(); /* create state */
if (L == NULL) {
l_message(argv[0], "cannot create state: not enough memory");
return EXIT_FAILURE;
}
lua_pushcfunction(L, &pmain); /* 将pmain放入L结构上 L->top值&pmain*/
lua_pushinteger(L, argc); /* 将argc 放入L结构上 L->top值argc*/
lua_pushlightuserdata(L, argv); /* 将argv 放入L结构上 L->top值argv*/
status = lua_pcall(L, 2, 1, 0); /* 函数操作,执行pmain 函数 do the call */
result = lua_toboolean(L, -1); /* 获取pmain函数lua_pushboolean(L, 1) 的信号值get result */
report(L, status);
lua_close(L);
return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}
static int pmain (lua_State *L) {
.......省.......
/* 打开常规Lua的标准库 */
luaL_openlibs(L); /* open standard libraries */
.......省.......
else dofile(L, NULL); /* executes stdin as a file */
.......省.......
lua_pushboolean(L, 1); /* 向栈顶L->top PUSH 返回值signal no errors */
return 1;
}
具体pmain函数的调用,可以看下图:
二、函数调用栈 - 函数调用主流程详解
具体的整个函数调用栈操作流程如下(可以参考图片中的文件顺序进行源码阅读):
- 首先Lua mian函数中,通过lua_push*系列函数将调用的函数以及参数数据进行入栈操作。
- Lua会调用lua_pcall执行Lua函数的真正调用。lua_pcall函数通过参数的个数和结果个数进行数据校验,以及拿到函数栈的StkId地址。lua_pcallk和lua_callk函数都在lapi.c文件中,两个函数的区别是一个有异常保护,一个没有异常保护。无论是保护状态下,还是非保护状态下,最终都是调用luaD_call函数(Lua的函数调用执行操作都在ldo.c文件中)。
- luaD_call函数中,首先会调用luaD_precall预处理函数。luaD_precall预处理函数主要会创建一个调用栈CallInfo,管理函数调用时的信息。CallInfo是一个双向链表形式管理。每次有新的函数进来,都会用新的CallInfo进行函数调用信息管理。
- Lua的函数调用一共分三种:C语言闭包函数(例如pmain),Lua的API函数库(例如字符串strlen函数),Lua语言解析。
- 如果是c语言闭包函数和Lua的API函数:会直接执行C方法(方法入参都为L),并调用luaD_poscall,调整堆栈,返回上一个调用栈。(我们语言中,肯定是一个函数内会嵌套N个函数。每次执行完一个函数后,都会返回到上一层CallInfo(L->ci会变更为上一层CallInfo),然后继续执行该函数的其它事务。)
- 如果是Lua语言,则会调用luaV_execute执行语言字节码调用。在luaV_execute函数中,当遇到函数调用尾部OP_TAILCALL的时候,则会调整CallInfo堆栈,返回上一个调用栈
- 如果你有返回值,在被执行的函数中通过lua_push*系列函数,将函数结果返回到栈顶部即可。外部可以通过lua_to*系列函数,获取当前调用栈的栈顶数据信息。
- 最后,在luaD_poscall函数中,会调用moveresults方法,该方法主要用于调整返回结果,将结果集调整到CallInfo函数的起始位置ci->func,并调整L->top。说白了,一个CallInfo调整完毕之后,只需要将得到的结果集返回到堆栈上,并调整堆栈,保障整体的堆栈回调大小控制在一定范围内。
三、调用栈操作 - 保护方式lua_pcallk函数调用
在lapi.c文件中,lua_pcallk方法是受保护的调用方式,lua_callk为非受保护的调用方式。受保护方法的唯一区别:函数的调用都不会因为错误直接导致程序直接退出,而是退回到调用点,然后将状态返回到外层的逻辑处理。
我们可以看一下ldo.c文件中的luaD_pcall方法。该方法最终调用luaD_rawrunprotected,而luaD_rawrunprotected实际最终调用f_call函数。保护性调用的情况下lua虚拟机使用lua_longjmp为函数实现堆栈续传功能,也就是当错误发生的时候,在Lua内部能够最终跳转到调用点继续向下执行。详见:《Lua源码分析 - 主流程篇 - 异常处理机制实现(09)》
/**
* 函数调用主方法(异常保护方式)
* func:f_call方法
* u:CallS 调用的方法等信息
* old_top:函数调用前的栈顶 L->top
* ef:错误状态
*/
int luaD_pcall (lua_State *L, Pfunc func, void *u,
ptrdiff_t old_top, ptrdiff_t ef) {
int status;
CallInfo *old_ci = L->ci; //老的函数回调CallInfo
lu_byte old_allowhooks = L->allowhook; //是否允许钩子
unsigned short old_nny = L->nny;
ptrdiff_t old_errfunc = L->errfunc;
L->errfunc = ef;
status = luaD_rawrunprotected(L, func, u); //异常保护调用主函数
/* 处理失败,栈状态回滚? */
if (status != LUA_OK) { /* an error occurred? */
StkId oldtop = restorestack(L, old_top);
luaF_close(L, oldtop); /* close possible pending closures */
seterrorobj(L, status, oldtop);
L->ci = old_ci;
L->allowhook = old_allowhooks;
L->nny = old_nny;
luaD_shrinkstack(L);
}
L->errfunc = old_errfunc;
return status;
}
/**
* 保护性调用(最终回调luaD_callnoyield方法)
* f=luaD_callnoyield方法
* ud=CallS *c ( c->func, c->nresults)
*/
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
unsigned short oldnCcalls = L->nCcalls;
struct lua_longjmp lj;
lj.status = LUA_OK;
lj.previous = L->errorJmp; /* chain new error handler */
L->errorJmp = &lj;
LUAI_TRY(L, &lj,
(*f)(L, ud);
);
L->errorJmp = lj.previous; /* restore old error handler */
L->nCcalls = oldnCcalls;
return lj.status;
}
/**
* 该方法包了很多层,最终调用luaD_call方法
*/
static void f_call (lua_State *L, void *ud) {
struct CallS *c = cast(struct CallS *, ud);
luaD_callnoyield(L, c->func, c->nresults);
}
/*
** Similar to 'luaD_call', but does not allow yields during the call
** 不允许挂起
*/
void luaD_callnoyield (lua_State *L, StkId func, int nResults) {
L->nny++;
luaD_call(L, func, nResults);
L->nny--;
}
四、调用栈操作 - 主函数luaD_call逻辑详解
无论是保护模式还是非保护模式下,最终会调用luaD_call方法。
该方法会先调用luaD_precall函数,对Lua函数执行作预处理。预处理主要作用是调整:调用栈CallInfo的堆栈
预处理函数主要逻辑:
- 检查栈信息,创建一个新的CallInfo 容器,填充相关的信息
- 如果是C语言闭包函数 or Lua的C语言 API函数库,则直接执行函数(PS:这些函数入参都统一为L)
- 如果是Lua语言,则进行CallInfo预处理之后,直接调用luaV_execute函数,执行虚拟机的指令集。
- 不论是C语言函数还是Lua语言函数,函数执行完毕后都会进行堆栈调整,将正在执行的操作站CallInfo指针会往上一层调用栈调整。C语言函数会调用luaD_poscall调整堆栈。而Lua语言则在虚拟机指令集运行中,找到函数调用尾部OP_TAILCALL的时候,会调整整体堆栈。
/*
** Call a function (C or Lua). The function to be called is at *func.
** The arguments are on the stack, right after the function.
** When returns, all the results are on the stack, starting at the original
** function position.
** 真正执行一个C语言方法 or 一个Lua方法
*/
void luaD_call (lua_State *L, StkId func, int nResults) {
if (++L->nCcalls >= LUAI_MAXCCALLS)
stackerror(L);
if (!luaD_precall(L, func, nResults)) /* is a Lua function? */
luaV_execute(L); /* call it Lua方法,则执行字节码方式 */
L->nCcalls--;
}
/*
** Prepares a function call: checks the stack, creates a new CallInfo
** entry, fills in the relevant information, calls hook if needed.
** If function is a C function, does the call, too. (Otherwise, leave
** the execution ('luaV_execute') to the caller, to allow stackless
** calls.) Returns true iff function has been executed (C function).
** 预处理一个方法call
** 1. 检查栈信息
** 2. 创建一个新的CallInfo 容器
** 3. 填充相关的信息
** 4. 如果需要,回调钩子函数
*/
int luaD_precall (lua_State *L, StkId func, int nresults) {
lua_CFunction f;
CallInfo *ci;
switch (ttype(func)) {
case LUA_TCCL: /* C语言闭包 C closure */
f = clCvalue(func)->f;
goto Cfunc;
case LUA_TLCF: /* C语言函数 light C function */
f = fvalue(func);
Cfunc: {
int n; /* number of returns */
checkstackp(L, LUA_MINSTACK, func); /* ensure minimum stack size */
ci = next_ci(L); /* 创建一个新的CallInfo栈对象 now 'enter' new function */
ci->nresults = nresults; //返回的结果个数
ci->func = func; //指向需要调用的函数栈
ci->top = L->top + LUA_MINSTACK; //C语言方法最小的调用栈允许20
lua_assert(ci->top <= L->stack_last);
ci->callstatus = 0;
if (L->hookmask & LUA_MASKCALL)
luaD_hook(L, LUA_HOOKCALL, -1);
lua_unlock(L);
n = (*f)(L); /* 直接调用C语言闭包函数 do the actual call */
lua_lock(L);
api_checknelems(L, n);
luaD_poscall(L, ci, L->top - n, n); //调整堆栈
return 1; //返回1 C语言本身函数
}
case LUA_TLCL: { /* Lua方法 Lua function: prepare its call */
StkId base;
Proto *p = clLvalue(func)->p;
int n = cast_int(L->top - func) - 1; /* number of real arguments */
int fsize = p->maxstacksize; /* frame size */
checkstackp(L, fsize, func);
if (p->is_vararg)
base = adjust_varargs(L, p, n);
else { /* non vararg function */
for (; n < p->numparams; n++)
setnilvalue(L->top++); /* complete missing arguments */
base = func + 1;
}
ci = next_ci(L); /* now 'enter' new function */
ci->nresults = nresults;
ci->func = func;
ci->u.l.base = base;
L->top = ci->top = base + fsize;
lua_assert(ci->top <= L->stack_last);
ci->u.l.savedpc = p->code; /* starting point */
ci->callstatus = CIST_LUA;
if (L->hookmask & LUA_MASKCALL)
callhook(L, ci);
return 0; //返回0,Lua函数
}
default: { /* not a function */
checkstackp(L, 1, func); /* ensure space for metamethod */
tryfuncTM(L, func); /* try to get '__call' metamethod */
return luaD_precall(L, func, nresults); /* now it must be a function */
}
}
}
五、调用栈操作 - 结果集moveresults调整
函数调用结束后,首先CI会回滚到上一层的调用,并调用moveresults函数,调整结果集数据。
- moveresults会将结果集(0个/1个/多个),逐个根据顺序拷贝到ci->func位置,并调整L->top的指针位置。如下图。
- 一个函数调用完毕之后,只需要将得到的结果集返回给上一层,并调整堆栈top指针,保障整体的堆栈回调大小控制在一定范围内。
- 返回值是我们回调函数里面,通过lua_push*方法向栈顶设置返回值。如果回调函数中没有设定返回值,则首个返回的结果为最后一个参数。
/*
** Given 'nres' results at 'firstResult', move 'wanted' of them to 'res'.
** Handle most typical cases (zero results for commands, one result for
** expressions, multiple results for tail calls/single parameters)
** separated.
*/
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;
}
case LUA_MULTRET: {
int i;
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;
}