Lua coroutines setjmp longjmp clobbering?

В не так давно сообщении в блоге Скотт Воукс описывает техническую проблему, связанную с 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! :-)

person Chris Beck    schedule 16.12.2015    source источник
comment
AFAIK, вся функция продолжения была исправлением этой проблемы. Если вы используете Lua 5.3 или 5.2, у вас нет проблем.   -  person user253751    schedule 16.12.2015
comment
@immibis: Нет, проблема все еще есть. Просто у вас есть потенциальное решение. Но вряд ли это происходит автоматически.   -  person Nicol Bolas    schedule 16.12.2015
comment
Хорошо, теперь вы задаете совершенно новый вопрос. Изначально вы спрашивали о том, о чем говорилось в этом посте о прыжках через стек. Теперь вы спрашиваете, как Lua 5.2 / 3 решает эту проблему. Это хороший вопрос, но это новый вопрос, который следует задать с помощью кнопки «Задать вопрос».   -  person Nicol Bolas    schedule 18.12.2015
comment
@NicolBolas: Итак, я понимаю, откуда вы взялись, но выслушайте меня. Когда программист задает вопрос о какой-то подобной проблеме, обычно он хочет знать, что на высоком уровне является проблемой, каковы некоторые примеры того, как это может случиться, что мне нужно делать, когда у меня есть проблема, практически говоря. Например, на этом сайте вы можете увидеть такие вопросы, как «висячий указатель», и на них будут даны очень подробные ответы с примерами кода. Я надеялся на такой ответ для сопрограмм. Мне потребовалось время, чтобы понять, какие именно вопросы мне нужно было задать по ходу дела.   -  person Chris Beck    schedule 18.12.2015
comment
Если вы думаете, что мне нужно все это переписать, чтобы сжать его, это разумно. Если вы думаете, что я должен разделить это на два вопроса, в чем проблема с сопрограммами, прыгающими через стек, и как мне исправить проблему с сопрограммами, прыгающими через стек в некотором примере кода, я имею в виду, что это тоже разумно. Но я думаю, что потенциально полезно иметь эти две части информации в одном месте. Я имею в виду, что обычно в программировании мне нужен высокий уровень, а также то, что это означает практически говоря   -  person Chris Beck    schedule 18.12.2015


Ответы (2)


Подумайте, что происходит, когда сопрограмма выполняет 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. Внутренний стек 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? Все между longjmp и setjmp ?. Ушел. Капут. Потерян навсегда.

Теперь вы можете сказать: «Подождите, разве стек Lua не знает, что он перешел в C и вернулся в Lua»? Немного. Но стек Lua не может делать то, на что не способен C. А C просто не способен сохранять стек (ну не без специальных библиотек). Таким образом, хотя стек Lua смутно осведомлен о том, что в середине его стека произошел какой-то процесс C, у него нет возможности восстановить то, что там было.

Так что же произойдет, если вы возобновите эту сопрограмму с 20 числами?

Носовые демоны. И они никому не нравятся. К счастью, 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, так что при возобновлении сопрограммы она вызывает предоставленную функцию продолжения.

Обратите внимание, что Coco дает Lua 5.1 возможность решить эту проблему. Он способен (с помощью магии ОС / сборки / и т. Д.) сохранить стек C во время операции yield. Версии LuaJIT до 2.0 также предоставляли эту функцию.


Примечание C ++

Вы отметили свой вопрос тегом C ++, поэтому я предполагаю, что он здесь задействован.

Среди множества различий между C и C ++ является тот факт, что C ++ намного более зависим от природы своего стека вызовов, чем Lua. В C, если вы сбросите стек, вы можете потерять ресурсы, которые не были очищены. Однако C ++ требуется для вызова деструкторов функций, объявленных в стеке, в какой-то момент. Стандарт не позволяет их просто выбросить.

Таким образом, продолжения работают только в C ++, если в стеке нет ничего, для которого требуется вызов деструктора. Или, более конкретно, в стеке могут находиться только тривиально разрушаемые типы, если вы вызываете любой из Lua API-интерфейсов функции продолжения.

