4.7 – C 中的让出处理
Lua 内部使用 C 的 longjmp
机制让出一个协程。因此,如果一个 C 函数 foo
调用了一个 API 函数,而这个 API 函数让出了(直接或间接调用了让出函数)。由于 longjmp
会移除 C 栈的栈帧,Lua 就无法返回到 foo
里了。
为了回避这类问题,碰到 API 调用中调用让出时,除了那些抛出错误的 API 外,还提供了三个函数:lua_yieldk
,lua_callk
,和 lua_pcallk
。它们在让出发生时,可以从传入的 延续函数(名为 k
的参数)继续运行。
我们需要预设一些术语来解释延续点。对于从 Lua 中调用的 C 函数,我们称之为 原函数。从这个原函数中调用的上面所述的三个 C API 函数我们称之为 被调函数。被调函数可以使当前线程让出。(让出发生在被调函数是 lua_yieldk
,或传入 lua_callk
或lua_pcallk
的函数调用了让出时。)
假设正在运行的线程在执行被调函数时让出。当再次延续这条线程,它希望继续被调函数的运行。然而,被调函数不可能返回到原函数中。这是因为之前的让出操作破坏了 C 栈的栈帧。作为替代品,Lua 调用那个作为被调函数参数给出的 延续函数 。正如其名,延续函数将延续原函数的任务。
下面的函数会做一个说明:
- int original_function (lua_State *L) {
- ... /* code 1 */
- status = lua_pcall(L, n, m, h); /* calls Lua */
- ... /* code 2 */
- }
现在我们想允许被lua_pcall
运行的 Lua 代码让出。首先,我们把函数改写成这个样子:
- 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 说,你必须在被lua_pcall
执行的 Lua 代码发生过中断(错误或让出)后,还得继续调用 k
。所以我们还得继续改写这段代码,把lua_pcall
替换成lua_pcallk
:
- int original_function (lua_State *L) {
- ... /* code 1 */
- return k(L, lua_pcallk(L, n, m, h, ctx2, k), ctx1);
- }
注意这里那个额外的显式的对延续函数的调用:Lua 仅在需要时,这可能是由错误导致的也可能是发生了让出而需要继续运行,才会调用延续函数。如果没有发生过任何让出,调用的函数正常返回,那么 lua_pcallk
(以及 lua_callk
)也会正常返回。(当然,这个例子中你也可以不在之后调用延续函数,而是在原函数的调用后直接写上需要做的工作。)
除了 Lua 状态,延续函数还有两个参数:一个是调用最后的状态码,另一个一开始由lua_pcallk
传入的上下文(ctx
)。(Lua 本身不使用这个值;它仅仅从原函数转发这个值给延续函数。)对于 lua_pcallk
而言,状态码和 lua_pcallk
本应返回值相同,区别仅在于发生过让出后才执行完时,状态码为LUA_YIELD
(而不是LUA_OK
)。对于 lua_yieldk
和lua_callk
而言,调用延续函数传入的状态码一定是LUA_YIELD
。(对这两个函数,Lua 不会因任何错误而调用延续函数。因为它们并不处理错误。)同样,当你使用 lua_callk
时,你应该用 LUA_OK
作为状态码来调用延续函数。(对于 lua_yieldk
,几乎没有什么地方需要直接调用延续函数,因为 lua_yieldk
本身并不会返回。)
Lua 会把延续函数看作原函数。延续函数将接收到和原函数相同的 Lua 栈,其接收到的 lua 状态也和被调函数若返回后应该有的状态一致。(例如,lua_callk
调用之后,栈中之前压入的函数和调用参数都被调用产生的返回值所替代。)这时也有相同的上值。等到它返回的时候,Lua 会将其看待成原函数的返回去操作。