4.7 – C 中的让出处理

Lua 内部使用 C 的 longjmp 机制让出一个协程。因此,如果一个 C 函数 foo 调用了一个 API 函数,而这个 API 函数让出了(直接或间接调用了让出函数)。由于 longjmp 会移除 C 栈的栈帧,Lua 就无法返回到 foo 里了。

为了回避这类问题,碰到 API 调用中调用让出时,除了那些抛出错误的 API 外,还提供了三个函数:lua_yieldklua_callk,和 lua_pcallk 。它们在让出发生时,可以从传入的 延续函数(名为 k 的参数)继续运行。

我们需要预设一些术语来解释延续点。对于从 Lua 中调用的 C 函数,我们称之为 原函数。从这个原函数中调用的上面所述的三个 C API 函数我们称之为 被调函数。被调函数可以使当前线程让出。(让出发生在被调函数是 lua_yieldk,或传入 lua_callklua_pcallk 的函数调用了让出时。)

假设正在运行的线程在执行被调函数时让出。当再次延续这条线程,它希望继续被调函数的运行。然而,被调函数不可能返回到原函数中。这是因为之前的让出操作破坏了 C 栈的栈帧。作为替代品,Lua 调用那个作为被调函数参数给出的 延续函数 。正如其名,延续函数将延续原函数的任务。

下面的函数会做一个说明:

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

现在我们想允许被lua_pcall运行的 Lua 代码让出。首先,我们把函数改写成这个样子:

  1. int k (lua_State *L, int status, lua_KContext ctx) {
  2. ... /* code 2 */
  3. }
  4.  
  5. int original_function (lua_State *L) {
  6. ... /* code 1 */
  7. return k(L, lua_pcall(L, n, m, h), ctx);
  8. }

上面的代码中,新函数 k就是一个 延续函数(函数类型为 lua_KFunction)。它的工作就是原函数中调用 lua_pcall之后做的那些事情。现在我们必须通知 Lua 说,你必须在被lua_pcall执行的 Lua 代码发生过中断(错误或让出)后,还得继续调用 k 。所以我们还得继续改写这段代码,把lua_pcall 替换成lua_pcallk

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

注意这里那个额外的显式的对延续函数的调用:Lua 仅在需要时,这可能是由错误导致的也可能是发生了让出而需要继续运行,才会调用延续函数。如果没有发生过任何让出,调用的函数正常返回,那么 lua_pcallk(以及 lua_callk)也会正常返回。(当然,这个例子中你也可以不在之后调用延续函数,而是在原函数的调用后直接写上需要做的工作。)

除了 Lua 状态,延续函数还有两个参数:一个是调用最后的状态码,另一个一开始由lua_pcallk 传入的上下文(ctx)。(Lua 本身不使用这个值;它仅仅从原函数转发这个值给延续函数。)对于 lua_pcallk 而言,状态码和 lua_pcallk本应返回值相同,区别仅在于发生过让出后才执行完时,状态码为LUA_YIELD(而不是LUA_OK)。对于 lua_yieldklua_callk 而言,调用延续函数传入的状态码一定是LUA_YIELD。(对这两个函数,Lua 不会因任何错误而调用延续函数。因为它们并不处理错误。)同样,当你使用 lua_callk 时,你应该用 LUA_OK作为状态码来调用延续函数。(对于 lua_yieldk,几乎没有什么地方需要直接调用延续函数,因为 lua_yieldk 本身并不会返回。)

Lua 会把延续函数看作原函数。延续函数将接收到和原函数相同的 Lua 栈,其接收到的 lua 状态也和被调函数若返回后应该有的状态一致。(例如,lua_callk 调用之后,栈中之前压入的函数和调用参数都被调用产生的返回值所替代。)这时也有相同的上值。等到它返回的时候,Lua 会将其看待成原函数的返回去操作。