Конечно, Coco отлично справляется с C ++, поскольку фактически сохраняет стек C ++.

person Nicol Bolas    schedule 16.12.2015
comment
Две вещи: Lua 5.1 также вызывает ошибку, когда вы пытаетесь перейти через границу метаметода / C-вызова, и текущий LuaJIT (2.0 / 2.1beta) имеет аналогичную проблему (хотя вы можете уступить из метаметодов и итераторов цикла). Coco использовался в LuaJIT 1.x. - person siffiejoe; 17.12.2015
comment
@NicolBolas: Это мелочь, но я думаю, что на диаграммах нужно поменять местами верхнюю и нижнюю линии, не так ли? Так что вершина рядом с пределом текучести - person Chris Beck; 18.12.2015
comment
@ChrisBeck: точка выхода представляет собой место, куда вернется сопрограмма. Стек растет снизу вверх. - person Nicol Bolas; 18.12.2015
comment
Значит, Lua использует буфер статического перехода или что-то в этом роде? C setjmp() позволяет вам поддерживать несколько целей перехода одновременно, и, насколько я понимаю проблему, похоже, что ее можно решить с помощью Lua, поддерживающего отдельный буфер перехода для каждого вызова из C в Lua. Кроме того, я думаю, что это могло бы вполне естественно выпасть из стека вызовов C. - person John Bollinger; 18.12.2015
comment
@JohnBollinger: Lua мог бы это сделать, но это не сработало бы правильно. Или, по крайней мере, не совсем по отношению к тому, что хочет Lua. coroutine.yield должен вернуться к коду, который вызвал resume вызов. То, что вы предлагаете, заставило бы yield вернуться к resume вызову или самой последней функции C, вызвавшей Lua. Это просто неправильное поведение. - person Nicol Bolas; 18.12.2015

Публикация этого как ответа, который дополняет ответ @Nicol Bolas, и чтобы у меня было место, чтобы записать, что мне потребовалось, чтобы понять исходный вопрос, и ответы на второстепенные вопросы / листинг кода.

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

  • Три уровня в стеке вызовов, Lua, C, Lua, имеют важное значение для решения проблемы. Если у вас всего два слоя, Lua и C, вы не получите проблемы.
  • Представляя, как должен работать вызов сопрограммы - стек lua ​​выглядит определенным образом, стек C выглядит определенным образом, вызов завершается (longjmp), а затем возобновляется ... проблема не возникает немедленно при возобновлении.
    Проблема возникает, когда возобновленная функция позже пытается вернуться к вашей функции C.
    Потому что для работы семантики сопрограмм необходимо должен вернуться в вызов функции C, но кадры стека для этого исчезли и не могут быть восстановлены.
  • Обходной путь для этого отсутствия возможности восстановить эти кадры стека заключается в использовании lua_callk, lua_pcallk, которые позволяют предоставить заменяющую функцию, которая может быть вызвана вместо той функции C, кадры которой были стерты.
  • Проблема с return lua_yieldk(...), похоже, не имеет к этому никакого отношения. Из беглого просмотра реализации lua_yieldk кажется, что он действительно всегда longjmp, и он может возвращаться только в каком-то неясном случае, связанном с отладочными хуками lua (?).
  • Lua внутренне (в текущей версии) отслеживает, когда yield не должен быть разрешен, путем сохранения переменной счетчика nny (число невыполнимых), связанной с состоянием lua, и когда вы вызываете lua_call или lua_pcall из функции C api (lua_CFunction который вы ранее отправили в lua), 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! :-)
person Chris Beck    schedule 18.12.2015
comment
Проблема return lua_yieldk(...) - это остаток Lua 5.1, который не использовал longjmp для уступки, а вместо этого использовал специальное возвращаемое значение. Теперь (с Lua 5.2+) он действует как напоминание о том, что выполнение текущей функции C заканчивается, когда вызывается lua_yieldk. - person siffiejoe; 18.12.2015