Lua 5.3 源码分析(十)线程的执行与中断

Lua 5.3 源码分析(十)线程的执行与中断

Lua 5.3 源码分析(十)线程的执行与中断

Lua 的程序运行时以线程为单位的。每个Lua 线程可以独立运行直到自行中断,把中断的信息留在状态机中。每条线程的执行互不干扰,可以独立延续之前中断的执行过程。

Lua 线程和系统线程无关,所以不会为每条 Lua 线程创建独立的系统堆栈,而是利用自己维护的线程栈,内存开销也就远小于系统线程。

Lua 是一门嵌入式语言,和 C 语言混合编程是一种常态。 一旦Lua 调用的 C 库中企图中断线程,延续它就是一个巨大的难题。

异常处理

如果Lua 被实现为一个纯粹的运行在字节码 VM 上的语言,只要不出 VM,可以很容易的实现自己的线程和异常处理。

事实上,Lua 的函数调用层次上只要没有 C 函数,是不会在 C 层面的调用栈上深入下去的。

但当Lua 函数调用了 C 函数,而这个 C 函数又进一步回调了 Lua 函数,这个问题就复杂很多。

Lua 的标准库中的 pairs 函数,就是一个典型的 C 扩展函数,却又回调了 Lua 函数。

Lua 底层把异常和线程中断用同一种机制来处理,也就是使用了 C 语言标准的 longjmp 机制来解决这个问题。
#if !defined(LUAI_THROW) /* { */

#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP)   /* { *//* C++ exceptions */#define LUAI_THROW(L,c)     throw(c)#define LUAI_TRY(L,c,a) try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; }#define luai_jmpbuf     int  /* dummy variable */#elif defined(LUA_USE_POSIX)                /* }{ *//* in POSIX, try _longjmp/_setjmp (more efficient) */#define LUAI_THROW(L,c)     _longjmp((c)->b, 1)#define LUAI_TRY(L,c,a)     if (_setjmp((c)->b) == 0) { a }#define luai_jmpbuf     jmp_buf#else                           /* }{ *//* ISO C handling with long jumps */#define LUAI_THROW(L,c)     longjmp((c)->b, 1)#define LUAI_TRY(L,c,a)     if (setjmp((c)->b) == 0) { a }#define luai_jmpbuf     jmp_buf#endif                          /* } */#endif                          /* } */

每条线程 L 中保存了当前的 longjmp 返回点: errorJmp ,其结构定义 为 struct lua_longjmp 。这是一条链表,每次运行一段受保护的 Lua 代码,都会生成一个新的错误返回点,链到这条链表上。

/* chain list of long jump buffers */struct lua_longjmp {    struct lua_longjmp *previous;    luai_jmpbuf b;    volatile int status;  /* error code */};

设置 longjmp 返回点是由 luaD_rawrunprotected 完成的。

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;}

这段代码很容易理解:设置新的 jmpbuf,串到链表上,调用函数。调用完成后恢复进入时状态。
如果想回直接返回到最近的错误恢复点,只需要调用 longjmp。Lua 使用一个内部API luaD_throw 封装了这个过程。

l_noret luaD_throw (lua_State *L, int errcode) {    if (L->errorJmp) {  /* thread has an error handler? */        L->errorJmp->status = errcode;  /* set status */        LUAI_THROW(L, L->errorJmp);  /* jump to it */    }    else {  /* thread has no error handler */        global_State *g = G(L);        L->status = cast_byte(errcode);  /* mark it as dead */        if (g->mainthread->errorJmp) {  /* main thread has a handler? */            setobjs2s(L, g->mainthread->top++, L->top - 1);  /* copy error obj. */            luaD_throw(g->mainthread, errcode);  /* re-throw in main thread */        }        else {  /* no handler at all; abort */            if (g->panic) {  /* panic function? */                seterrorobj(L, errcode, L->top);  /* assume EXTRA_STACK */                if (L->ci->top < L->top)                    L->ci->top = L->top;  /* pushing msg. can break this invariant */                lua_unlock(L);                g->panic(L);  /* call panic function (last chance to jump out) */            }            abort();        }    }}

考虑到新构造的线程可能在不受保护的情况下运行。这时的任何错误都必须被捕获,不能让程序崩溃。这种情况合理的处理方式就是把正在运行的线程标记为死线程,并且在主线程中抛出异常。

函数调用

