当前位置:  开发笔记 > 编程语言 > 正文

Lua coroutines - setjmp longjmp clobbering?

如何解决《Luacoroutines-setjmplongjmpclobbering?》经验,为你挑选了1个好方法。

在不久前的博客文章中,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指出的那样,抛出longjmpC堆栈的一部分,如果你希望稍后恢复堆栈,那就太糟糕了.

在@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错误时它看起来像什么.我的想法,有可能是问题的关联setjmplongjmp折腾堆栈帧,我们以后需要的,但我想看到一些真正的LUA/LUA C API代码,我可以指出,并说:"例如,不这样做" ,这是令人惊讶的难以捉摸的.

我发现这个电子邮件,有人用一些lua 5.1代码报告了这个错误,我试图在lua 5.3中重现它.然而,我发现,这看起来只是来自lua实现的错误报告 - 实际的错误是由于用户没有正确设置他们的协同程序.加载协同程序的正确方法是,创建线程,将函数推送到线程堆栈,然后调用lua_resume线程状态.相反,用户正在使用dofile线程堆栈,它在加载后执行函数,而不是恢复它.所以它实际上是yield outside of a coroutineiiuc,当我修补它时,他的代码工作正常,使用lua_yieldlua_yieldklua 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堆栈?longjmpsetjmp?之间的一切.不见了.过时了.永远失去.

现在你可以去,"等等,Lua堆栈不知道它进入C并回到Lua"吗?一点点.但Lua堆栈无法执行C无法执行的操作.并且C根本不能保留堆栈(好吧,不是没有特殊的库).因此,虽然Lua堆栈模糊地意识到在堆栈中间发生了某种C进程,但它无法重构那里的内容.

那么如果你恢复这个yielded协程会发生什么呢?

鼻子恶魔.没有人喜欢那些.幸运的是,无论何时尝试跨越C,Lua 5.1及以上(至少)都会出错.

请注意,Lua 5.2+ 确实有办法解决这个问题.但这不是自动的; 它需要你的明确编码.

当协同程序中的Lua代码调用您的C代码,并且您的C代码调用可能产生的Lua代码时,您可以使用lua_callklua_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++堆栈.



1> Nicol Bolas..:

想想当一个协程做了什么时会发生什么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堆栈?longjmpsetjmp?之间的一切.不见了.过时了.永远失去.

现在你可以去,"等等,Lua堆栈不知道它进入C并回到Lua"吗?一点点.但Lua堆栈无法执行C无法执行的操作.并且C根本不能保留堆栈(好吧,不是没有特殊的库).因此,虽然Lua堆栈模糊地意识到在堆栈中间发生了某种C进程,但它无法重构那里的内容.

那么如果你恢复这个yielded协程会发生什么呢?

鼻子恶魔.没有人喜欢那些.幸运的是,无论何时尝试跨越C,Lua 5.1及以上(至少)都会出错.

请注意,Lua 5.2+ 确实有办法解决这个问题.但这不是自动的; 它需要你的明确编码.

当协同程序中的Lua代码调用您的C代码,并且您的C代码调用可能产生的Lua代码时,您可以使用lua_callklua_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++堆栈.

推荐阅读
保佑欣疼你的芯疼
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有