В Сообщение блога не так давно Скотт Вокес описывает техническую проблему, связанную с реализацией сопрограмм lua с использованием функций Си setjmp
а также longjmp
:
Основное ограничение сопрограмм Lua состоит в том, что, поскольку они реализованы с помощью setjmp (3) и longjmp (3), их нельзя использовать для вызова из Lua в C-код, который вызывает обратный вызов в Lua и обратный вызов в C, потому что вложенный longjmp закроет стековые фреймы функции C. (Это обнаруживается во время выполнения, а не происходит молча.)
Я не нашел, чтобы это было проблемой на практике, и я не знаю ни одного способа исправить это, не повредив переносимость Lua, одну из моих любимых вещей в Lua — она будет работать буквально на всем с компилятором ANSI C и скромное количество места. Использование Lua означает, что я могу путешествовать налегке. 🙂
Я использовал сопрограммы изрядное количество, и я думал, что я понял в общих чертах, что происходит и что setjmp
а также longjmp
но я прочитал это в какой-то момент и понял, что не совсем понял. Чтобы попытаться понять это, я попытался создать программу, которая, как мне казалось, должна вызывать проблему, основанную на описании, и вместо этого она, кажется, работает нормально.
Однако есть еще несколько мест, которые, как я видел, люди утверждают, что есть проблемы:
Вопрос в том:
Вот код, который я произвел. В моем тесте это связано с 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 за очень подробный ответ!
Прочитав его ответ, прочитав официальные документы, прочитав несколько писем и поиграв с ними еще раз, я хочу уточнить вопрос / задать конкретный дополнительный вопрос, однако вы хотите посмотреть на него.
Я думаю, что термин «забивание» не подходит для описания этой проблемы, и это было частью того, что смутило меня — ничто не «забито» в том смысле, что записано дважды, а первое значение потеряно, проблема исключительно в как отмечает @Nolol Bolas, что longjmp
отбрасывает часть стека C, и, если вы надеетесь восстановить этот стек позже, тоже неплохо.
Проблема на самом деле очень хорошо описана в раздел 4.7 руководства lua 5.2, в ссылке, предоставленной @Nicol Bolas.
Любопытно, что в документации по lua 5.1 нет эквивалентного раздела. Тем не менее, Луа 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
который был передан Луа, все еще возможно вызвать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
в Луа 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! :-)
Подумайте о том, что происходит, когда сопрограмма 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
, И есть стек С. Внутренний стек Lua, в то время, когда yield
собирается позвонить, выглядит примерно так:
-- top
-- Some C stuff
-- bottom
-- yield point
Так как же выглядит стек на C? Ну, это выглядит так:
-- arbitrary Lua interpreter stuff
-- middle
-- arbitrary Lua interpreter stuff
-- setjmp
И вот тут-то и есть проблема. Видишь, когда Луа делает yield
позвонит longjmp
, Эта функция основана на поведении стека C. А именно, он собирается вернуться туда, где setjmp
было.
Стек Lua будет сохранен, потому что стек Lua отдельный из стека C. Но стек C? Все между longjmp
а также setjmp
?. Ушел. Капут. Потерял навсегда.
Теперь вы можете сказать: «Подожди, разве стек Lua не знает, что он вошел в C и обратно в Lua»? Немного. Но стек Lua не способен делать то, на что не способен C. А C просто не способен сохранять стек (ну, не без специальных библиотек). Поэтому, хотя стек Lua смутно осознает, что какой-то процесс C произошел в середине его стека, он никак не может воссоздать то, что было там.
Так что же произойдет, если вы возобновите это yield
Эд сопрограмма?
Носовые демоны. И никто не любит их. К счастью, Lua 5.1 и выше (по крайней мере) будет выдавать ошибку всякий раз, когда вы пытаетесь уступить через C.
Обратите внимание, что Lua 5.2+ есть способы исправить это. Но это не автоматически; это требует явного кодирования с вашей стороны.
Когда код Lua в сопрограмме вызывает ваш код C, а код C вызывает код Lua, который может дать, вы можете использовать lua_callk
или же lua_pcallk
для вызова возможно приносящих Lua функций. Эти вызывающие функции принимают дополнительный параметр: функцию «продолжения».
Если код Lua, который вы называете, дает результат, то lua_*callk
функция никогда не вернется (поскольку ваш стек C будет уничтожен). Вместо этого он вызовет функцию продолжения, которую вы указали в lua_*callk
функция. Как вы можете догадаться по названию, работа функции продолжения заключается в том, чтобы продолжить работу с того места, где остановилась предыдущая функция.
Теперь Lua сохраняет стек для вашей функции продолжения, поэтому он получает стек в том же состоянии, в котором находилась ваша исходная функция C. Хорошо, за исключением того, что функция + аргументы, которые вы вызвали (с помощью lua_*callk
) удаляются, а возвращаемые значения из этой функции помещаются в ваш стек. Помимо этого, стек все тот же.
Существует также lua_yieldk
, Это позволяет вашей C-функции возвращаться к Lua, так что, когда сопрограмма возобновляется, она вызывает предоставленную функцию продолжения.
Обратите внимание, что кокос дает Lua 5.1 возможность решить эту проблему. Он способен (хотя ОС / сборка / и т. Д. Магия) на сохранение стек C во время операции выхода. Версии LuaJIT до 2.0 также предоставили эту функцию.
C ++ note
Вы пометили свой вопрос тегом C ++, так что я предполагаю, что он здесь задействован.
Среди многих различий между C и C ++ является тот факт, что C ++ далеко больше зависит от характера своего стека вызовов, чем Lua. В C, если вы отбрасываете стек, вы можете потерять ресурсы, которые не были очищены. C ++, однако, обязан вызывать деструкторы функций, объявленных в стеке в какой-то момент. Стандарт не позволяет просто выбросить их.
Так что продолжения работают только в C ++, если есть ничего такого в стеке, который должен иметь вызов деструктора. Точнее говоря, только те типы, которые являются тривиально разрушаемыми, могут находиться в стеке, если вы вызываете любую из функций продолжения Lua API.
Конечно, Coco прекрасно справляется с C ++, так как на самом деле сохраняет стек C ++.
Размещать это как ответ, который дополняет ответ @Nicol Bolas, и чтобы
У меня может быть место, чтобы записать, что мне понадобилось, чтобы понять оригинал
вопрос, и ответы на второстепенные вопросы / листинг кода.
Если вы прочитали ответ Николь Болас, но у вас остались вопросы, как я, вот
некоторые дополнительные советы:
lua_callk
, lua_pcallk
, которые позволяют вам заменитьreturn lua_yieldk(...)
кажется, не имеет ничего общего сlua_yieldk
похоже, чтоlongjmp
и может вернуться только в каком-то непонятном случаеnny
(число не подлежит возврату) связаноlua_call
или же lua_pcall
из C apilua_CFunction
который вы ранее подтолкнули к Луа), nny
является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! :-)