Lua coroutines - setjmp longjmp clobbering?

时间:2015-12-16 03:33:29

标签: c++ c lua coroutine longjmp

在不久前的blog post中,Scott Vokes描述了使用C函数setjmplongjmp执行协同程序时与lua相关的技术问题:

  

Lua协同程序的主要限制是,因为它们是用setjmp(3)和longjmp(3)实现的,所以你不能用它们从Lua调用C代码,这些代码回调回调用C的Lua,因为嵌套的longjmp将破坏C函数的堆栈帧。 (这是在运行时检测到的,而不是以静默方式失败。)

     

我没有发现这在实践中是一个问题,我不知道有什么方法可以解决它而不损坏Lua的可移植性,这是我最喜欢的Lua之一 - 它几乎可以运行任何ANSI C编译器和适度的空间。使用Lua意味着我可以轻装上阵。 :)

我已经使用了很多协同程序,我认为我已经广泛地理解了发生了什么以及setjmplongjmp做了什么,但是我在某个时候读到了这一点,并意识到我没有'真的明白了。为了弄明白这一点,我尝试制作一个我认为应该根据描述引起问题的程序,相反它似乎工作正常。

然而,我认为其他一些地方的人似乎也指出存在问题:

问题是:

  • 在什么情况下,由于C函数堆栈框架遭到破坏,lua协同程序无法正常工作?
  • 结果到底是什么?是否"在运行时检测到" lari恐慌?或其他什么?
  • 这是否仍会影响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的详细解答!
在阅读了他的答案,阅读官方文档,阅读一些电子邮件并再玩一遍之后,我想改进问题/提出具体的后续问题,但是你想看看它。

我认为这个术语&#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 boundaryattempt 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中使用dofileyield 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! :-)

2 个答案:

答案 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堆栈? longjmpsetjmp之间的所有内容?不见了。过时了。永远失去

现在你可以去,等等,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_callklua_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,C,Lua对问题至关重要。 如果你只有两个层,Lua和C,你就不会遇到问题。
  • 想象协程调用应该如何工作--Lua堆栈看起来 某种方式,C堆栈看起来是某种方式,调用yield(longjmp)和 以后恢复...问题不会立即发生立即 恢复。
    当恢复的功能稍后尝试返回时,问题就发生了 C功能。
    因为,为了协同程序的语义,它应该返回 进入一个C函数调用,但堆栈帧已经消失了,不可能 恢复。
  • 缺少恢复这些堆栈帧的能力的解决方法是 使用lua_callklua_pcallk,您可以提供替代品 可以调用的函数代替帧的C函数 消灭了。
  • 关于return lua_yieldk(...)的问题似乎与此无关 任何一个。从略读lua_yieldk的实施情况看来 它确实总是longjmp,它可能只会在一些模糊的情况下返回 涉及lua调试钩子(?)。
  • Lua内部(当前版本)跟踪产量不应该是什么时候 允许,通过保持计数器变量nny(数字不可屈服)相关联 到lua州,当你从C api拨打lua_calllua_pcall时 函数(您之前推送到lua的lua_CFunction),nny是 递增,并且仅在该调用或pcall返回时递减。什么时候 nny非零,产量不安全,如果您尝试屈服,则会出现此yield across C-api boundary错误。

这是一个产生问题并报告错误的简单列表, 如果你像我一样喜欢有具体的代码示例。它证明了 使用lua_calllua_pcalllua_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! :-)