Skip to content

4.5 - 在C代码中处理让出

在内部,Lua使用C库函数longjump来让出协程。因此,如果有一个C函数foo调用了一个API函数且这个API函数让出了(直接或间接地由其他函数发起的让出),那么Lua不会再从foo中返回了,因为longjmp在C栈上移除了其栈帧。

为了避免此类问题,每当尝试通过API调用来让出时都会抛出错误,除非是这三个函数:lua_yieldklua_callklua_pcallk。这些函数会接收一个延续函数 continuation function(作为参数名k)以在让出后继续执行。

我们需要说到一些术语以解释延续。我们把从Lua调用的C函数称为源函数 original function。这个源函数中调用的上述这三种C API中的函数,我们称为被调用方函数 callee function,它们会让出当前Lua线程。这种情况会在callee函数为lua_yieldk时发生,或者callee函数是lua_callklua_pcallk且它们发生了让出时。

假设运行的Lua线程执行callee函数时让出了,那么在Lua线程重入后,其最终源函数应当完成运行。然而callee函数并不能返回源函数,因为其C堆栈上的栈帧已经因为让出而被销毁了。作为代替,Lua会调用延续函数,其通过callee函数的参数来给出。顾名思义,延续函数应当继续完成源函数的工作。

作为示例,请考量以下函数:

C
int original_function (lua_State *L) {
  /*... code 1 */
  status = lua_pcall(L, n, m, h);  /* calls Lua */
  /*... code 2 */
}

现在我们想让Lua代码可以在lua_pcall中运行时让出。首先我们可以重写我们的函数,像这样:

C
int k (lua_State *L, int status, lua_KContext ctx) {
  /*... code 2 */
}

int original_function (lua_State *L) {
  /*... code 1 */
  return k(L, lua_pcall(L, n, m, h), ctx);
}

在上边的代码中,新函数k为延续函数(类型为lua_KFunction),它会完成源函数在调用lua_pcall后的所有工作。现在我们必须告知Lua代码被某些方式中断执行(错误或者让出)后应该调用k,所以我们需要重写代码,用lua_pcallk替换lua_pcall,像这样:

C
int original_function (lua_State *L) {
  /* ... code 1 */
  return k(L, lua_pcallk(L, n, m, h, ctx2, k), ctx1);
}

注意外层显式调用的延续函数k:Lua只会在需要的情况下调用延续函数,即因为错误或让出后的时候。如果被调用的函数并没有让出而是正常返回,lua_pcallk(以及lua_callk)也将会正常返回。(当然,这种情况下与其调用延续函数k,还不如把等效的工作直接放到源函数中完成。)

除了Lua状态机,延续函数还有两个其他参数:调用的最终状态码和上下文(ctx),其由原来的lua_pcallk传递而来。Lua并不使用这个上下文,它只是从源函数中传递给延续函数。对于lua_pcallk,状态应当和lua_pcallk本身的返回相同,除了执行让出后返回LUA_YIELD(而不是LUA_OK)。对于lua_yieldklua_callk,当Lua调用延续函数时它们的状态码永远是LUA_YIELD。(对于这两个函数,Lua将不会因为错误而调用延续函数,因为它们根本就不会处理错误。)同样,当使用lua_callk时,你应当将LUA_OK当作状态码来调用延续函数。(对于lua_yieldk,这里直接显式调用延续函数并没有太多意义,因为lua_yieldk通常不返回。)

Lua将延续函数和源函数作相同看待。延续函数同样接收来自源函数的Lua栈,如果callee函数已经返回了,那么还是在相同的Lua状态机内。(例如,在lua_callk之后,函数和其参数都从栈上移除了,取而代之的是调用结果。)其拥有同样的上值。无论如何,此返回都会被Lua作和源函数返回一样的处理。