В не так давно сообщении в блоге Скотт Воукс описывает техническую проблему, связанную с lua реализация сопрограмм с использованием функций C setjmp
и longjmp
:
Основное ограничение сопрограмм Lua заключается в том, что, поскольку они реализованы с помощью setjmp (3) и longjmp (3), вы не можете использовать их для вызова из Lua в код C, который обращается обратно в Lua, который вызывает обратно в C, потому что вложенный longjmp будет затирать фреймы стека C-функции. (Это обнаруживается во время выполнения, а не происходит тихо.)
Я не обнаружил, что это проблема на практике, и я не знаю никакого способа исправить это, не повредив переносимости Lua, одной из моих любимых вещей в Lua - он будет работать буквально на чем угодно с компилятором ANSI C и скромное количество места. Использование Lua означает, что я могу путешествовать налегке. :)
Я довольно часто использовал сопрограммы, и мне казалось, что я в общих чертах понимаю, что происходит и что делают setjmp
и longjmp
, однако в какой-то момент я прочитал это и понял, что на самом деле не понимаю этого. Чтобы попытаться разобраться в этом, я попытался создать программу, которая, как я думал, должна вызывать проблему на основе описания, и вместо этого, похоже, она работает нормально.
Однако есть еще несколько мест, где я видел, как люди утверждают, что есть проблемы:
Вопрос в том:
- При каких обстоятельствах сопрограммы lua не работают из-за засорения фреймов стека функций C?
- Какой именно результат? Означает ли "обнаружено во время выполнения" lua panic? Или что-то другое?
- Это все еще влияет на самые последние версии lua (5.3) или это действительно проблема 5.1 или что-то в этом роде?
Вот код, который я произвел. В моем тесте он связан с lua 5.3.1, скомпилирован как код C, а сам тест скомпилирован как код C ++ в стандарте C ++ 11.
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 за очень подробный ответ!
Прочитав его ответ, прочитав официальную документацию, прочитав несколько электронных писем и еще немного поигравшись с ним, я хочу уточнить вопрос / задать конкретный дополнительный вопрос , однако вы хотите на это посмотреть.
Я думаю, что этот термин «затирание» не подходит для описания этой проблемы, и это было частью того, что меня смутило - ничто не «затирается» в том смысле, что оно записывается дважды и первое значение теряется, проблема исключительно в том, как отмечает @Nicol Bolas, это longjmp
отбрасывает часть стека C, и если вы надеетесь восстановить стек позже, это очень плохо.
Проблема на самом деле очень хорошо описана в разделе 4.7 руководства по lua 5.2 по ссылке, предоставленной @ Никол Болас.
Любопытно, что в документации по 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
. У меня возникла идея, что может быть проблема, связанная с setjmp
и longjmp
подбрасыванием фреймов стека, которые нам понадобятся позже, но я хочу увидеть какой-то реальный код lua / lua c api, на который я могу указать и сказать «например, не делайте это ", и это на удивление неуловимо.
Я нашел это письмо, в котором кто-то сообщил об этой ошибке с некоторым кодом lua 5.1, и Я попытался воспроизвести его в lua 5.3. Однако я обнаружил, что это похоже на плохое сообщение об ошибках из реализации lua - фактическая ошибка возникает из-за того, что пользователь неправильно настраивает свою сопрограмму. Правильный способ загрузки сопрограммы - создать поток, поместить функцию в стек потока, а затем вызвать lua_resume
для состояния потока. Вместо этого пользователь использовал dofile
в стеке потоков, который выполняет функцию там после загрузки, а не возобновляет ее. Таким образом, это фактически yield outside of a coroutine
iiuc, и когда я исправляю это, его код работает нормально, используя как lua_yield
, так и lua_yieldk
в lua 5.3.
Вот список, который я подготовил:
#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! :-)