函数调用分为受保护调用和不受保护的调用。
受保护的函数调用可以看到一个 C 层面意义上完整的过程。在Lua 代码中,pcall 是用函数而不是语音机制完成的。受保护的函数调用一定在 C 层面进出一次调用栈。
它使用一个独立的内部API luaD_pcall 来实现。公开 API luaD_pcallk 仅仅是对它做了一些封装。

int luaD_pcall (lua_State *L, Pfunc func, void *u,                ptrdiff_t old_top, ptrdiff_t ef) {    int status;    CallInfo *old_ci = L->ci;    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;}

从这段代码我们可以看到 pcall 的处理模式:用 C 层面的堆栈来保护和恢复状态。
L->ci、L->allowhook、L->nny( nny 的全称是 number of
non-yieldable calls。由于 C 语言本身无法提供延续点的支持,所以 Lua 也无法让所有函数都是 yieldable 的。当一级函数处于 non-yieldable 状态时,更深的层次都无法 yieldable。这个变量用于监督这个状态,在错误发生报告。每级 C 调用是否允许 yield 取决于是否有设置 C 延续点,或是 Lua 内核实现时认为这次调用在发生 yield 时无法正确处理。这些都是由 luaD_call 的最后一个参数来制定。)、L->errfunc 都保存在 luaD_pcall 的 C 堆栈上,一旦 luaD_rawrunprotected 就可以正确恢复。

luaD_rawrunprotected 没有正确返回时,需要根据 old_top 找到堆栈上刚才调用的函数,给它做收尾工作(调用luaF_close 涉及 upvalue 的 gc 流程)。

因为 luaD_rawrunprotected 调用的是一个函数对象,而不是数据栈上的索引,这就需要额外的变量来定位了。

这里使用 restorestack 这个宏来定位栈上的地址,是因为数据栈的内存地址是会随着数据栈的大小而变化。保存地址是不可能的,而应该记住一个相对量。 savestack 和 restorestack这两个宏就是做这个工作的。
#define savestack(L,p) ((char )(p) - (char )L->stack)
#define restorestack(L,n) ((TValue )((char )L->stack + (n)))

一般的 Lua 层面的函数调用并不对应一个 C 层面上函数调用行为。对于Lua 函数而言,应该看成是生成新的 CallInfo,修正数据栈,然后把字节码的执行位置调整到被调用的函数开头。而Lua 函数的return 操作则做了 相反的操作,恢复数据栈,弹出 CallInfo ,修改字节码的执行位置,恢复到原有的执行序列上。

理解了这一点就能明白,在底层 API 中,为何分为 luaD_precall 和 luaD_poscall 。

luaD_precall 执行的是函数调用部分的工作,而 luaD_poscall 做的是函数返回的工作。

从 C 层面看,层层函数调用的过程并不是递归的。对于C 类型的函数调用,整个函数调用时完整的,不需要等待后续再调用 luaD_poscall,所以 luaD_precall 可以替代完成,并返回1; 而Lua 函数,执行完 luaD_precall 后,只是切换了 lua_State 的执行状态,被调用的函数的字节码尚未运行,luaD_precall 返回 0。待到Lua VM 执行到对应的 OP_RETURN 指令时,才会去调用 luaD_poscall 完成整次调用。

call

/* ** returns true if function has been executed (C function) */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;        }        case LUA_TLCL: {  /* Lua function: prepare its call */            StkId base;            Proto *p = clLvalue(func)->p;            n = cast_int(L->top - func) - 1;  /* number of real arguments */            luaD_checkstack(L, p->maxstacksize);            for (; n < p->numparams; n++)                setnilvalue(L->top++);  /* complete missing arguments */            if (!p->is_vararg) {                func = restorestack(L, funcr);                base = func + 1;            }            else {                base = adjust_varargs(L, p, n);                func = restorestack(L, funcr);  /* previous call can change stack */            }            ci = next_ci(L);  /* now 'enter' new function */            ci->nresults = nresults;            ci->func = func;            ci->u.l.base = base;            ci->top = base + p->maxstacksize;            lua_assert(ci->top <= L->stack_last);            ci->u.l.savedpc = p->code;  /* starting point */            ci->callstatus = CIST_LUA;            L->top = ci->top;            luaC_checkGC(L);  /* stack grow uses memory */            if (L->hookmask & LUA_MASKCALL)                callhook(L, ci);            return 0;        }        default: {  /* not a function */            luaD_checkstack(L, 1);  /* ensure space for metamethod */            func = restorestack(L, funcr);  /* previous call may change stack */            tryfuncTM(L, func);  /* try to get '__call' metamethod */            return luaD_precall(L, func, nresults);  /* now it must be a function */        }    }}

