Я люблю Lua. I love Lua.

Обработка ошибок Lua в C++

Итак, случилось страшное: при вызове из С(С++) в Lua скрипте произошла ошибка. Как узнать все подробности произошедшего? Исходники возьмем из Шаг10.

Для начала сделаем ошибку в скрипте(scripts/button1.lua):

function outerFunction4()
  fun1()
end

function fun1()
  fun2()
end  

function fun2()
  fun3()
end 

function fun3()
  -- ОШИБКА!!!
  local x = y.z
end

В классе GlobalLuaState создадим метод void outerFunction4() который (не)просто вызовет функцию outerFunction4() из скрипта.

Сначала сделаем вызов lua_call() и посмотрим, что нам на это скажет Lua:

void GlobalLuaState::outerFunction4()
{
 // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction4"); // stack: outerFunction4

  lua_call(L_, 0, 0);
}

Также изменим обработчик для Button3:

long DialogTester::onButton3(FXObject*,FXSelector,void*){
  printf("Button3 clicked!\n");

  globalLuaState->outerFunction4();

  return 1;
  }

И результат будет такой:

Сообщение об ошибке указывает на источник проблем, но у такого вызова lua_call() есть неприятный побочный эффект — если в вызове происходит ошибка, то Lua вызывает функцию exit(). Обычно это нежелательное поведение. Кроме того, хотелось бы узнать, откуда произошел этот вызов, благо Lua предоставляет такую возможность. Давайте вставим в скрипт печать стека Lua:

function fun3()
  print(debug.traceback())
  -- ОШИБКА!!!
  local x = y.z
end

Запускаем:

Стек есть! И ошибка есть. И завершение программы есть. Нехорошо. Попробуем исправить. Вместо lua_call() воспользуемся lua_pcall() и напечатаем стек из С++:

void GlobalLuaState::outerFunction4()
{
 // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction4"); // stack: outerFunction4

  if(lua_pcall(L_, 0, 0, 0))
  {
    // stack: err
    const char* err = lua_tostring(L_, -1);

    printf("Error:%s\n", err);

    lua_getglobal(L_, "debug"); // stack: err debug
    lua_getfield(L_, -1, "traceback"); // stack: err debug debug.traceback

    // debug.traceback() возвращает 1 значение
    if(lua_pcall(L_, 0, 1, 0))
    {
      const char* err = lua_tostring(L_, -1);

      printf("Error in debug.traceback() call: %s\n", err);
    }
    else
    {
      const char* stackTrace = lua_tostring(L_, -1);

      printf("C++ stack traceback: %s\n", stackTrace);
    }
  }
}

Запускаем:

Что-то С++ не спешит поделиться с нами содержимым стека Lua, при том, что из Lua стек отлично виден. Идем в документацию Lua:

If errfunc is 0, then the error message returned on the stack is exactly the original error message. Otherwise, errfunc is the stack index of an error handler function. (In the current implementation, this index cannot be a pseudo-index.) In case of runtime errors, this function will be called with the error message and its return value will be the message returned on the stack by lua_pcall.

Typically, the error handler function is used to add more debug information to the error message, such as a stack traceback. Such information cannot be gathered after the return of lua_pcall, since by then the stack has unwound.

Ага, понятно, мы не можем получить информацию о стеке после вызова lua_pcall(), поскольку к этому моменту стек уже раскручен. Информацию о стеке мы можем получить в специальной функции — обработчике ошибок. Делаем обработчик ошибок:

namespace
{
int errorHandler(lua_State* L)
{
  //stack: err
  const char* err = lua_tostring(L, 1);

  printf("Error:%s\n", err);

  lua_getglobal(L, "debug"); // stack: err debug
  lua_getfield(L, -1, "traceback"); // stack: err debug debug.traceback

  // debug.traceback() возвращает 1 значение
  if(lua_pcall(L, 0, 1, 0))
  {
    const char* err = lua_tostring(L, -1);

    printf("Error in debug.traceback() call: %s\n", err);
  }
  else
  {
    const char* stackTrace = lua_tostring(L, -1);

    printf("C++ stack traceback: %s\n", stackTrace);
  }

  return 1;
}
}

Теперь обработчик нужно как-то поместить в стек. Если его просто поместить на вершину стека, то при вызове lua_pcall() будет вызван этот обработчик, вместо ожидаемой функции outerFunction4(). Значит, его нужно поместить в стек перед функцией outerFunction4().

void GlobalLuaState::outerFunction4()
{
 // stack:
  LuaStackGuard stackGuard(L_);

  lua_pushcfunction(L_, errorHandler); // stack: errorHandler
  lua_getglobal(L_, "outerFunction4"); // stack: errorHandler outerFunction4

  // вся обработка ошибок переехала в errorHandler
  lua_pcall(L_, 0, 0, -2);
}

Запускаем:

Замечательно, теперь мы имеем исчерпывающую информацию об ошибке — что, где, когда.

