【发布时间】:2016-03-22 02:04:54
【问题描述】:
在不久前的blog post 中,Scott Vokes 描述了一个与 lua 使用 C 函数 setjmp 和 longjmp 实现协程相关的技术问题:
Lua 协程的主要限制是,由于它们是使用 setjmp(3) 和 longjmp(3) 实现的,因此您不能使用它们从 Lua 调用 C 代码,然后再调用 Lua 再调用 C,因为嵌套的 longjmp 将破坏 C 函数的堆栈帧。 (这是在运行时检测到的,而不是静默失败。)
我没有发现这在实践中是个问题,而且我不知道有什么方法可以在不损害 Lua 的可移植性的情况下修复它,这是我最喜欢的 Lua 之一——它几乎可以在任何带有 ANSI 的东西上运行C 编译器和少量空间。使用 Lua 意味着我可以轻装上阵。 :)
我已经使用了相当多的协程,并且我认为我大致了解发生了什么以及 setjmp 和 longjmp 做了什么,但是我在某些时候读到了这篇文章并意识到我并没有真正理解它。为了弄清楚这一点,我尝试制作一个我认为应该根据描述导致问题的程序,但它似乎工作正常。
但是,我在其他一些地方看到人们似乎声称存在问题:
问题是:
- 在什么情况下 lua 协程会因为 C 函数栈帧被破坏而无法工作?
- 结果到底是什么? “在运行时检测到”是否意味着 lua 恐慌?还是别的什么?
- 这是否仍会影响最新版本的 lua (5.3) 或者这实际上是 5.1 问题还是什么?
这是我生成的代码。在我的测试中,它与lua 5.3.1链接,编译为C代码,测试本身编译为C++ 11标准的C++代码。
extern "C" {
#include <lauxlib.h>
#include <lua.h>
}
#include <cassert>
#include <iostream>
#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 提供的链接中的 section 4.7 of lua 5.2 manual 实际上很好地描述了这个问题。
奇怪的是,lua 5.1 文档中没有等效的部分。然而,lua 5.2 有this to say 大约lua_yieldk:
产生一个协程。
该函数只能作为C函数的返回表达式调用,如下:
return lua_yieldk (L, n, i, k);
Lua 5.1 手册说something similar,而不是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调用者本身。
还有,最重要的问题:
如果我一直以文档中指定的
return lua_yieldk(...)形式使用lua_yieldk,从传递给lua 的lua_CFunction返回,是否仍然可以触发attempt to yield across a C-call boundary错误?
最后,(但这不太重要),我想看看一个具体的例子,说明当一个天真的程序员“不小心”并触发attempt to yield across a C-call boundary 错误时会是什么样子。我知道setjmp 和longjmp 抛出我们以后需要的堆栈帧可能存在问题,但我想看到一些真正的 lua / lua c api 代码,我可以指向并说“例如,不要那样做”,这令人惊讶地难以捉摸。
我发现this email 有人用一些 lua 5.1 代码报告了这个错误,我试图在 lua 5.3 中重现它。但是我发现,这看起来只是来自 lua 实现的错误报告——实际的错误是由于用户没有正确设置他们的协程而引起的。加载协程的正确方法是,创建线程,将函数压入线程堆栈,然后在线程状态上调用lua_resume。相反,用户在线程堆栈上使用dofile,它在加载后执行函数,而不是恢复它。所以它实际上是yield outside of a coroutine iiuc,当我修补它时,他的代码工作正常,在 lua 5.3 中同时使用 lua_yield 和 lua_yieldk。
这是我制作的清单:
#include <cassert>
#include <cstdio>
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! :-)
【问题讨论】:
-
AFAIK,整个延续功能就是解决该问题的修复。如果您使用的是 Lua 5.3 或 5.2,则没有问题。
-
@immibis:不,你仍然有问题。只是你有一个潜在的解决方案。但这几乎不是自动的。
-
好的,你现在问的是一个全新的问题。您最初是在询问该帖子在谈论跳过堆栈的内容。现在你在问 Lua 5.2/3 是如何解决这个问题的。这是一个很好的问题,但它是一个新问题,应该使用“”按钮提出。
-
@NicolBolas:所以我知道你是从哪里来的,但请听我说完。当程序员就这样的问题提出问题时,他们通常想知道“在高层次上是什么问题,有哪些例子说明它是如何发生的,当我遇到问题时我需要做什么,实际上”。例如,在这个站点上,您可以看到诸如“什么是悬空指针”之类的问题,他们将通过代码示例给出非常详细的答案。我希望为协程提供这样的答案。我花了一段时间才弄清楚在此过程中我需要问什么问题。
-
如果你认为我需要重写整个东西来浓缩它,那是合理的。如果您认为我应该将其拆分为两个问题“协程跳过堆栈的问题是什么”和“如何解决某些示例代码中的协程跳过堆栈的问题”,我的意思是这也是合理的。但我认为将这两条信息放在同一个地方可能很有用。我的意思是通常在编程中我想要高水平以及“实际上这意味着什么”
标签: c++ c lua coroutine setjmp