Light C function 和 C closure 仅仅是在存储上有所不同,处理逻辑是一致的:压入新的 CallInfo,把数据栈栈顶设置好。调用 C 函数,然后 luaD_poscall。

Lua 函数要复杂一些: 先通入函数对象在数据栈上的位置和栈顶差,计算出数据栈上的调用参数个数 n 。 如果 Lua 函数对输入参数个数有明确的最小要求,这点可以通过查询函数原型 numparams 字段获知; 如果 栈上提供的参数数量不足,就需要把不足的部分补为 nil。 当调用函数需要可变参数的时候,还需要进一步处理:

static StkId adjust_varargs (lua_State *L, Proto *p, int actual) {    int i;    int nfixargs = p->numparams;    StkId base, fixed;    lua_assert(actual >= nfixargs);    /* move fixed parameters to final position */    luaD_checkstack(L, p->maxstacksize);  /* check again for new 'base' */    fixed = L->top - actual;  /* first fixed argument */    base = L->top;  /* final position of first argument */    for (i=0; i<nfixargs; i++) {        setobjs2s(L, L->top++, fixed + i);        setnilvalue(fixed + i);    }    return base;}

变长参数表这个概念,只在Lua 函数中出现。当一个函数接收变长参数时,这部分的参数是放在 上一级数据栈帧尾部。 adjust_varargs 将需要固定参数复制到被调用的函数的新一级数据栈帧上,而变长参数留在原地。

接下来,要构造出新的一层调用栈 CallInfo。这个结构需要初始化字节码执行指针 savepc ,将其指向Lua 函数对象中的 字节指令区。

Lua 函数 整体所需要的栈空间是在生成字节码时就已知的,所以可以用 luaD_checkstack 一次性分配好。 CallInfo 中的栈顶可以直接调整到位。CallInfo 中的返回参数个数,所引用的函数对象一一初始化完毕,最后初始化线程运行状态 callstatus ,标记上 CIST_LUA 就够了。

真正如何运行 Lua 函数,是由调用 luaD_precall 者决定的。
有些对象是通过元表驱动函数调用的行为的,这时需要通过 tryfuncTM 函数取得真正的调用函数。

static void tryfuncTM (lua_State *L, StkId func) {    const TValue *tm = luaT_gettmbyobj(L, func, TM_CALL);    StkId p;    if (!ttisfunction(tm))        luaG_typeerror(L, func, "call");    /* Open a hole inside the stack at 'func' */    for (p = L->top; p > func; p--)        setobjs2s(L, p, p-1);    L->top++;  /* slot ensured by caller */    setobj2s(L, func, tm);  /* tag method is the new function to be called */}

根据Lua 的定义,通过元方法进行的函数调用和原生的函数调用有所区别。通过元方法进行的函数调用,会将对象自身作为第一个参数传入。这就需要移动数据栈,把对象插到第一个参数的位置。这个过程在源代码中有清晰的展示。

return

luaD_poscall 做的工作很简单,主要是数据栈的调整工作。

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_precall 设置在CalInfo 里的返回参数的个数 nresult,以及数据栈上在这次函数调用中实际新增的数据个数,需要对数据栈做一次调整。多余的抛弃,不足的补为 nil。

luaD_call

luaD_call 主要用来实现外部 API lua_callk。它调用完 luaD_precall 后,接着调用 luaV_execute 完成对函数本身的字节码执行。

luaD_call 的最后一个参数用来标示这次调用是否可以在其中挂起。因为在 Lua VM 执行期间,有许多情况都会引起新的一层 C 层面的函数调用。

Lua 线程并不拥有独立的 C 堆栈,所以对于发生在 C 函数内部的线程挂起操作,不是所有情况都正确处理的。是否接受 yield 操作,只有调用 luaD_call 才清楚。

void luaD_call (lua_State *L, StkId func, int nResults, int allowyield) {    if (++L->nCcalls >= LUAI_MAXCCALLS) {        if (L->nCcalls == LUAI_MAXCCALLS)            luaG_runerror(L, "C stack overflow");        else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3)))            luaD_throw(L, LUA_ERRERR);  /* error while handing stack error */    }    if (!allowyield)        L->nny++;    if (!luaD_precall(L, func, nResults))  /* is a Lua function? */        luaV_execute(L);  /* call it */    if (!allowyield)        L->nny--;    L->nCcalls--;}

