在不久前的博客文章中,Scott Vokes描述了与lua使用C函数实现协程相关的技术问题,setjmp
并且longjmp
:
Lua协同程序的主要限制是,由于它们是用setjmp(3)和longjmp(3)实现的,所以你不能用它们从Lua调用C代码调用回调用回调用C的Lua,因为嵌套的longjmp将破坏C函数的堆栈帧.(这是在运行时检测到的,而不是静默失败.)
我没有发现这在实践中是一个问题,我不知道有什么方法可以修复它而不损坏Lua的可移植性,这是我最喜欢的Lua之一 - 它几乎可以运行任何ANSI C编译器和适度的空间.使用Lua意味着我可以轻装上阵.:)
我已经使用了很多协同程序,我认为我已经广泛地理解了发生了什么setjmp
,longjmp
做了什么和做了什么,但是我在某个时候读到了它,并意识到我并没有真正理解它.为了弄明白这一点,我尝试制作一个我认为应该根据描述引起问题的程序,相反它似乎工作正常.
然而,我看到其他一些地方人们似乎声称存在问题:
http://coco.luajit.org/
http://lua-users.org/lists/lua-l/2005-03/msg00179.html
问题是:
在什么情况下,由于C函数堆栈框架遭到破坏,lua协同程序无法工作?
到底是什么结果?"在运行时检测到"是否意味着,lua恐慌?或者是其他东西?
这是否仍会影响lua(5.3)的最新版本,或者这实际上是5.1问题还是什么?
这是我制作的代码.在我的测试中,它与lua 5.3.1链接,编译为C代码,测试本身在C++ 11标准下编译为C++代码.
extern "C" { #include#include } #include #include #define CODE(C) \ case C: { \ std::cout << "When returning to " << where << " got code '" #C "'" << std::endl; \ break; \ } void handle_resume_code(int code, const char * where) { switch (code) { CODE(LUA_OK) CODE(LUA_YIELD) CODE(LUA_ERRRUN) CODE(LUA_ERRMEM) CODE(LUA_ERRERR) default: std::cout << "An unknown error code in " << where << std::endl; } } int trivial(lua_State *, int, lua_KContext) { std::cout << "Called continuation function" << std::endl; return 0; } int f(lua_State * L) { std::cout << "Called function 'f'" << std::endl; return 0; } int g(lua_State * L) { std::cout << "Called function 'g'" << std::endl; lua_State * T = lua_newthread(L); lua_getglobal(T, "f"); handle_resume_code(lua_resume(T, L, 0), __func__); return lua_yieldk(L, 0, 0, trivial); } int h(lua_State * L) { std::cout << "Called function 'h'" << std::endl; lua_State * T = lua_newthread(L); lua_getglobal(T, "g"); handle_resume_code(lua_resume(T, L, 0), __func__); return lua_yieldk(L, 0, 0, trivial); } int main () { std::cout << "Starting:" << std::endl; lua_State * L = luaL_newstate(); // init { lua_pushcfunction(L, f); lua_setglobal(L, "f"); lua_pushcfunction(L, g); lua_setglobal(L, "g"); lua_pushcfunction(L, h); lua_setglobal(L, "h"); } assert(lua_gettop(L) == 0); // Some action { lua_State * T = lua_newthread(L); lua_getglobal(T, "h"); handle_resume_code(lua_resume(T, nullptr, 0), __func__); } lua_close(L); std::cout << "Bye! :-)" << std::endl; }
我得到的输出是:
Starting: Called function 'h' Called function 'g' Called function 'f' When returning to g got code 'LUA_OK' When returning to h got code 'LUA_YIELD' When returning to main got code 'LUA_YIELD' Bye! :-)
非常感谢@ Nicol Bolas的详细解答!
在阅读了他的答案,阅读官方文档,阅读一些电子邮件并再玩一遍之后,我想改进问题/提出具体的后续问题,但是你想看看它.
我认为这个术语"破坏"并不适合描述这个问题,而这也是困扰我的一部分 - 没有任何东西在被写入两次并且第一个价值被丢失的意义上被"摧毁",问题仅在于,正如@Nicol Bolas指出的那样,抛出longjmp
C堆栈的一部分,如果你希望稍后恢复堆栈,那就太糟糕了.
在@Nicol Bolas提供的链接中,实际上在lua 5.2手册的4.7节中非常清楚地描述了这个问题.
奇怪的是,lua 5.1文档中没有等效的部分.然而,LUA 5.2有这样一段话约lua_yieldk
:
产生协程.
该函数只应作为C函数的返回表达式调用,如下所示:
return lua_yieldk (L, n, i, k);
相反,Lua 5.1手册说了类似的东西lua_yield
:
产生协程.
该函数只应作为C函数的返回表达式调用,如下所示:
return lua_yieldk (L, n, i, k);
那么一些自然的问题:
如果我return
在这里使用与否,为什么重要?如果lua_yieldk
会打电话,longjmp
那么lua_yieldk
永远不会返回,所以如果我回来那么无所谓?所以这不可能是正在发生的事情,对吧?
相反,假设lua_yieldk
只是在lua状态中记录当前的C api调用已经声明它想要屈服,然后当它最终返回时,lua将弄清楚接下来会发生什么.那么这解决了保存C堆栈帧的问题,不是吗?因为在我们正常返回lua之后,那些堆栈帧已经过期了 - 所以@Nicol Bolas图片中描述的并发症是绕过的吗?第二,在5.2中,至少语义从来都不是我们应该恢复C堆栈帧,似乎 - lua_yieldk
恢复到延续函数,而不是lua_yieldk
调用者,并且lua_yield
显然恢复到当前api调用的调用者,而不是在lua_yield
调用者本身.
而且,最重要的问题是:
如果我一直使用文档中指定
lua_yieldk
的表单return lua_yieldk(...)
,从lua_CFunction
传递给lua 的表单返回,是否仍然可以触发attempt to yield across a C-call boundary
错误?
最后,(但这不太重要),我想看一个具体的例子,当一个天真的程序员"不小心"并触发attempt to yield across a C-call boundary
错误时它看起来像什么.我的想法,有可能是问题的关联setjmp
和longjmp
折腾堆栈帧,我们以后需要的,但我想看到一些真正的LUA/LUA C API代码,我可以指出,并说:"例如,不这样做" ,这是令人惊讶的难以捉摸的.
我发现这个电子邮件,有人用一些lua 5.1代码报告了这个错误,我试图在lua 5.3中重现它.然而,我发现,这看起来只是来自lua实现的错误报告 - 实际的错误是由于用户没有正确设置他们的协同程序.加载协同程序的正确方法是,创建线程,将函数推送到线程堆栈,然后调用lua_resume
线程状态.相反,用户正在使用dofile
线程堆栈,它在加载后执行函数,而不是恢复它.所以它实际上是yield outside of a coroutine
iiuc,当我修补它时,他的代码工作正常,使用lua_yield
和lua_yieldk
lua 5.3.
这是我制作的列表:
#include#include extern "C" { #include "lua.h" #include "lauxlib.h" } //#define USE_YIELDK bool running = true; int lua_print(lua_State * L) { if (lua_gettop(L)) { printf("lua: %s\n", lua_tostring(L, -1)); } return 0; } int lua_finish(lua_State *L) { running = false; printf("%s called\n", __func__); return 0; } int trivial(lua_State *, int, lua_KContext) { printf("%s called\n", __func__); return 0; } int lua_sleep(lua_State *L) { printf("%s called\n", __func__); #ifdef USE_YIELDK printf("Calling lua_yieldk\n"); return lua_yieldk(L, 0, 0, trivial); #else printf("Calling lua_yield\n"); return lua_yield(L, 0); #endif } const char * loop_lua = "print(\"loop.lua\")\n" "\n" "local i = 0\n" "while true do\n" " print(\"lua_loop iteration\")\n" " sleep()\n" "\n" " i = i + 1\n" " if i == 4 then\n" " break\n" " end\n" "end\n" "\n" "finish()\n"; int main() { lua_State * L = luaL_newstate(); lua_pushcfunction(L, lua_print); lua_setglobal(L, "print"); lua_pushcfunction(L, lua_sleep); lua_setglobal(L, "sleep"); lua_pushcfunction(L, lua_finish); lua_setglobal(L, "finish"); lua_State* cL = lua_newthread(L); assert(LUA_OK == luaL_loadstring(cL, loop_lua)); /*{ int result = lua_pcall(cL, 0, 0, 0); if (result != LUA_OK) { printf("%s error: %s\n", result == LUA_ERRRUN ? "Runtime" : "Unknown", lua_tostring(cL, -1)); return 1; } }*/ // ^ This pcall (predictably) causes an error -- if we try to execute the // script, it is going to call things that attempt to yield, but we did not // start the script with lua_resume, we started it with pcall, so it's not // okay to yield. // The reported error is "attempt to yield across a C-call boundary", but what // is really happening is just "yield from outside a coroutine" I suppose... while (running) { int status; printf("Waking up coroutine\n"); status = lua_resume(cL, L, 0); if (status == LUA_YIELD) { printf("coroutine yielding\n"); } else { running = false; // you can't try to resume if it didn't yield if (status == LUA_ERRRUN) { printf("Runtime error: %s\n", lua_isstring(cL, -1) ? lua_tostring(cL, -1) : "(unknown)" ); lua_pop(cL, -1); break; } else if (status == LUA_OK) { printf("coroutine finished\n"); } else { printf("Unknown error\n"); } } } lua_close(L); printf("Bye! :-)\n"); return 0; }
这是USE_YIELDK
注释掉时的输出:
Waking up coroutine lua: loop.lua lua: lua_loop iteration lua_sleep called Calling lua_yield coroutine yielding Waking up coroutine lua: lua_loop iteration lua_sleep called Calling lua_yield coroutine yielding Waking up coroutine lua: lua_loop iteration lua_sleep called Calling lua_yield coroutine yielding Waking up coroutine lua: lua_loop iteration lua_sleep called Calling lua_yield coroutine yielding Waking up coroutine lua_finish called coroutine finished Bye! :-)
这USE_YIELDK
是定义时的输出:
Waking up coroutine lua: loop.lua lua: lua_loop iteration lua_sleep called Calling lua_yieldk coroutine yielding Waking up coroutine trivial called lua: lua_loop iteration lua_sleep called Calling lua_yieldk coroutine yielding Waking up coroutine trivial called lua: lua_loop iteration lua_sleep called Calling lua_yieldk coroutine yielding Waking up coroutine trivial called lua: lua_loop iteration lua_sleep called Calling lua_yieldk coroutine yielding Waking up coroutine trivial called lua_finish called coroutine finished Bye! :-)
Nicol Bolas.. 9
想想当一个协程做了什么时会发生什么yield
.它停止执行,并且处理返回到resume
该协程上调用的任何人,对吗?
好吧,假设你有这个代码:
function top() coroutine.yield() end function middle() top() end function bottom() middle() end local co = coroutine.create(bottom); coroutine.resume(co);
在调用的那一刻yield
,Lua堆栈看起来像这样:
-- top -- middle -- bottom -- yield point
当你调用时yield
,保留了作为协同程序一部分的Lua调用堆栈.执行此操作时resume
,将再次执行保留的调用堆栈,从之前停止的位置开始.
好的,现在让我们说这middle
实际上不是Lua函数.相反,它是一个C函数,C函数调用Lua函数top
.从概念上讲,你的堆栈看起来像这样:
-- Lua - top -- C - middle -- Lua - bottom -- Lua - yield point
现在,请注意我之前说过的内容:这就是你的堆栈在概念上的样子.
因为你的实际调用堆栈看起来不像这样.
实际上,实际上有两个堆栈.有一个Lua的内部堆栈,由a定义lua_State
.并且有C的堆栈.Lua的内部堆栈,在yield
即将被调用的时候,看起来像这样:
-- top -- Some C stuff -- bottom -- yield point
那么堆栈对C来说是什么样的?好吧,它看起来像这样:
-- arbitrary Lua interpreter stuff -- middle -- arbitrary Lua interpreter stuff -- setjmp
那就是问题所在.看,当Lua做了yield
,它会打电话longjmp
.该函数基于C堆栈的行为.也就是说,它会回到setjmp
原来的位置.
Lua堆栈将被保留,因为Lua堆栈与C堆栈是分开的.但是C堆栈?longjmp
和setjmp
?之间的一切.不见了.过时了.永远失去了.
现在你可以去,"等等,Lua堆栈不知道它进入C并回到Lua"吗?一点点.但Lua堆栈无法执行C无法执行的操作.并且C根本不能保留堆栈(好吧,不是没有特殊的库).因此,虽然Lua堆栈模糊地意识到在堆栈中间发生了某种C进程,但它无法重构那里的内容.
那么如果你恢复这个yield
ed协程会发生什么呢?
鼻子恶魔.没有人喜欢那些.幸运的是,无论何时尝试跨越C,Lua 5.1及以上(至少)都会出错.
请注意,Lua 5.2+ 确实有办法解决这个问题.但这不是自动的; 它需要你的明确编码.
当协同程序中的Lua代码调用您的C代码,并且您的C代码调用可能产生的Lua代码时,您可以使用lua_callk
或lua_pcallk
调用可能产生的Lua函数.这些调用函数需要一个额外的参数:"延续"函数.
如果你调用的Lua代码确实产生了,那么该lua_*callk
函数将不会实际返回(因为你的C堆栈将被销毁).相反,它将调用您在lua_*callk
函数中提供的延续函数.正如您可以通过名称猜测的那样,延续功能的工作是继续前一个功能停止的位置.
现在,Lua确实保留了连续函数的堆栈,因此它使堆栈处于原始C函数所处的相同状态.好吧,除了你调用的函数+参数(with lua_*callk
)被删除,返回值从该函数被推入您的堆栈.除此之外,堆栈完全相同.
还有lua_yieldk
.这允许你的C函数回退到Lua,这样当协程恢复时,它会调用提供的continuation函数.
请注意,Coco为Lua 5.1提供了解决此问题的能力.它能够(虽然OS/assembly/etc magic)在yield操作期间保留 C堆栈.2.0之前的LuaJIT版本也提供了此功能.
C++注意
你用C++标记标记了你的问题,所以我假设这里涉及到了.
在C和C之间的诸多差异++的事实是,C++是远远更多地依赖于它的调用堆栈比的Lua的本质.在C中,如果丢弃堆栈,则可能会丢失未清理的资源.但是,C++需要在某个时刻调用在栈上声明的函数的析构函数.该标准不允许您扔掉它们.
因此,如果堆栈中没有任何东西需要进行析构函数调用,那么continuation只能在C++中工作.或者更具体地说,如果你调用任何一个连续函数Lua API,那么只有易于破坏的类型可以放在堆栈上.
当然,Coco处理C++很好,因为它实际上保留了C++堆栈.
想想当一个协程做了什么时会发生什么yield
.它停止执行,并且处理返回到resume
该协程上调用的任何人,对吗?
好吧,假设你有这个代码:
function top() coroutine.yield() end function middle() top() end function bottom() middle() end local co = coroutine.create(bottom); coroutine.resume(co);
在调用的那一刻yield
,Lua堆栈看起来像这样:
-- top -- middle -- bottom -- yield point
当你调用时yield
,保留了作为协同程序一部分的Lua调用堆栈.执行此操作时resume
,将再次执行保留的调用堆栈,从之前停止的位置开始.
好的,现在让我们说这middle
实际上不是Lua函数.相反,它是一个C函数,C函数调用Lua函数top
.从概念上讲,你的堆栈看起来像这样:
-- Lua - top -- C - middle -- Lua - bottom -- Lua - yield point
现在,请注意我之前说过的内容:这就是你的堆栈在概念上的样子.
因为你的实际调用堆栈看起来不像这样.
实际上,实际上有两个堆栈.有一个Lua的内部堆栈,由a定义lua_State
.并且有C的堆栈.Lua的内部堆栈,在yield
即将被调用的时候,看起来像这样:
-- top -- Some C stuff -- bottom -- yield point
那么堆栈对C来说是什么样的?好吧,它看起来像这样:
-- arbitrary Lua interpreter stuff -- middle -- arbitrary Lua interpreter stuff -- setjmp
那就是问题所在.看,当Lua做了yield
,它会打电话longjmp
.该函数基于C堆栈的行为.也就是说,它会回到setjmp
原来的位置.
Lua堆栈将被保留,因为Lua堆栈与C堆栈是分开的.但是C堆栈?longjmp
和setjmp
?之间的一切.不见了.过时了.永远失去了.
现在你可以去,"等等,Lua堆栈不知道它进入C并回到Lua"吗?一点点.但Lua堆栈无法执行C无法执行的操作.并且C根本不能保留堆栈(好吧,不是没有特殊的库).因此,虽然Lua堆栈模糊地意识到在堆栈中间发生了某种C进程,但它无法重构那里的内容.
那么如果你恢复这个yield
ed协程会发生什么呢?
鼻子恶魔.没有人喜欢那些.幸运的是,无论何时尝试跨越C,Lua 5.1及以上(至少)都会出错.
请注意,Lua 5.2+ 确实有办法解决这个问题.但这不是自动的; 它需要你的明确编码.
当协同程序中的Lua代码调用您的C代码,并且您的C代码调用可能产生的Lua代码时,您可以使用lua_callk
或lua_pcallk
调用可能产生的Lua函数.这些调用函数需要一个额外的参数:"延续"函数.
如果你调用的Lua代码确实产生了,那么该lua_*callk
函数将不会实际返回(因为你的C堆栈将被销毁).相反,它将调用您在lua_*callk
函数中提供的延续函数.正如您可以通过名称猜测的那样,延续功能的工作是继续前一个功能停止的位置.
现在,Lua确实保留了连续函数的堆栈,因此它使堆栈处于原始C函数所处的相同状态.好吧,除了你调用的函数+参数(with lua_*callk
)被删除,返回值从该函数被推入您的堆栈.除此之外,堆栈完全相同.
还有lua_yieldk
.这允许你的C函数回退到Lua,这样当协程恢复时,它会调用提供的continuation函数.
请注意,Coco为Lua 5.1提供了解决此问题的能力.它能够(虽然OS/assembly/etc magic)在yield操作期间保留 C堆栈.2.0之前的LuaJIT版本也提供了此功能.
C++注意
你用C++标记标记了你的问题,所以我假设这里涉及到了.
在C和C之间的诸多差异++的事实是,C++是远远更多地依赖于它的调用堆栈比的Lua的本质.在C中,如果丢弃堆栈,则可能会丢失未清理的资源.但是,C++需要在某个时刻调用在栈上声明的函数的析构函数.该标准不允许您扔掉它们.
因此,如果堆栈中没有任何东西需要进行析构函数调用,那么continuation只能在C++中工作.或者更具体地说,如果你调用任何一个连续函数Lua API,那么只有易于破坏的类型可以放在堆栈上.
当然,Coco处理C++很好,因为它实际上保留了C++堆栈.