Lua 协程 -- setjmp longjmp 破坏?
Lua coroutines -- setjmp longjmp clobbering?
在不久前的 blog post 中,Scott Vokes 描述了与 lua 使用 C 函数 setjmp
和 [=17= 实现协程相关的技术问题]:
The main limitation of Lua coroutines is that, since they are implemented with setjmp(3) and longjmp(3), you cannot use them to call from Lua into C code that calls back into Lua that calls back into C, because the nested longjmp will clobber the C function’s stack frames. (This is detected at runtime, rather than failing silently.)
I haven’t found this to be a problem in practice, and I’m not aware of any way to fix it without damaging Lua’s portability, one of my favorite things about Lua — it will run on literally anything with an ANSI C compiler and a modest amount of space. Using Lua means I can travel light. :)
我使用了相当多的协程,我认为我大致了解发生了什么以及 setjmp
和 longjmp
做了什么,但是我在某个时候读到这篇文章并意识到我没有'不是很懂。为了解决这个问题,我尝试编写一个我认为应该根据描述引起问题的程序,但它似乎运行良好。
然而,我看到其他一些地方似乎有人声称存在问题:
问题是:
- 在什么情况下 lua 协程会因为 C 函数堆栈帧被破坏而无法工作?
- 结果究竟是什么? "detected at runtime" 是否意味着 lua 恐慌?或者别的什么?
- 这是否仍然影响 lua (5.3) 的最新版本,或者这实际上是 5.1 问题还是什么?
这是我生成的代码。在我的测试中,它是 linked with 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 的详细回答!
在阅读了他的回答、阅读了官方文档、阅读了一些电子邮件并进行了更多尝试之后,我想完善问题/提出一个具体的后续问题,但是你想看看它。
我认为这个术语 'clobbering' 不适合描述这个问题,这是让我感到困惑的部分原因 -- 从被写入两次和第一次的意义上来说,没有什么是 "clobbered"值丢失,问题只是,正如@Nicol Bolas 指出的那样,longjmp
扔掉了 C 堆栈的一部分,如果您希望稍后恢复堆栈,那就太糟糕了。
在@Nicol Bolas 提供的 link 中 section 4.7 of lua 5.2 manual 中实际上很好地描述了这个问题。
奇怪的是,lua 5.1 文档中没有对应的部分。然而,lua 5.2 有 this to say 关于 lua_yieldk
:
Yields a coroutine.
This function should only be called as the return expression of a C function, as follows:
return lua_yieldk (L, n, i, k);
Lua 5.1 手册说 something similar,关于 lua_yield
而不是:
Yields a coroutine.
This function should only be called as the return expression of a C function, as follows:
return lua_yieldk (L, n, i, k);
然后是一些自然的问题:
- 为什么我在这里使用
return
很重要?如果 lua_yieldk
会调用 longjmp
,那么 lua_yieldk
永远不会 return,所以我 return 应该没关系吧?所以这不可能是正在发生的事情,对吧?
- 假设
lua_yieldk
只是在 lua 状态中记录当前 C api 调用已经声明它想要让步,然后当它最终执行时 return, lua 会弄清楚接下来会发生什么。那么这就解决了保存 C 堆栈帧的问题,不是吗?因为在我们正常 return 到 lua 之后,那些堆栈帧无论如何都过期了——所以@Nicol Bolas 图片中描述的并发症被绕过了?其次,至少在 5.2 中,语义从来不是我们应该恢复 C 堆栈帧,似乎 - lua_yieldk
恢复到延续函数,而不是 lua_yieldk
调用者,并且 lua_yield
显然恢复到当前 api 调用的调用者,而不是 lua_yield
调用者本身。
还有,最重要的问题:
If I consistently use lua_yieldk
in the form return lua_yieldk(...)
specified in the docs, returning from a lua_CFunction
that was passed to lua, is it still possible to trigger the attempt to yield across a C-call boundary
error?
最后,(但这不太重要),我想看一个具体的例子,说明天真的程序员 "isn't careful" 并触发 attempt to yield across a C-call boundary
错误时的情况。我的想法是可能存在与 setjmp
和 longjmp
相关的问题,我们稍后需要扔堆栈帧,但我想看到一些真正的 lua / lua c api 代码,我可以指着它说 "for instance, don't do that",这出奇地难以捉摸。
我发现 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! :-)
想一想协程执行 yield
时会发生什么。它停止执行,并向在该协程上调用 resume
的任何人处理 returns,对吗?
嗯,假设你有这个代码:
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 的内部堆栈,由 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 堆栈的行为。也就是说,它将 return 到 setjmp
所在的位置。
Lua 堆栈将被保留,因为 Lua 堆栈与 C 堆栈分开。但是 C 堆栈? longjmp
和 setjmp
之间的所有内容?走了。卡普特。永远失去.
现在你可以走了,"wait, doesn't the Lua stack know that it went into C and back into Lua"?一点点。但是 Lua 堆栈不能做 C 不能做的事情。而且 C 根本无法保存堆栈(好吧,不是没有特殊的库)。因此,虽然 Lua 堆栈模糊地意识到某种 C 进程发生在它的堆栈中间,但它无法重建那里的内容。
那么如果你恢复这个 yield
ed 协同程序会发生什么?
Nasal demons. 没有人喜欢这些。幸运的是,Lua 5.1 及更高版本(至少)会在您尝试跨 C yield 时出错。
请注意 Lua 5.2+ does have ways of fixing this。但这不是自动的;它需要您进行明确的编码。
当协程中的Lua代码调用你的C代码,你的C代码调用Lua可能yield的代码时,你可以使用lua_callk
或lua_pcallk
调用可能产生的 Lua 函数。这些调用函数有一个额外的参数:"continuation" 函数。
如果您调用的 Lua 代码确实产生了,那么 lua_*callk
函数实际上不会 return (因为您的 C 堆栈将被破坏)。相反,它将调用您在 lua_*callk
函数中提供的延续函数。顾名思义,continuation 函数的工作是从上一个函数停止的地方继续。
现在,Lua 确实为您的延续函数保留了堆栈,因此它使堆栈处于与您的原始 C 函数所在的状态相同的状态。好吧,除了您调用的函数+参数 ( with lua_*callk
) 被删除,并且该函数的 return 值被压入堆栈。除此之外,堆栈都是一样的。
还有lua_yieldk
。这允许您的 C 函数返回到 Lua,这样当协程恢复时,它会调用提供的延续函数。
请注意 Coco 赋予 Lua 5.1 解决此问题的能力。它能够(尽管 OS/assembly/etc 魔法)在 yield 操作期间 保留 C 堆栈。 Lua2.0之前的JIT版本也提供了这个功能。
C++ 笔记
你用 C++ 标记标记了你的问题,所以我假设这里涉及到。
在 C 和 C++ 之间的许多差异中,C++ 远 比 Lua 更依赖于其调用堆栈的性质。在 C 语言中,如果丢弃堆栈,您可能会丢失未清理的资源。然而,C++ 需要在某个时候调用在堆栈上声明的函数的析构函数。该标准不允许您将它们扔掉。
因此,如果堆栈上没有 nothing 需要调用析构函数,那么延续仅在 C++ 中有效。或者更具体地说,如果您调用任何延续函数 Lua API,则只有可轻易破坏的类型才能位于堆栈中。
当然,Coco 可以很好地处理 C++,因为它实际上保留了 C++ 堆栈。
将此作为补充@Nicol Bolas 答案的答案发布,这样
我可以 space 写下我理解原文所花费的时间
问题,以及次要问题的答案/代码清单。
如果你阅读了 Nicol Bolas 的回答但仍然有像我一样的问题,这里是
一些额外的提示:
- 调用堆栈上的 三 层,Lua,C,Lua,对问题至关重要。
如果你只有两层,Lua 和 C,你就不会遇到这个问题。
- 想象协程调用应该如何工作——lua 堆栈看起来
某种方式,C 堆栈看起来某种方式,调用产生 (longjmp) 和
稍后恢复...问题不会立即发生
已恢复。
当恢复函数稍后尝试 return 到您的
C函数.
因为,要使协程语义生效,它应该 return
进入 C 函数调用,但是它的堆栈帧已经消失,并且不能
已恢复。
- 无法恢复这些堆栈帧的解决方法是
使用
lua_callk
、lua_pcallk
,这样您就可以提供替代品
可以调用的函数来代替其框架是的 C 函数
消灭了。
- 关于
return lua_yieldk(...)
的问题似乎与
任何一个。从略读 lua_yieldk
的实现看来
它确实总是 longjmp
,并且在一些模糊的情况下它可能只 return
涉及 lua 调试挂钩 (?)。
- Lua 在内部(当前版本)跟踪 yield 何时不应该
允许,通过保持计数器变量
nny
(数字不可屈服)关联
到 lua 状态,当您从 C api 调用 lua_call
或 lua_pcall
时
函数(您之前推送到 lua 的 lua_CFunction
),nny
是
递增,并且仅在调用或 pcall returns 时递减。什么时候
nny
是非零的,屈服是不安全的,如果你仍然尝试屈服,你会得到这个 yield across
C-api boundary
错误。
这是一个产生问题并报告错误的简单清单,
如果你像我一样喜欢有一个具体的代码示例。它展示了
使用 lua_call
、lua_pcall
和 lua_pcallk
的一些区别
在协程调用的函数中。
extern "C" {
#include <lauxlib.h>
#include <lua.h>
}
#include <cassert>
#include <iostream>
//#define USE_PCALL
//#define USE_PCALLK
#define CODE(C) \
case C: { \
std::cout << "When returning to " << where << " got code '" #C "'" << std::endl; \
break; \
}
#define ERRCODE(C) \
case C: { \
std::cout << "When returning to " << where << " got code '" #C "': " << lua_tostring(L, -1) << std::endl; \
break; \
}
int report_resume_code(int code, const char * where, lua_State * L) {
switch (code) {
CODE(LUA_OK)
CODE(LUA_YIELD)
ERRCODE(LUA_ERRRUN)
ERRCODE(LUA_ERRMEM)
ERRCODE(LUA_ERRERR)
default:
std::cout << "An unknown error code in " << where << ": " << lua_tostring(L, -1) << std::endl;
}
return code;
}
int report_pcall_code(int code, const char * where, lua_State * L) {
switch(code) {
CODE(LUA_OK)
ERRCODE(LUA_ERRRUN)
ERRCODE(LUA_ERRMEM)
ERRCODE(LUA_ERRERR)
default:
std::cout << "An unknown error code in " << where << ": " << lua_tostring(L, -1) << std::endl;
}
return code;
}
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', yielding" << std::endl;
return lua_yield(L, 0);
}
int g(lua_State * L) {
std::cout << "Called function 'g'" << std::endl;
lua_getglobal(L, "f");
#ifdef USE_PCALL
std::cout << "pcall..." << std::endl;
report_pcall_code(lua_pcall(L, 0, 0, 0), __func__, L);
// ^ yield across pcall!
// If we yield, there is no way ever to return normally from this pcall,
// so it is an error.
#elif defined(USE_PCALLK)
std::cout << "pcallk..." << std::endl;
report_pcall_code(lua_pcallk(L, 0, 0, 0, 0, trivial), __func__, L);
#else
std::cout << "call..." << std::endl;
lua_call(L, 0, 0);
// ^ yield across call!
// This results in an error being reported in lua_resume, rather than at
// the pcall
#endif
return 0;
}
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");
}
assert(lua_gettop(L) == 0);
// Some action
{
lua_State * T = lua_newthread(L);
lua_getglobal(T, "g");
while (LUA_YIELD == report_resume_code(lua_resume(T, L, 0), __func__, T)) {}
}
lua_close(L);
std::cout << "Bye! :-)" << std::endl;
}
示例输出:
call
Starting:
Called function 'g'
call...
Called function 'f', yielding
When returning to main got code 'LUA_ERRRUN': attempt to yield across a C-call boundary
Bye! :-)
pcall
Starting:
Called function 'g'
pcall...
Called function 'f', yielding
When returning to g got code 'LUA_ERRRUN': attempt to yield across a C-call boundary
When returning to main got code 'LUA_OK'
Bye! :-)
pcallk
Starting:
Called function 'g'
pcallk...
Called function 'f', yielding
When returning to main got code 'LUA_YIELD'
Called continuation function
When returning to main got code 'LUA_OK'
Bye! :-)
在不久前的 blog post 中,Scott Vokes 描述了与 lua 使用 C 函数 setjmp
和 [=17= 实现协程相关的技术问题]:
The main limitation of Lua coroutines is that, since they are implemented with setjmp(3) and longjmp(3), you cannot use them to call from Lua into C code that calls back into Lua that calls back into C, because the nested longjmp will clobber the C function’s stack frames. (This is detected at runtime, rather than failing silently.)
I haven’t found this to be a problem in practice, and I’m not aware of any way to fix it without damaging Lua’s portability, one of my favorite things about Lua — it will run on literally anything with an ANSI C compiler and a modest amount of space. Using Lua means I can travel light. :)
我使用了相当多的协程,我认为我大致了解发生了什么以及 setjmp
和 longjmp
做了什么,但是我在某个时候读到这篇文章并意识到我没有'不是很懂。为了解决这个问题,我尝试编写一个我认为应该根据描述引起问题的程序,但它似乎运行良好。
然而,我看到其他一些地方似乎有人声称存在问题:
问题是:
- 在什么情况下 lua 协程会因为 C 函数堆栈帧被破坏而无法工作?
- 结果究竟是什么? "detected at runtime" 是否意味着 lua 恐慌?或者别的什么?
- 这是否仍然影响 lua (5.3) 的最新版本,或者这实际上是 5.1 问题还是什么?
这是我生成的代码。在我的测试中,它是 linked with 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 的详细回答!
在阅读了他的回答、阅读了官方文档、阅读了一些电子邮件并进行了更多尝试之后,我想完善问题/提出一个具体的后续问题,但是你想看看它。
我认为这个术语 'clobbering' 不适合描述这个问题,这是让我感到困惑的部分原因 -- 从被写入两次和第一次的意义上来说,没有什么是 "clobbered"值丢失,问题只是,正如@Nicol Bolas 指出的那样,longjmp
扔掉了 C 堆栈的一部分,如果您希望稍后恢复堆栈,那就太糟糕了。
在@Nicol Bolas 提供的 link 中 section 4.7 of lua 5.2 manual 中实际上很好地描述了这个问题。
奇怪的是,lua 5.1 文档中没有对应的部分。然而,lua 5.2 有 this to say 关于 lua_yieldk
:
Yields a coroutine.
This function should only be called as the return expression of a C function, as follows:
return lua_yieldk (L, n, i, k);
Lua 5.1 手册说 something similar,关于 lua_yield
而不是:
Yields a coroutine.
This function should only be called as the return expression of a C function, as follows:
return lua_yieldk (L, n, i, k);
然后是一些自然的问题:
- 为什么我在这里使用
return
很重要?如果lua_yieldk
会调用longjmp
,那么lua_yieldk
永远不会 return,所以我 return 应该没关系吧?所以这不可能是正在发生的事情,对吧? - 假设
lua_yieldk
只是在 lua 状态中记录当前 C api 调用已经声明它想要让步,然后当它最终执行时 return, lua 会弄清楚接下来会发生什么。那么这就解决了保存 C 堆栈帧的问题,不是吗?因为在我们正常 return 到 lua 之后,那些堆栈帧无论如何都过期了——所以@Nicol Bolas 图片中描述的并发症被绕过了?其次,至少在 5.2 中,语义从来不是我们应该恢复 C 堆栈帧,似乎 -lua_yieldk
恢复到延续函数,而不是lua_yieldk
调用者,并且lua_yield
显然恢复到当前 api 调用的调用者,而不是lua_yield
调用者本身。
还有,最重要的问题:
If I consistently use
lua_yieldk
in the formreturn lua_yieldk(...)
specified in the docs, returning from alua_CFunction
that was passed to lua, is it still possible to trigger theattempt to yield across a C-call boundary
error?
最后,(但这不太重要),我想看一个具体的例子,说明天真的程序员 "isn't careful" 并触发 attempt to yield across a C-call boundary
错误时的情况。我的想法是可能存在与 setjmp
和 longjmp
相关的问题,我们稍后需要扔堆栈帧,但我想看到一些真正的 lua / lua c api 代码,我可以指着它说 "for instance, don't do that",这出奇地难以捉摸。
我发现 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! :-)
想一想协程执行 yield
时会发生什么。它停止执行,并向在该协程上调用 resume
的任何人处理 returns,对吗?
嗯,假设你有这个代码:
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 的内部堆栈,由 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 堆栈的行为。也就是说,它将 return 到 setjmp
所在的位置。
Lua 堆栈将被保留,因为 Lua 堆栈与 C 堆栈分开。但是 C 堆栈? longjmp
和 setjmp
之间的所有内容?走了。卡普特。永远失去.
现在你可以走了,"wait, doesn't the Lua stack know that it went into C and back into Lua"?一点点。但是 Lua 堆栈不能做 C 不能做的事情。而且 C 根本无法保存堆栈(好吧,不是没有特殊的库)。因此,虽然 Lua 堆栈模糊地意识到某种 C 进程发生在它的堆栈中间,但它无法重建那里的内容。
那么如果你恢复这个 yield
ed 协同程序会发生什么?
Nasal demons. 没有人喜欢这些。幸运的是,Lua 5.1 及更高版本(至少)会在您尝试跨 C yield 时出错。
请注意 Lua 5.2+ does have ways of fixing this。但这不是自动的;它需要您进行明确的编码。
当协程中的Lua代码调用你的C代码,你的C代码调用Lua可能yield的代码时,你可以使用lua_callk
或lua_pcallk
调用可能产生的 Lua 函数。这些调用函数有一个额外的参数:"continuation" 函数。
如果您调用的 Lua 代码确实产生了,那么 lua_*callk
函数实际上不会 return (因为您的 C 堆栈将被破坏)。相反,它将调用您在 lua_*callk
函数中提供的延续函数。顾名思义,continuation 函数的工作是从上一个函数停止的地方继续。
现在,Lua 确实为您的延续函数保留了堆栈,因此它使堆栈处于与您的原始 C 函数所在的状态相同的状态。好吧,除了您调用的函数+参数 ( with lua_*callk
) 被删除,并且该函数的 return 值被压入堆栈。除此之外,堆栈都是一样的。
还有lua_yieldk
。这允许您的 C 函数返回到 Lua,这样当协程恢复时,它会调用提供的延续函数。
请注意 Coco 赋予 Lua 5.1 解决此问题的能力。它能够(尽管 OS/assembly/etc 魔法)在 yield 操作期间 保留 C 堆栈。 Lua2.0之前的JIT版本也提供了这个功能。
C++ 笔记
你用 C++ 标记标记了你的问题,所以我假设这里涉及到。
在 C 和 C++ 之间的许多差异中,C++ 远 比 Lua 更依赖于其调用堆栈的性质。在 C 语言中,如果丢弃堆栈,您可能会丢失未清理的资源。然而,C++ 需要在某个时候调用在堆栈上声明的函数的析构函数。该标准不允许您将它们扔掉。
因此,如果堆栈上没有 nothing 需要调用析构函数,那么延续仅在 C++ 中有效。或者更具体地说,如果您调用任何延续函数 Lua API,则只有可轻易破坏的类型才能位于堆栈中。
当然,Coco 可以很好地处理 C++,因为它实际上保留了 C++ 堆栈。
将此作为补充@Nicol Bolas 答案的答案发布,这样 我可以 space 写下我理解原文所花费的时间 问题,以及次要问题的答案/代码清单。
如果你阅读了 Nicol Bolas 的回答但仍然有像我一样的问题,这里是 一些额外的提示:
- 调用堆栈上的 三 层,Lua,C,Lua,对问题至关重要。 如果你只有两层,Lua 和 C,你就不会遇到这个问题。
- 想象协程调用应该如何工作——lua 堆栈看起来
某种方式,C 堆栈看起来某种方式,调用产生 (longjmp) 和
稍后恢复...问题不会立即发生
已恢复。
当恢复函数稍后尝试 return 到您的 C函数.
因为,要使协程语义生效,它应该 return 进入 C 函数调用,但是它的堆栈帧已经消失,并且不能 已恢复。 - 无法恢复这些堆栈帧的解决方法是
使用
lua_callk
、lua_pcallk
,这样您就可以提供替代品 可以调用的函数来代替其框架是的 C 函数 消灭了。 - 关于
return lua_yieldk(...)
的问题似乎与 任何一个。从略读lua_yieldk
的实现看来 它确实总是longjmp
,并且在一些模糊的情况下它可能只 return 涉及 lua 调试挂钩 (?)。 - Lua 在内部(当前版本)跟踪 yield 何时不应该
允许,通过保持计数器变量
nny
(数字不可屈服)关联 到 lua 状态,当您从 C api 调用lua_call
或lua_pcall
时 函数(您之前推送到 lua 的lua_CFunction
),nny
是 递增,并且仅在调用或 pcall returns 时递减。什么时候nny
是非零的,屈服是不安全的,如果你仍然尝试屈服,你会得到这个yield across C-api boundary
错误。
这是一个产生问题并报告错误的简单清单,
如果你像我一样喜欢有一个具体的代码示例。它展示了
使用 lua_call
、lua_pcall
和 lua_pcallk
的一些区别
在协程调用的函数中。
extern "C" {
#include <lauxlib.h>
#include <lua.h>
}
#include <cassert>
#include <iostream>
//#define USE_PCALL
//#define USE_PCALLK
#define CODE(C) \
case C: { \
std::cout << "When returning to " << where << " got code '" #C "'" << std::endl; \
break; \
}
#define ERRCODE(C) \
case C: { \
std::cout << "When returning to " << where << " got code '" #C "': " << lua_tostring(L, -1) << std::endl; \
break; \
}
int report_resume_code(int code, const char * where, lua_State * L) {
switch (code) {
CODE(LUA_OK)
CODE(LUA_YIELD)
ERRCODE(LUA_ERRRUN)
ERRCODE(LUA_ERRMEM)
ERRCODE(LUA_ERRERR)
default:
std::cout << "An unknown error code in " << where << ": " << lua_tostring(L, -1) << std::endl;
}
return code;
}
int report_pcall_code(int code, const char * where, lua_State * L) {
switch(code) {
CODE(LUA_OK)
ERRCODE(LUA_ERRRUN)
ERRCODE(LUA_ERRMEM)
ERRCODE(LUA_ERRERR)
default:
std::cout << "An unknown error code in " << where << ": " << lua_tostring(L, -1) << std::endl;
}
return code;
}
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', yielding" << std::endl;
return lua_yield(L, 0);
}
int g(lua_State * L) {
std::cout << "Called function 'g'" << std::endl;
lua_getglobal(L, "f");
#ifdef USE_PCALL
std::cout << "pcall..." << std::endl;
report_pcall_code(lua_pcall(L, 0, 0, 0), __func__, L);
// ^ yield across pcall!
// If we yield, there is no way ever to return normally from this pcall,
// so it is an error.
#elif defined(USE_PCALLK)
std::cout << "pcallk..." << std::endl;
report_pcall_code(lua_pcallk(L, 0, 0, 0, 0, trivial), __func__, L);
#else
std::cout << "call..." << std::endl;
lua_call(L, 0, 0);
// ^ yield across call!
// This results in an error being reported in lua_resume, rather than at
// the pcall
#endif
return 0;
}
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");
}
assert(lua_gettop(L) == 0);
// Some action
{
lua_State * T = lua_newthread(L);
lua_getglobal(T, "g");
while (LUA_YIELD == report_resume_code(lua_resume(T, L, 0), __func__, T)) {}
}
lua_close(L);
std::cout << "Bye! :-)" << std::endl;
}
示例输出:
call
Starting:
Called function 'g'
call...
Called function 'f', yielding
When returning to main got code 'LUA_ERRRUN': attempt to yield across a C-call boundary
Bye! :-)
pcall
Starting:
Called function 'g'
pcall...
Called function 'f', yielding
When returning to g got code 'LUA_ERRRUN': attempt to yield across a C-call boundary
When returning to main got code 'LUA_OK'
Bye! :-)
pcallk
Starting:
Called function 'g'
pcallk...
Called function 'f', yielding
When returning to main got code 'LUA_YIELD'
Called continuation function
When returning to main got code 'LUA_OK'
Bye! :-)