如前面所述,Lua VM 在解析字节码执行的过程中,对 Lua 函数的调用并不直接使用 luaD_call。它不会产生 C 层面的函数调用行为,就可以尽量不引起 C 函数中挂起线程的问题。但在某些情况上的处理,也这么做的话会让 VM 的实现变得相当复杂。这些情况包括 for 语句引起的函数调用以及触发元方法引起的函数调用。Lua 利用 luaD_call ,可以简化实现。

从 C 函数中挂起线程

如何让 C 函数正确的配合工作?

C 语言是不支持延续点这个特性的。如果你从 C 函数中利用 longjmp 跳出,就再也回不到跳出点了。这对 Lua 工作在 VM 字节码上的大部分特性都不是问题。

但是,pcall 和元表 都涉及 C 函数调用,有这样的限制,让 Lua 不那么完整。Lua 应用一系统的技巧,绕开了这个限制,支持了 yieldable pcall and metamethods。

在Lua 文档中,可以找到这么一小节: Handing Yields in C 就是围绕解决这个难题展开的。首先我们来看问题的产生:
resume 的发起总是通过一次 lua_resume 的调用,在 Lua5.1 以前,yield 的调用必定结束于一次 lua_yield 调用,而调用它的 C 函数必须立刻返回。中间不能有任何 C 函数执行到中途的状态。这样,Lua VM 才能正常工作。
(C) lua_resume -> Lua functions -> coroutine.yield -> (C) lua yield -> (C) return
在这个流程中,无论Lua functions 有多少层,都被 lua_State 的调用栈管理。所以当最后 C return 返回到最初 resume 点,都不存在什么问题,可以让下一次 resume 正确继续。 也就是说,在 yield 时, Lua 调用栈上可以有 没有执行完的 Lua 函数,但不可以有 没有执行完的 C 函数。

如果我们写了一个 C 扩展, 在 C function 里回调了传入的一个 Lua 函数。情况就变得不一样了。
(C) lua_resume -> Lua function -> C function -> (C) lua_call -> Lua function -> coroutine.yield -> (C) lua_yield
C 通过 lua_call 调用的 Lua 函数中再调用 coroutine.yield 会导致 在 yield 之后, 再次 resume 时,不再可能从 lua_call 的下一行继续执行。 Lua 在遇到这种情况时,会抛出一个异常“attempt to yield across a C-call boundary”

在 C 和 Lua 的边界,如果在 yield 之后,resume 如何继续运行 C 边界之后的 C 代码?
所有 Lua 协程共享一个 C 堆栈。可以使用 longjmp 从调用深处跳出来,却无法回到那个位置。因为一旦跳出,堆栈就被破坏。
C 进入 Lua 的边界一共有四个 API:lua_call、lua_pcall、lua_resume、lua_yield 。其中要解决的关键问题在于调用一个 Lua 函数,却可能有两条返回路径。

Lua 函数的正常返回应该执行 lua_call 调用后面的 C 代码,而中途如果 yield 发生,会导致执行序回到前面 lua_resume 调用处的下一行 C 代码执行。
对于后一种,再次调用 lua_resume ,还需要回到 lua_call 之后完成后续的 C 执行逻辑。 C 语言是不允许这样做的,因为当初的 C 堆栈 已经不存在了。

Lua 5.2 改造了 API lua_callk 来解决这个问题。既然在yield 之后, C 的执行序无法回到 lua_callk 的下一行代码,那么就让 C 语言使用者自己提供一个 Continuation 函数 k 来继续。

我们可以这里理解 k 这个参数:当lua_callk 调用的 Lua 函数中没有发生yield 时,它会正常返回。一旦发生 yield,调用者要明白, C 代码无法正常延续,而 Lua VM 会在需要延续时调用 k 来完成后续工作。 k 会得到正确的 L 保存正确的 lua_State状态,看起来就好像用一个新的 C 执行序替代掉原来的 C 执行序一样。

一个容易理解的用法就是一个 C 函数调用的最后使用 lua_callk :