Небольшое замечание. Ежели повнимательнее посмотреть на код вызовов

void GlobalLuaState::outerFunction1()
{
  // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction1"); // stack: outerFunction1

  if(lua_pcall(L_, 0, 0, 0)) // stack:
  {
    string err(lua_tostring(L_, -1));

    lua_close(L_);

    throw Exception(err);
  }

  printf("GlobalLuaState::outerFunction1() called!\n");
}

int GlobalLuaState::outerFunction2()
{
  // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction2"); // stack: outerFunction2

  if(lua_pcall(L_, 0, 1, 0)) // stack: число
  {
    string err(lua_tostring(L_, -1));

    lua_close(L_);

    throw Exception(err);
  }

  const int result = (int)lua_tonumber(L_, 1);

  printf("GlobalLuaState::outerFunction2() called! Result %d\n", result);

  return result;

  // после выхода из функции стек Lua должен быть в том же состоянии, 
  // что и до входа в функцию.
  // stackGuard сам все подчистит за нами
}

std::string GlobalLuaState::outerFunction3()
{
 // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction3"); // stack: outerFunction3

  if(lua_pcall(L_, 0, 1, 0)) // stack: строка
  {
    string err(lua_tostring(L_, -1));

    lua_close(L_);

    throw Exception(err);
  }

  string result;

  // функция может вернуть nil
  if(const char* s = lua_tostring(L_, 1))
  {
    result = s;
  }

  printf("GlobalLuaState::outerFunction3() called! Result %s\n", result.c_str());

  return result;

  // после выхода из функции стек Lua должен быть в том же состоянии, 
  // что и до входа в функцию.
  // stackGuard сам все подчистит за нами
}

то станет заметно, что код здесь неоднократно повторяется(да, писал его я) . Если мы хотим(а мы должны хотеть!) вставить в эти вызовы обработку ошибок, то нам придется все время помнить о том, что перед каждым вызовом lua_pcall() нужно поместить в стек обработчик ошибок. Давайте займемся небольшим рефакторингом и вынесем вызов lua_pcall() в отдельную функцию:

void GlobalLuaState::callLuaFunc_(int args, int results)
{
  // stack: func arg1 arg2 ... argn
  lua_pushcfunction(L_, errorHandler); // stack: func arg1 arg2 ... argn errorHandler 

  const int errorHandlerIndex = -(args + 2);

  lua_pushvalue(L_, errorHandlerIndex); // stack: errorHandler func arg1 arg2 ... argn

  if(lua_pcall(L_, args, results, errorHandlerIndex))
  {
    const char* err = lua_tostring(L_, -1);

    printf("%s\n", err);

    throw(Exception(err));
  }
}

Также немного усовершенствуем обработчик ошибок:

namespace
{
int errorHandler(lua_State* L)
{
  //stack: err
  lua_getglobal(L, "debug"); // stack: err debug
  lua_getfield(L, -1, "traceback"); // stack: err debug debug.traceback

  // debug.traceback() возвращает 1 значение
  if(lua_pcall(L, 0, 1, 0))
  {
    const char* err = lua_tostring(L, -1);

    printf("Error in debug.traceback() call: %s\n", err);
  }
  else
  {
    // stack: err debug stackTrace
    lua_insert(L, -2); // stack: err stackTrace debug
    lua_pop(L, 1); // stack: err stackTrace
    lua_pushstring(L, "Error:"); // stack: err stackTrace "Error:"
    lua_insert(L, -3); // stack: "Error:" err stackTrace  
    lua_pushstring(L, "\n"); // stack: "Error:" err stackTrace "\n"
    lua_insert(L, -2); // stack: "Error:" err "\n" stackTrace
    lua_concat(L, 4); // stack: "Error:"+err+"\n"+stackTrace
  }

  return 1;
}
}

Функции вызова примут такой вид опрятный вид:

void GlobalLuaState::outerFunction1()
{
  // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction1"); // stack: outerFunction1

  callLuaFunc_(0,0);

  printf("GlobalLuaState::outerFunction1() called!\n");
}

int GlobalLuaState::outerFunction2()
{
  // stack:
  LuaStackGuard stackGuard(L_);

  lua_getglobal(L_, "outerFunction2"); // stack: outerFunction2

  callLuaFunc_(0, 1);

  const int result = (int)lua_tonumber(L_, 1);

  printf("GlobalLuaState::outerFunction2() called! Result %d\n", result);

  return result;

  // после выхода из функции стек Lua должен быть в том же состоянии, 
  // что и до входа в функцию.
  // stackGuard сам все подчистит за нами
}

std::string GlobalLuaState::outerFunction3()
{
 // stack:
  LuaStackGuard stackGuard(L_);
  string result;

  lua_getglobal(L_, "outerFunction3"); // stack: outerFunction3

  callLuaFunc_(0, 1);

  // функция может вернуть nil
  if(const char* s = lua_tostring(L_, 1))
  {
    result = s;
  }

  printf("GlobalLuaState::outerFunction3() called! Result %s\n", result.c_str());

  return result;

  // после выхода из функции стек Lua должен быть в том же состоянии, 
  // что и до входа в функцию.
  // stackGuard сам все подчистит за нами
}

