在不久前的blog post中,Scott Vokes描述了使用C函数setjmp
和longjmp
执行协同程序时与lua相关的技术问题:
Lua协同程序的主要限制是,因为它们是用setjmp(3)和longjmp(3)实现的,所以你不能用它们从Lua调用C代码,这些代码回调回调用C的Lua,因为嵌套的longjmp将破坏C函数的堆栈帧。 (这是在运行时检测到的,而不是以静默方式失败。)
我没有发现这在实践中是一个问题,我不知道有什么方法可以解决它而不损坏Lua的可移植性,这是我最喜欢的Lua之一 - 它几乎可以运行任何ANSI C编译器和适度的空间。使用Lua意味着我可以轻装上阵。 :)
我已经使用了很多协同程序,我认为我已经广泛地理解了发生了什么以及setjmp
和longjmp
做了什么,但是我在某个时候读到了这一点,并意识到我没有'真的明白了。为了弄明白这一点,我尝试制作一个我认为应该根据描述引起问题的程序,相反它似乎工作正常。
然而,我认为其他一些地方的人似乎也指出存在问题:
问题是:
这是我制作的代码。在我的测试中,它与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的详细解答!
在阅读了他的答案,阅读官方文档,阅读一些电子邮件并再玩一遍之后,我想改进问题/提出具体的后续问题,但是你想看看它。
我认为这个术语&#39; blobbering&#39;对于描述这个问题并不好,这也是让我感到困惑的一部分 - 没有任何东西被破坏了#34;在被写入两次并且第一个值丢失的意义上,问题仅仅是,正如@Nicol Bolas指出的那样,longjmp
抛出C堆栈的一部分,如果您希望稍后恢复堆栈太糟糕了。
在section 4.7 of lua 5.2 manual中,@ Nicol Bolas提供的链接中实际上很好地描述了这个问题。
奇怪的是,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
调用者,{{1显然恢复到当前api调用的调用者,而不是lua_yield
调用者本身。而且,最重要的问题是:
如果我始终在文档中指定的
lua_yield
格式中使用lua_yieldk
,从传递给lua的return lua_yieldk(...)
返回,是否仍然可以触发{{1}错误?
最后,(但这不那么重要),我希望看到一个具体的例子,当一个天真的程序员不小心时,它会是什么样子。并触发lua_CFunction
错误。我认为可能存在与attempt to yield across a C-call boundary
和attempt to yield across a C-call boundary
抛出我们后来需要的堆栈帧相关的问题,但我希望看到一些真实的lua / lua c api代码,我可以指出并说出来&#34;例如,不要这样做&#34;,这是令人惊讶的难以捉摸。
我发现this email有人用某些lua 5.1代码报告了这个错误,我试图在lua 5.3中重现它。然而,我发现,这看起来只是来自lua实现的错误报告 - 实际的错误是由于用户没有正确设置他们的协同程序。加载协程的正确方法是,创建线程,将函数推送到线程堆栈,然后在线程状态上调用setjmp
。相反,用户在线程堆栈上使用longjmp
,它在加载后执行函数,而不是恢复它。所以它实际上是lua_resume
iiuc,当我修补它时,他的代码在lua 5.3中使用dofile
和yield outside of a coroutine
工作正常。
以下是我制作的商品目录:
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! :-)
答案 0 :(得分: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的内部堆栈,由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&#34;?一点点。但Lua堆栈无法执行C无法执行的操作。并且C根本不能保留堆栈(好吧,不是没有特殊的库)。因此,虽然Lua堆栈模糊地意识到在堆栈中间发生了某种C进程,但它无法重构那里的内容。
那么如果你恢复这个yield
ed协程会怎么样?
Nasal demons.没有人喜欢这些。幸运的是,只要你尝试跨越C语言,Lua 5.1及以上(至少)就会出错。
请注意Lua 5.2+ does have ways of fixing this。但它并不是自动的;它需要你的明确编码。
当协程中的Lua代码调用您的C代码,并且您的C代码调用可能产生的Lua代码时,您可以使用lua_callk
或lua_pcallk
来调用可能产生的Lua函数。这些调用函数需要一个额外的参数:a&#34; continuation&#34;功能
如果你调用的Lua代码确实产生了,那么lua_*callk
函数实际上不会返回(因为你的C堆栈已被销毁)。相反,它会调用您在lua_*callk
函数中提供的延续函数。正如您可以通过名称猜测的那样,继续功能的工作是继续前一个功能停止的地方。
现在,Lua确实保留了连续函数的堆栈,因此它使堆栈处于与原始C函数相同的状态。好吧,除了你调用的函数+参数(使用lua_*callk
)被删除,并且该函数的返回值被压入堆栈。除此之外,堆栈是完全相同的。
还有lua_yieldk
。这允许你的C函数回退到Lua,这样当协程恢复时,它会调用提供的延续函数。
请注意Coco使Lua 5.1能够解决此问题。在屈服运算期间,保留 C堆栈能够(虽然OS /汇编/等魔术)。 2.0之前的LuaJIT版本也提供了此功能。
C ++ note
您使用C ++标记标记了您的问题,因此我假设此处涉及。
C和C ++之间的许多差异在于,C ++ 远更依赖于其调用堆栈的性质而不是Lua。在C中,如果丢弃堆栈,可能会丢失未清理的资源。但是,C ++需要在某个时刻调用在栈上声明的函数的析构函数。该标准不允许你扔掉它们。
因此,如果堆栈上有 nothing 需要进行析构函数调用,则continuation仅适用于C ++。或者更具体地说,如果你调用任何一个连续函数Lua API,那么只有易于破坏的类型可以放在堆栈上。
当然,Coco处理C ++很好,因为它实际上保留了C ++堆栈。
答案 1 :(得分:1)
将此作为答案补充@Nicol Bolas的回答,以及如此 我可以有空间写下我理解原文的内容 问题,以及次要问题/代码清单的答案。
如果您阅读Nicol Bolas的回答,但仍然有类似我的问题,请点击此处 一些额外的提示:
lua_callk
,lua_pcallk
,您可以提供替代品
可以调用的函数代替帧的C函数
消灭了。return lua_yieldk(...)
的问题似乎与此无关
任何一个。从略读lua_yieldk
的实施情况看来
它确实总是longjmp
,它可能只会在一些模糊的情况下返回
涉及lua调试钩子(?)。nny
(数字不可屈服)相关联
到lua州,当你从C api拨打lua_call
或lua_pcall
时
函数(您之前推送到lua的lua_CFunction
),nny
是
递增,并且仅在该调用或pcall返回时递减。什么时候
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! :-)