lua_callk(L, 0, LUA_MULTRET, 0, k);return k(L);static int luaB_dofile (lua_State *L) {    const char *fname = luaL_optstring(L, 1, NULL);    lua_settop(L, 1);    if (luaL_loadfile(L, fname) != LUA_OK)    return lua_error(L);    lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);    return dofilecont(L, 0, 0);}

也就是把 callk 后面的执行逻辑放在一个独立 C 函数 k中,分别在 callk 后调用它,或是 传递给框架,让框架在resume 后调用。这里 Lua 状态机的状态被正确保存在 L 中,而C 函数堆栈会被yield 后 被破坏掉。 如果我们需要在 k 中得到延续点前的 C 函数状态怎么办呢? Lua 提供了lua_KContext ctx 用于辅助记录 C 中的状态。

Lua 的线程结构 L 中保存有完整的 CallInfo 调用栈。当 C 层面的调用栈被破坏时,尚未返回的 C 函数会 在 切入 Lua VM 前在 CallInfo 中留下延续点函数。原本在 C 层面利用原生代码和系统提供的 C 堆栈维系的 C 函数调用线索,被平坦化为 L 里 CallInfo 中的一个个延续点函数。想延续一个 C 调用栈被破坏掉的 Lua 线程,只需要依次调用 CallInfo 中的延续点函数就能完成同样的执行逻辑。

挂起与延续

理解了Lua 5.2 对线程挂起和延续的处理方式,再来看相关代码要容易理解一些。
中断并挂起一个线程和线程的执行发生异常,这两种情况对 Lua VM 的执行来说是类似的。都是利用 luaD_throw 回到最近的保护点。

不同的是,线程的状态不同。主动挂起需要调用 API lua_yieldk ,把当前执行处的函数保存到CallInfo 的 extra 中,并设置线程状态为 LUA_YIELD, 然后抛出异常。

和异常抛出不同,yield 只可能被lua_yieldk 触发,这是一个 C API,而不是 Lua VM 的指令。也就是说, yield 必然来源于某次 C 函数调用,从 luaD_call 或 luaD_pcall 中退出的。这比异常的发生点药少的多。

先看下 lua_yieldk 的实现。

LUA_API int lua_yieldk (lua_State *L, int nresults, lua_KContext ctx,                        lua_KFunction k) {    CallInfo *ci = L->ci;    luai_userstateyield(L, nresults);    lua_lock(L);    api_checknelems(L, nresults);    if (L->nny > 0) {        if (L != G(L)->mainthread)            luaG_runerror(L, "attempt to yield across a C-call boundary");        else            luaG_runerror(L, "attempt to yield from outside a coroutine");    }    L->status = LUA_YIELD;    ci->extra = savestack(L, ci->func);  /* save current 'func' */    if (isLua(ci)) {  /* inside a hook? */        api_check(k == NULL, "hooks cannot continue after yielding");    }    else {        if ((ci->u.c.k = k) != NULL)  /* is there a continuation? */            ci->u.c.ctx = ctx;  /* save context */        ci->func = L->top - nresults - 1;  /* protect stack below results */        luaD_throw(L, LUA_YIELD);    }    lua_assert(ci->callstatus & CIST_HOOKED);  /* must be inside a hook */    lua_unlock(L);    return 0;  /* return to 'luaD_hook' */}

不是所有的C 函数都可以正常恢复,只要调用层次上面有一个这样的 C 函数, yield 就无法正确工作。这是由 nny 的值来检测的。

lua_yieldk 函数是一个公开的API ,只用于给 Lua 程序编写 C 扩展模块使用。所以处于整个函数内部时,一定处于一个 C 函数调用中。但 钩子函数的运行是个例外。 HOOK 函数本身就是一个 C 函数,但是并不是通常正常的 C API 调用进来的。 在Lua 函数中触发钩子会认为当前状态是处于Lua 函数执行中。这个时候允许yield 线程,但无法正确的处理 C 层面的延续点,所以禁止传入延续点函数。而对于正常的 C 调用,允许修改延续点 k 来改变执行流程。这里只需要简单的把 k 和 ctx 设入 L ,其它的活都交给 resume 去处理就可以了。

lua_resume 的过程要复杂的多,先列出代码,再分析。

LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs) {    int status;    int oldnny = L->nny;  /* save "number of non-yieldable" calls */    lua_lock(L);    luai_userstateresume(L, nargs);    L->nCcalls = (from) ? from->nCcalls + 1 : 1;    L->nny = 0;  /* allow yields */    api_checknelems(L, (L->status == LUA_OK) ? nargs + 1 : nargs);    status = luaD_rawrunprotected(L, resume, L->top - nargs);    if (status == -1)  /* error calling 'lua_resume'? */        status = LUA_ERRRUN;    else {  /* continue running after recoverable errors */        while (errorstatus(status) && recover(L, status)) {            /* unroll continuation */            status = luaD_rawrunprotected(L, unroll, &status);        }        if (errorstatus(status)) {  /* unrecoverable error? */            L->status = cast_byte(status);  /* mark thread as 'dead' */            seterrorobj(L, status, L->top);  /* push error message */            L->ci->top = L->top;        }        else lua_assert(status == L->status);  /* normal end or yield */    }    L->nny = oldnny;  /* restore 'nny' */    L->nCcalls--;    lua_assert(L->nCcalls == ((from) ? from->nCcalls : 0));    lua_unlock(L);    return status;}

lua_resume 开始运行时,等价于一次保护性调用。所以它是允许直接调用的 C 函数 yield 的。这里把 nny 设置为 0 开启。然后利用对 resume 函数的保护调用来进行前半段工作。

static void resume (lua_State *L, void *ud) {    int nCcalls = L->nCcalls;    StkId firstArg = cast(StkId, ud);    CallInfo *ci = L->ci;    if (nCcalls >= LUAI_MAXCCALLS)        resume_error(L, "C stack overflow", firstArg);    if (L->status == LUA_OK) {  /* may be starting a coroutine */        if (ci != &L->base_ci)  /* not in base level? */            resume_error(L, "cannot resume non-suspended coroutine", firstArg);        /* coroutine is in base level; start running it */        if (!luaD_precall(L, firstArg - 1, LUA_MULTRET))  /* Lua function? */            luaV_execute(L);  /* call it */    }    else if (L->status != LUA_YIELD)        resume_error(L, "cannot resume dead coroutine", firstArg);    else {  /* resuming from previous yield */        L->status = LUA_OK;  /* mark that it is running (again) */        ci->func = restorestack(L, ci->extra);        if (isLua(ci))  /* yielded inside a hook? */            luaV_execute(L);  /* just continue running Lua code */        else {  /* 'common' yield */            if (ci->u.c.k != NULL) {  /* does it have a continuation function? */                int n;                lua_unlock(L);                n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */                lua_lock(L);                api_checknelems(L, n);                firstArg = L->top - n;  /* yield results come from continuation */            }            luaD_poscall(L, firstArg);  /* finish 'luaD_precall' */        }        unroll(L, NULL);  /* run continuation */    }    lua_assert(nCcalls == L->nCcalls);}

如果 resume 是重新启动一个函数,那么只需要按和 luaD_call 相同的正常的调用流程进行。

若需要延续之前的调用,如上文所述,之前只可能从一次 C 调用中触发 lua_yieldk 挂起。但钩子函数是一个特殊情况,它是一个 C 函数,却看起来在 Lua 中。这时从 CallInfo 中的 extra 取出上次运行到的函数,可以识别出这个情况。

当它是一个 Lua 调用,那么必然是从钩子函数中切出的,不会有被打断的 Lua VM 指令,直接通过 lua_exwcute 函数继续它的字节码解析执行流程。若是 C 函数,按照延续点的约定,调用延续点 k,之后经过 luaD_poscall 完成这次调用。

上述事情做完之后,不一定完成了所有的工作。这是因为之前完整的调用层次,包含在 L 的 CallInfo中, 而不是存在于当前的 C 调用栈上。如果检查到 Lua 的调用栈上有未尽的工作,必须完成它。这项工作可通过 unroll 函数完成。

static void unroll (lua_State *L, void *ud) {    if (ud != NULL)  /* error status? */        finishCcall(L, *(int *)ud);  /* finish 'lua_pcallk' callee */    while (L->ci != &L->base_ci) {  /* something in the stack */        if (!isLua(L->ci))  /* C function? */            finishCcall(L, LUA_YIELD);  /* complete its execution */        else {  /* Lua function */            luaV_finishOp(L);  /* finish interrupted instruction */            luaV_execute(L);  /* execute down to higher C 'boundary' */        }    }}

unroll 发现 L 中的当前函数如果是一个 Lua 函数时,由于字节码的解析过程也可能因为触发元方法等情况调用 luaD_call 而从中间中断。故需要先调用 luaV_finishOp 函数,再交给 luaV_execute 函数开启 VM 来执行未完成的字节码。

