Lua сопрограммы — setjmp longjmp забивает?

В Сообщение блога не так давно Скотт Вокес описывает техническую проблему, связанную с реализацией сопрограмм lua с использованием функций Си setjmp а также longjmp:

Основное ограничение сопрограмм Lua состоит в том, что, поскольку они реализованы с помощью setjmp (3) и longjmp (3), их нельзя использовать для вызова из Lua в C-код, который вызывает обратный вызов в Lua и обратный вызов в C, потому что вложенный longjmp закроет стековые фреймы функции C. (Это обнаруживается во время выполнения, а не происходит молча.)

Я не нашел, чтобы это было проблемой на практике, и я не знаю ни одного способа исправить это, не повредив переносимость Lua, одну из моих любимых вещей в Lua — она ​​будет работать буквально на всем с компилятором ANSI C и скромное количество места. Использование Lua означает, что я могу путешествовать налегке. 🙂

Я использовал сопрограммы изрядное количество, и я думал, что я понял в общих чертах, что происходит и что setjmp а также longjmp но я прочитал это в какой-то момент и понял, что не совсем понял. Чтобы попытаться понять это, я попытался создать программу, которая, как мне казалось, должна вызывать проблему, основанную на описании, и вместо этого она, кажется, работает нормально.

Однако есть еще несколько мест, которые, как я видел, люди утверждают, что есть проблемы:

Вопрос в том:

  • При каких обстоятельствах lua сопрограммы не работают из-за засорения фреймов стека C-функций?
  • Что именно является результатом? Означает ли «обнаружен во время выполнения», lua паника? Или что-то другое?
  • Влияет ли это на самые последние версии 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 за очень подробный ответ!
Прочитав его ответ, прочитав официальные документы, прочитав несколько писем и поиграв с ними еще раз, я хочу уточнить вопрос / задать конкретный дополнительный вопрос, однако вы хотите посмотреть на него.

Я думаю, что термин «забивание» не подходит для описания этой проблемы, и это было частью того, что смутило меня — ничто не «забито» в том смысле, что записано дважды, а первое значение потеряно, проблема исключительно в как отмечает @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! :-)

8

Решение

Подумайте о том, что происходит, когда сопрограмма 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 ++.

9

Другие решения

Размещать это как ответ, который дополняет ответ @Nicol Bolas, и чтобы
У меня может быть место, чтобы записать, что мне понадобилось, чтобы понять оригинал
вопрос, и ответы на второстепенные вопросы / листинг кода.

Если вы прочитали ответ Николь Болас, но у вас остались вопросы, как я, вот
некоторые дополнительные советы:

  • три уровни в стеке вызовов Lua, C, Lua являются существенными для решения проблемы.
    Если у вас есть только два слоя, Lua и C, вы не получите проблемы.
  • Вообразив, как должен работать вызов сопрограммы — выглядит стек lua
    определенным образом, стек C выглядит определенным образом, вызов приводит к (longjmp) и
    позже возобновляется … проблемы не бывает немедленно когда он является
    возобновлено.
    Проблема возникает, когда возобновленная функция позже пытается вернуться к вашему
    С функция.

    Потому что, чтобы семантика сопрограмм сработала, она должна вернуться
    в вызов функции C, но кадры стека для этого ушли, и не может быть
    восстановлены.
  • Обходной путь для этот Отсутствие возможности восстановить эти кадры стека
    использование lua_callk, lua_pcallk, которые позволяют вам заменить
    функция, которая может быть вызвана вместо той функции C, чьи кадры были
    уничтожен.
  • Вопрос о return lua_yieldk(...) кажется, не имеет ничего общего с
    любой из этого. Скимминг реализации lua_yieldk похоже, что
    это действительно всегда longjmpи может вернуться только в каком-то непонятном случае
    с использованием крючков отладки lua (?).
  • Lua внутренне (в текущей версии) отслеживает, когда yield не должен быть
    разрешено, сохраняя переменную счетчика nny (число не подлежит возврату) связано
    в состояние Луа, и когда вы звоните lua_call или же lua_pcall из C api
    функция (а lua_CFunction который вы ранее подтолкнули к Луа), nny является
    увеличивается и уменьшается только при возврате этого вызова или pcall. когда
    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! :-)
1

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector