gpt4 book ai didi

c++ - Lua协程-setjmp longjmp破坏?

转载 作者:可可西里 更新时间:2023-11-01 17:39:02 27 4
gpt4 key购买 nike

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

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. :)



我已经大量使用了协程,并且我认为我大致了解了正在发生的事情以及 setjmplongjmp的工作,但是我在某个时候读了一下,并意识到我并不真正了解它。为了解决这个问题,我尝试根据描述创建一个我认为应该引起问题的程序,它似乎可以正常工作。

但是,我还看到了其他一些地方,人们似乎声称存在问题:
  • http://coco.luajit.org/
  • http://lua-users.org/lists/lua-l/2005-03/msg00179.html

  • 问题是:
  • 在什么情况下lua协程会因为C函数堆栈帧被破坏而无法工作?
  • 结果到底是什么? “在运行时检测到”意味着 panic 吗?或者是其他东西?
  • 这是否仍然会影响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的详细回答!
    在阅读了他的回答,阅读了官方文档,阅读了一些电子邮件并对其进行了更多处理之后,我想细化问题/提出一个具体的后续问题,但是您想看看它。

    我认为“笼统”一词不适用于描述这个问题,这是令我感到困惑的一部分-从写两次并失去第一个值(value)的意义上讲,没有什么被“笼罩”了,这个问题仅仅是,正如@Nicol Bolas指出的那样, longjmp丢掉了一部分C堆栈,如果您希望以后再恢复堆栈,那就太糟糕了。

    实际上,该问题在@Nicol Bolas提供的链接中的 section 4.7 of lua 5.2 manual中得到了很好的描述。

    奇怪的是,lua 5.1文档中没有等效的部分。但是,lua 5.2具有有关 lua_yieldkthis to say:

    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将永远不会返回,因此,如果我返回则没关系吗?所以那不可能是正在发生的事,对吧?
  • 假设lua_yieldk只是在lua状态下做一个记号,说明当前C api调用已声明要屈服,然后在最终返回时,lua会弄清楚接下来会发生什么。然后,这解决了保存C堆栈帧的问题,不是吗?自从我们正常返回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?



    最后,(但这不太重要),我想看一个具体的例子,说明一个天真的程序员“不小心”并触发 attempt to yield across a C-call boundary错误的样子。我的想法是,可能存在与 setjmplongjmp扔掉以后需要的堆栈框架相关的问题,但是我想看到一些我可以指向的真正的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_yieldlua_yieldk

    这是我产生的 list :
    #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的任何人,对吗?

    好吧,假设您有以下代码:

    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的堆栈。在即将调用 yield时,Lua的内部堆栈看起来像这样:
    -- 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进程发生在其堆栈的中间,但它无法重新构造其中的内容。

    那么,如果您恢复此 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函数。这些调用函数带有一个附加参数:“继续”函数。

    如果您调用的Lua代码确实产生了,那么 lua_*callk函数将永远不会真正返回(因为您的C堆栈将被销毁)。相反,它将调用您在 lua_*callk函数中提供的延续函数。顾名思义,延续功能的工作是从上一个功能中断的地方继续。

    现在,Lua确实为您的延续函数保留了栈,因此它使栈处于与原始C函数所处的状态相同。好,除了您调用的函数+参数(使用 lua_*callk)被删除,并返回该函数的值被压入堆栈。除此之外,堆栈都是一样的。

    还有 lua_yieldk。这使您的C函数可以返回Lua,以便在协程恢复后调用提供的延续函数。

    注意 Coco使Lua 5.1能够解决此问题。它有能力(尽管OS/assembly/etc具有魔力)在yield操作期间保留C堆栈。 LuaJIT 2.0之前的版本也提供了此功能。

    C++笔记

    您用C++标记标记了您的问题,所以我假设这里涉及到。

    在C和C++之间的众多差异中,有一个事实是C++比Lua更依赖于其调用堆栈的性质。在C语言中,如果丢弃堆栈,则可能会丢失未清理的资源。但是,在某些时候需要C++调用在堆栈上声明的函数的析构函数。该标准不允许您仅将它们扔掉。

    因此,只有在堆栈上没有 且没有且需要进行析构函数调用的情况下,延续才能在C++中起作用。或更具体地说,如果调用任何延续函数Lua API,则只有可微破坏的类型才能坐在堆栈上。

    当然,Coco可以很好地处理C++,因为它实际上保留了C++堆栈。

    关于c++ - Lua协程-setjmp longjmp破坏?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34303507/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com