当执行流中断于一次 C 函数调用,finishCcall 函数能完成当初执行了一半的 C 函数的 剩余工作。

static void finishCcall (lua_State *L, int status) {    CallInfo *ci = L->ci;    int n;    /* must have a continuation and must be able to call it */    lua_assert(ci->u.c.k != NULL && L->nny == 0);    /* error status can only happen in a protected call */    lua_assert((ci->callstatus & CIST_YPCALL) || status == LUA_YIELD);    if (ci->callstatus & CIST_YPCALL) {  /* was inside a pcall? */        ci->callstatus &= ~CIST_YPCALL;  /* finish 'lua_pcall' */        L->errfunc = ci->u.c.old_errfunc;    }    /* finish 'lua_callk'/'lua_pcall'; CIST_YPCALL and 'errfunc' already     handled */    adjustresults(L, ci->nresults);    /* call continuation function */    lua_unlock(L);    n = (*ci->u.c.k)(L, status, ci->u.c.ctx);    lua_lock(L);    api_checknelems(L, n);    /* finish 'luaD_precall' */    luaD_poscall(L, L->top - n);}

前面曾提到过,此时线程一定处于健康的状态。那么之前的工作肯定终止于 lua_callk 或 lua_pcallk。这时,应该先完成 lua_callk 没完成的工作(lua_callk 和 lua_pcallk 在调用完 luaD_call 后,后续的代码没有区别,都是 adjustresults(L, ci->nresults); 可以一致对待); 然后调用 C 函数中设置的延续点函数; 由于这时一次未完成的 C 函数调用,那么一定来源于一次被中断的 luaD_precall, 收尾的工作还剩下 luaD_poscall(L, L->top - n);

当 resume 这前半段工作完成,结果要么是一切顺利,状态码为 LUA_OK 结束或是 LUA_YIELD 主动挂起。那么就没有太多剩下的工作。L 的状态是完全正常的。可当代码中有错误发生时,问题就复杂一些。

从定义上说,lua_resume 函数需要具有捕获错误的能力。同样有这个能力的还有 lua_pcallk。如果在调用栈上,有 lua_pcallk 优先于它捕获错误,那么执行流应该交到 lua_pcallk 之后,也就是 lua_pcallk 设置的延续点函数()。

对 lua_resume 函数来说,错误被 lua_pcallk 捕获了,程序应该继续运行。它就有责任完成延续点的约定。这是用 recover 和 unroll 函数完成的。

static int recover (lua_State *L, int status) {    StkId oldtop;    CallInfo *ci = findpcall(L);    if (ci == NULL) return 0;  /* no recovery point */    /* "finish" luaD_pcall */    oldtop = restorestack(L, ci->extra);    luaF_close(L, oldtop);    seterrorobj(L, status, oldtop);    L->ci = ci;    L->allowhook = getoah(ci->callstatus);  /* restore original 'allowhook' */    L->nny = 0;  /* should be zero to be yieldable */    luaD_shrinkstack(L);    L->errfunc = ci->u.c.old_errfunc;    return 1;  /* continue running the coroutine */}

recover 函数用来把错误引导到调用栈上 最近的 lua_pcallk 的延续点上。
首先回溯CallInfo 栈,找到从 C 中调用 lua_pcallk 的位置。这次 lua_pcallk 一定从luaD_pcall 中被打断。

接下来就必须完成 luaD_pcall 本应该完成却没有机会去做的事情。所以 我们会看到,接下来的代码和 luaD_pcall 的后半部分非常相似。

最后需要把线程运行状态设上 CIST_STAT 标记让 unroll函数正确的设置线程状态。 然后只需要保护性调用unroll 函数来依据 Lua 调用栈执行逻辑上后续的流程。

回到 lua_resume 函数,其中的参数 from ,它是用来更准确的统计 C 调用栈的层级的。 nCcalls 的意义在于当发生无穷递归后,Lua VM 可以先于 C 层面的堆栈溢出导致的毁灭性错误之前,捕获到这种情况,完全的抛出异常。 由于现在可以 在 C 函数中切出,那么发起resume 的位置可能处于 逻辑上调用层次较深的位置。 这就需要调用者出入resume 的调用来源线程,正确的计算 NCcalls。

lua_callk 和 lua_pcallk

有了上面的基础,公开 API lua_callk 和 lua_pcallk 函数就能理解清楚了。