Результат нажатия всех трех кнопок подряд:

Ну и сообщение о не перехваченном исключении, разумеется(а потому что я его не перехватил!):

На этой оптимистичной ноте позвольте откланяться.

Реклама

комментариев 6

Subscribe to comments with RSS.

  1. nordaux said, on Июль 19, 2012 at 12:33 дп

    К сожалению исходники на narod.ru потерялись. Не могли бы вы выложить их в месте понадежней ? Заранее, спасибо.

  2. ilovelua said, on Июль 19, 2012 at 5:20 дп

    «Э… Исходники, кажется потерялись совсем. Если найду свободную минутку, то изготовлю их снова, а пока копи-паст Вам в помощь 😦

  3. Alexey said, on Май 23, 2013 at 5:22 дп

    В функции GlobalLuaState::callLuaFunc_ необходимо вместо lua_pushvalue использовать lua_insert.
    При использовании lua_pushvalue работает только при пустом списке аргументов для функций.

  4. ilovelua said, on Май 23, 2013 at 5:40 дп

    Спасибо за комментарий.

    Насколько я вижу, при вычислении индекса функции перехватчика ошибок учитывается количество аргументов функции

    const int errorHandlerIndex = -(args + 2);

    В чем я мог ошибиться, так в том, что lua_pushvalue() копирует значение с вершины стека в нужную позицию, таким образом, на вершине стека останется функция перехватчика:

    void GlobalLuaState::callLuaFunc_(int args, int results)
    {
    // stack: func arg1 arg2 … argn
    lua_pushcfunction(L_, errorHandler); // stack: func arg1 arg2 … argn errorHandler

    const int errorHandlerIndex = -(args + 2);

    lua_pushvalue(L_, errorHandlerIndex); // stack: errorHandler func arg1 arg2 … argn errorHandler

    И нужно будет его вытолкнуть со стека:

    lua_pop(L, 1); // stack: errorHandler func arg1 arg2 … argn

    Ну, это, так сказать, мои умозаключения. Код я не запускал, сейчас у меня на это, к сожалению, маловато времени. Может быть потом, в свободную минутку.

    • Alexey said, on Май 23, 2013 at 6:44 дп

      > В чем я мог ошибиться, так в том, что lua_pushvalue() копирует значение с вершины стека в нужную позицию, таким образом, на вершине стека останется функция перехватчика

      Наоборот, lua_pushvalue из указанной позиции стека копирует на вершину стека.
      А lua_insert берет значение с вершины стека и помещает его в указанную позицию, сдвигая элементы вверх.
      см. 24.2.3 – Other Stack Operations: http://www.lua.org/pil/24.2.3.html

      В вашем варианте получится вот что:
      lua_pushcfunction(L_, errorHandler); // stack: func arg1 arg2 … argn errorHandler
      lua_pushvalue(L_, errorHandlerIndex); // stack: func arg1 arg2 … argn errorHandler func

      при использовании lua_insert на стеке все будет правильно:
      lua_pushcfunction(L_, errorHandler); // stack: func arg1 arg2 … argn errorHandler
      lua_insert(L_, errorHandlerIndex); // stack: errorHandler func arg1 arg2 … argn

      Кроме того, необходимо вручную удалять со стека функцию errorHandler (при этом надо учитывать, что выше нее находятся N возвращаемых значений либо, если произошла ошибка, то одно строковое значение):
      int res = lua_pcall(L_, args, results, errorHandlerIndex)
      if( res )
      {
      results = 1; // if error, only one result is placed on stack (instead of N results)
      }
      lua_remove( L, -results-1 ); // remove function errorHandler from stack

      Таким образом, функция примет следующий вид:
      =========================================
      void GlobalLuaState::callLuaFunc_(int args, int results)
      {
      // stack: func arg1 arg2 … argn
      lua_pushcfunction(L_, errorHandler); // stack: func arg1 arg2 … argn errorHandler

      const int errorHandlerIndex = -(args + 2);

      lua_insert(L_, errorHandlerIndex); // stack: errorHandler func arg1 arg2 … argn

      int res = lua_pcall(L_, args, results, errorHandlerIndex)
      if( res )
      {
      results = 1; // if error, only one result is placed on stack (instead of N results)
      }
      lua_remove( L, -results-1 ); // remove function errorHandler from stack

      if( res )
      {
      const char* err = lua_tostring(L_, -1);

      printf(«%s\n», err);

      throw(Exception(err));
      }
      }

  5. ilovelua said, on Май 23, 2013 at 7:16 дп

    Чтобы заменить мой код Вашим мне нужно будет все проверить. Спасибо! 🙂


Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: