在Lua和C的交互中,Lua经常调用C来完成一些性能敏感的操作,有时候C也会反过来回调Lua层的代码,比如 table.sort
这个API。
假设我们要用C来实现一个 foreach
函数用于遍历table,并在遍历过程中回调Lua的函数,让Lua能够访问每个key和value。
Lua层像这样使用这个函数:
local mylib = require "mylib" local t = {x = 1, y = 23, name = "jim"} mylib.foreach(t, print)
预期的输出是:
name jim x 1 y 23
这个函数的C代码如下:
static int l_foreach(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); luaL_checktype(L, 2, LUA_TFUNCTION); lua_pushnil(L); // table | fun | nil while (lua_next(L, 1) != 0) { // table | fun | key | value lua_pushvalue(L, 2); // table | fun | key | value | fun lua_pushvalue(L, -3); // table | fun | key | value | fun |key lua_pushvalue(L, -3); // table | fun | key | value | fun |key | value lua_call(L, 2, 0); // table | fun | key | value lua_pop(L, 1); // table | fun | key } return 0; }
foreach正常情况下执行得很好,但有一种情况却有问题:就是Lua的回调函数在一个协程中,且调用了coroutine.yield时,比如:
local mylib = require "mylib" local t = {x = 1, y = 23, name = "jim"} --创建一个协程 local co = coroutine.wrap(function() mylib.foreach(t, function(k, v) coroutine.yield(k, v) end) end) --让协程去取table的值返回 while true do local k, v = co() if k then print(k, v) else break end end
运行这段代码会得到这个错误: attempt to yield across a C-call boundary
。
原因是Lua使用longjmp来实现协程的挂起,longjmp会跳到其他地方去执行,使得后面的C代码被中断。 l_foreach
函数执行到lua_call,由于longjmp会使得后面的指令没机会再执行,就像这个函数突然消失了一样,这肯定会引起不可预知的后果,所以Lua不允许这种情况发生,它在调用coroutine.yield时抛出上面的错误。
那我们有没有办法让回调函数可以yield呢?在Lua5.1以前是不可以的,Lua5.2之后提供了几个新的API,允许回调的Lua函数可以yield,这几个API是: lua_yieldk, lua_callk, lua_pcallk
,像lua_callk的声明如下:
void lua_callk (lua_State *L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k);
其中最重要的是k参数,这是一个称为 continuation function
的回调函数。它的含义是lua_callk回调了函数,函数调用了yield使协程挂起,接着当协程恢复执行权回到C层时,Lua会调用这个k函数。那这个k函数的作用就很清楚了:延续lua_callk后面的逻辑。k函数的原型如下:
typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx);
status表示调用该函数时的状态,比如 LUA_YIELD
表明协程是从yield恢复回来的。
ctx就是lua_callk传入的那个参数,没有特别的含义,由调用者自己解释,它是一个可以容纳指针的整型。
有了lua_callk,我们就可以改造foreach函数,使其支持yield:
static int finishcall(lua_State *L, int status, lua_KContext ctx) { if (status == LUA_OK) lua_pushnil(L); else // LUA_YIELD lua_pop(L, 1); while (lua_next(L, 1) != 0) { lua_pushvalue(L, 2); lua_pushvalue(L, -3); lua_pushvalue(L, -3); lua_callk(L, 2, 0, 0, finishcall); lua_pop(L, 1); } return 0; } static int l_foreach(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); luaL_checktype(L, 2, LUA_TFUNCTION); return finishcall(L, LUA_OK, 0); }
具体的逻辑都移到finishcall去了,它有两种状态:
- LUA_OK 是l_foreach主动传入的,相当于初始执行状态,在这里lua_pushnil开始遍历table。
- LUA_YIELD 是Lua底层回调过来的,表明它所在的Lua协程从yield恢复了执行权。lua_calk后面的代码在yield之后没有机会执行,所以这个分支要做的是恢复后面的代码(lua_pop),然后继续循环。
我们再次执行上面的Lua代码,就能看到下面的输出:
y 23 x 1 name jim
在协程里,可以通过 coroutine.isyieldable
判断协程是否可以yield。
虽然可以用延续函数来实现回调函数的yield。但这不是万灵药,有些C函数需要很多上下文才能支持代码的延续,比如像table.sort这种严重依赖于递归的函数。为了支持yield而把代码搞得很复杂,而且还丢失了一些性能,这明显有些得不偿失。所以Lua API很多回调函数都不支持yield,这也是完全合理的。
参考资料