lua_callk 只是对 luaD_call 的简单封装。 在调用之前,根据需要把延续点 k 以及 ctx 设置到当前的 CallInfo 结构中。

LUA_API void lua_callk (lua_State *L, int nargs, int nresults,                        lua_KContext ctx, lua_KFunction k) {    StkId func;    lua_lock(L);    api_check(k == NULL || !isLua(L->ci),              "cannot use continuations inside hooks");    api_checknelems(L, nargs+1);    api_check(L->status == LUA_OK, "cannot do calls on non-normal thread");    checkresults(L, nargs, nresults);    func = L->top - (nargs+1);    if (k != NULL && L->nny == 0) {  /* need to prepare continuation? */        L->ci->u.c.k = k;  /* save continuation */        L->ci->u.c.ctx = ctx;  /* save context */        luaD_call(L, func, nresults, 1);  /* do the call */    }    else  /* no continuation or no yieldable */        luaD_call(L, func, nresults, 0);  /* just do the call */    adjustresults(L, nresults);    lua_unlock(L);}

lua_pcallk 函数 类似,只是对 luaD_pcall 的简单封装。
如果不需要延续点的支持或是处于不能被挂起的状态,那么 ,简单的调用 luaD_pcall 函数就可以。否则不能设置保护点,而改在调用前设置好延续点以及 ctx,并将线程状态标记为 CIST_YPCALL。这样在resume 过程中被 recover函数找到。

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(k == NULL || !isLua(L->ci),              "cannot use continuations inside hooks");    api_checknelems(L, nargs+1);    api_check(L->status == LUA_OK, "cannot do calls on non-normal thread");    checkresults(L, nargs, nresults);    if (errfunc == 0)        func = 0;    else {        StkId o = index2addr(L, errfunc);        api_checkstackindex(errfunc, o);        func = savestack(L, o);    }    c.func = L->top - (nargs+1);  /* function to be called */    if (k == NULL || L->nny > 0) {  /* no continuation or no yieldable? */        c.nresults = nresults;  /* do a 'conventional' protected call */        status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);    }    else {  /* prepare continuation (call is already protected by 'resume') */        CallInfo *ci = L->ci;        ci->u.c.k = k;  /* save continuation */        ci->u.c.ctx = ctx;  /* save context */        /* save information for error recovery */        ci->extra = savestack(L, c.func);        ci->u.c.old_errfunc = L->errfunc;        L->errfunc = func;        setoah(ci->callstatus, L->allowhook);  /* save value of 'allowhook' */        ci->callstatus |= CIST_YPCALL;  /* function can do error recovery */        luaD_call(L, c.func, nresults, 1);  /* do the call */        ci->callstatus &= ~CIST_YPCALL;        L->errfunc = ci->u.c.old_errfunc;        status = LUA_OK;  /* if it is here, there were no errors */    }    adjustresults(L, nresults);    lua_unlock(L);    return status;}

异常处理

Lua 的内部运行期异常,即错误码为 LUA_ERRRUN的那个,都是直接或间接的由 luaG_errormsg 函数抛出的。

按Lua 的约定,这类异常会在数据栈上留下错误信息,或是调用一个用户定义的错误处理函数。

l_noret luaG_errormsg (lua_State *L) {    if (L->errfunc != 0) {  /* is there an error handling function? */        StkId errfunc = restorestack(L, L->errfunc);        setobjs2s(L, L->top, L->top - 1);  /* move argument */        setobjs2s(L, L->top - 1, errfunc);  /* push function */        L->top++;  /* assume EXTRA_STACK */        luaD_call(L, L->top - 2, 1, 0);  /* call it */    }    luaD_throw(L, LUA_ERRRUN);}

它尝试从 L中读出 errfunc , 并使用 luaD_call 函数调用它。 如果在 errfunc 里再次出错,会继续调用自己。这样就有可能在错误处理函数中递归下去。但调用达到一定层次后, nCcalls 会超过上限最终产生一个 LUA_ERRERR 终止这个过程。

公开的API lua_error 函数 是对它的简单封装。

LUALIB_API int luaL_error (lua_State *L, const char *fmt, ...) {    va_list argp;    va_start(argp, fmt);    luaL_where(L, 1);    lua_pushvfstring(L, fmt, argp);    va_end(argp);    lua_concat(L, 2);    return lua_error(L);}

推荐阅读