Шаг 4

Сегодня мы рассмотрим некоторые утилиты для работы с Lua.
Читая что либо из стека, важно следить за тем, чтобы после окончания чтения стек Lua вернулся в исходное состояние (это вообще хорошая программистская практика подтирать за собой). Например, мы знаем, что есть глобальная таблица table1 и у нее есть поле field. Функция, которая читает значение из этого поля может выглядеть так (плохой вариант):

bool getField(lua_State* L, int& field)
{
  // stack:
  lua_getglobal(L, "table1"); // stack: table1
  if(lua_istable(L, -1))
  {
    lua_getfield(L, -1, "field"); // stack: table1 field
    if(lua_isnumber(L, -1))
    {
      field = (int)lua_tonumber(L, -1);

      return true;
    }
  }

  return false;
}

Что мы здесь видим? После выхода из функции на вершине стека осталось одно или два ненужных значения - это table1 и/или field. Если вызвать эту функцию много раз подряд вполне возможно, что Lua сгенерирует ошибку "переполнение стека" и аварийно завершится. Чтобы этого не произошло, нужно поддерживать стек Lua в равновесном состоянии, то есть при выходе из функции стек должен быть в таком же состоянии, как и при входе в нее. Разумеется, это относится только к функциям, которые не должны изменять стек Lua, то есть функции чтения данных из стека. Функции, вызываемые из Lua и возвращающие в Lua значения должны модифицировать стек, но они об этом прямо говорят, возвращая число аргументов, помещенных в стек). Добавим в нашу функцию пару строк, которые удаляют ненужные данные из стека:

bool getField(lua_State* L, int& field)
{
  // stack:
  bool ret = false;
  lua_getglobal(L, "table1"); // stack: table1
  if(lua_istable(L, -1))
  {
    lua_getfield(L, -1, "field"); // stack: table1 field
    if(lua_isnumber(L, -1))
    {
      field = (int)lua_tonumber(L, -1);
      ret = true;
    }
    lua_pop(L, 1); // stack: table1
  }
  lua_pop(L, 1); // stack:

  return ret;
}

Здесь хорошо видно, что при выходе из функции стек находится в таком же сотстоянии, как и при входе в нее (комментарии типа // stack: в этом сильно помогают, в очередной раз настоятельно призываю их использовать).
Этот пример достаточно простой, чтобы можно было легко отследить количество push-ей и pop-ов. Для проверки, действительно ли стек остался в исходном состоянии, можно добавить в функцию еще несколько строк:

bool getField(lua_State* L, int& field)
{
  // stack:
  bool ret = false;
  int top = lua_gettop(L);
  lua_getglobal(L, "table1"); // stack: table1
  if(lua_istable(L, -1))
  {
    lua_getfield(L, -1, "field"); // stack: table1 field
    if(lua_isnumber(L, -1))
    {
      field = (int)lua_tonumber(L, -1);
      ret = true;
    }
    lua_pop(L, 1); // stack: table1
  }
  lua_pop(L, 1); // stack:

  if(0 != top - lua_gettop(L))
  {
    printf("Lua stack is changed!\n");
  }

  return ret;
}

Идея ясна: при входе в функцию мы запоминаем число элементов в стеке, а при выходе из нее сравниваем текущее число элементов с запомненным ранее. Если эти значения не совпадают, то генерируется сообщение об ошибке.
Отметим следующее обстоятельство - проверки стека поизводятся в самом начале и в самом конце функции. А в С++ как раз есть одна методика, которая позволяет удобно организовать вызов двух функций - одну при входе в область видимости (тело функции) и одну при выходе из нее. Это конструктор и деструктор объекта класса. Оформим проверку стека в виде класса. Я назвал его LuaStackChecker. Файл LuaStackChecker.h:

#pragma once

struct lua_State;
class LuaStackChecker
{
public:
  LuaStackChecker(lua_State* L, const char* filename = "", int line = 0);
  ~LuaStackChecker();

private:
  lua_State* luaState_;
  const char* filename_;
  int line_;
  int top_;
};

Файл LuaStackChecker.cpp:

#include "LuaStackChecker.h"
#include "lua/lua.hpp"

LuaStackChecker::LuaStackChecker(lua_State* L, const char* filename/* = ""*/, int line/* = 0*/)
: luaState_(L),
  filename_(filename),
  line_(line)
{
  top_ = lua_gettop(L);
}

LuaStackChecker::~LuaStackChecker()
{
  if(top_ != lua_gettop(luaState_))
  {
    luaL_error(luaState_, "Lua stack corrupted! File [%s] line[%d]", filename_, line_);
  }
}

Как видим, почти все тоже самое, что и описанное выше. Только в конструкторе LuaStackChecker добавились еще два параметра - имя файла и номер строки. Естественно, их значения не стоит вбивать в ручную, для этого есть две определенные компилятором переменные - __FILE__ и __LINE__. Перепишем нашу функцию с использованием LuaStackChecker:

bool getField(lua_State* L, int& field)
{
  // stack:
  LuaStackChecker sc(L, __FILE__, __LINE__);

  bool ret = false;
  lua_getglobal(L, "table1"); // stack: table1
  if(lua_istable(L, -1))
  {
    lua_getfield(L, -1, "field"); // stack: table1 field
    if(lua_isnumber(L, -1))
    {
      field = (int)lua_tonumber(L, -1);
      ret = true;
    }
    lua_pop(L, 1); // stack: table1
  }
  lua_pop(L, 1); // stack:

  return ret;
}

Для проверки я написал маленький тестик и в функции getField() закомментировал последний вызов lua_pop(). В результате в я получил следующее сообщение:

error

Для отладки - то что надо!
Внутри функции я предпочитаю самостоятельно следить за стеком. Однако можно написать простой класс, который сам восстанавливает стек. Я назвал его LuaStackGuard. Стоит обратить внимание на то, что LuaStackGuard правильно восстановит стек только в том случае, если в стек значения добавлялись. Если же значения из стека удалялись, то Lua заполнит стек до нужного значения nil-ами, что скорее всего так же приведет к проблемам, поскольку в стеке окажется совсем не то, чего ожидает пользователь. Файл LuaStackGuard.h:

#pragma once

struct lua_State;
class LuaStackGuard
{
public:
  explicit LuaStackGuard(lua_State* L);
  ~LuaStackGuard();

private:
  lua_State* luaState_;
  int top_;
};

Файл LuaStackGuard.cpp:

#include "LuaStackGuard.h"
#include "lua/lua.hpp"

LuaStackGuard::LuaStackGuard(lua_State* L)
: luaState_(L)
{
  top_ = lua_gettop(L);
}

LuaStackGuard::~LuaStackGuard()
{
  lua_settop(luaState_, top_);
}

Функцию getField() перепишем так (почти как плохой вариант, только теперь он хороший) и для верности оставим LuaStackChecker:

bool getField(lua_State* L, int& field)
{
  // stack:
  LuaStackChecker sc(L, __FILE__, __LINE__);
  LuaStackGuard sg(L);

  bool ret = false;
  lua_getglobal(L, "table1"); // stack: table1
  if(lua_istable(L, -1))
  {
    lua_getfield(L, -1, "field"); // stack: table1 field
    if(lua_isnumber(L, -1))
    {
      field = (int)lua_tonumber(L, -1);

      return true;
    }
  }

  return false;
}

Запускаем тест - все работает, как и ожидалось.
Для получения значения из таблицы приходится делать несколько проверок. Существует ли таблица с таким именем? Если да, то существует ли в этой таблице поле с нужым именем? Если да, то это поле ожидаемого ли типа?
Естественно, что и этот аспект хочется как-то автоматизировать. Объявим шаблонную функцию для получения значения из стека по индексу:

template<typename T>
bool fromLua(lua_State* L, int index, T& ret);

Эта функция так и останется объявленной, но не определенной, поскольку для каждого типа данных нужно вызывать специальные функции. Сделаем специализацию этой шаблонной функции для примитивных типов данных - bool, int, double, string:

template<> bool fromLua(lua_State* L, int index, bool& ret)
{
  if(lua_isboolean(L, index))
  {
    ret = lua_toboolean(L, index) != 0;
    return true;
  }

  return false;
}

template<> bool fromLua(lua_State* L, int index, double& ret)
{
  if(lua_isnumber(L, index))
  {
    ret = lua_tonumber(L, index);
    return true;
  }

  return false;
}

template<> bool fromLua(lua_State* L, int index, int& ret)
{
  if(lua_isnumber(L, index))
  {
    ret = (int)lua_tonumber(L, index);
    return true;
  }

  return false;
}

template<> bool fromLua(lua_State* L, int index, std::string& ret)
{
  if(lua_isstring(L, index))
  {
    ret = lua_tostring(L, index);
    return true;
  }

  return false;
}

Было бы неплохо читать из Lua массивы(std::vector) и карты (std::map):

template<typename T>
bool luaT_to(lua_State* L, int index, std::vector<T>& ret)
{
  // stack:
  if(!lua_istable(L, index))
    return false;

  LuaStackChecker sc(L, __FILE__, __LINE__);

  lua_pushvalue(L, index); // stack: vector

  const int count = luaL_getn(L, -1);
  for(int i = 1; count >= i; ++i)
  {
    lua_pushnumber(L, i);
    lua_gettable(L, -2);
    T value;
    fromLua(L, -1, value);
    ret.push_back(value);
    lua_pop(L, 1); // stack: vector
  }
  lua_pop(L, 1); // stack:

  return true;
}

template<typename TKey, typename TValue>
bool fromLua(lua_State* L, int index, std::map<TKey, TValue>& ret)
{
  // stack:
  if(!lua_istable(L, index))
    return false;

  LuaStackChecker sc(L, __FILE__, __LINE__);

  lua_pushvalue(L, index); // stack: map

  lua_pushnil(L); // stack: map nil
  while(lua_next(L, -2)) // stack: map key value
  {
    TKey key;
    fromLua(L, -2, key);
    TValue value;
    fromLua(L, -1, value);
    ret[key] = value;
    lua_pop(L, 1); // stack: map key
  }
  // stack: map
  lua_pop(L, 1); // stack:

  return true;
}

И еще одна удобная функция, получения значения по текстовому ключу в таблице:

template<typename T>
bool fromLua(lua_State* L, int index, const char* key, T& ret)
{
  // stack: table
  LuaStackChecker sc(L, __FILE__, __LINE__);
  lua_getfield(L, index? index : LUA_GLOBALSINDEX, key); // stack: table value
  bool res = fromLua(L, -1, ret);
  lua_pop(L, 1); // stack: table

  return res;
}

Если индекс равен нулю, то переменная берется из глобального пространства имен.
Так же обратите внимание на то, что для определения размера массива используется функция  luaL_getn(), аналогичная оператору # в Lua. А для перебора всех значений в таблице используется функция lua_next(), аналогичная функции pairs() в Lua.

Теперь разберемся с возвратом значений в Lua. Сначала напишем функции для возврата примитивных типов данных - bool, int, double, string:

void toLua(lua_State* L, const bool& arg)
{
  lua_pushboolean(L, arg ? 1 : 0);
}

void toLua(lua_State* L, const double& arg)
{
  lua_pushnumber(L, arg);
}

void toLua(lua_State* L, const int& arg)
{
  lua_pushnumber(L, arg);
}

void toLua(lua_State* L, const std::string& arg)
{
  lua_pushstring(L, arg.c_str());
}

Затем  функции для возврата массивов(std::vector) и карт (std::map). И тут без помощи шаблонов не обойтись:

template<typename T>
void toLua(lua_State* L, const std::vector<T>& arg)
{
  // stack:
  lua_newtable(L); // stack: table
  const size_t size = arg.size();
  for(size_t i = 0; arg.size() > i; ++i)
  {
    lua_pushnumber(L, i + 1); // stack: table i
    toLua(L, arg[i]); // stack: table i value
    lua_settable(L, -3); // stack: table
  }
}

template<typename TKey, typename TValue>
void toLua(lua_State* L, const std::map<TKey, TValue>& arg)
{
  // stack:
  lua_newtable(L); // stack: table
  for(typename std::map<TKey, TValue>::const_iterator i = arg.begin(); arg.end() != i; ++i)
  {
    toLua(L, i->first); // stack: table key
    toLua(L, i->second); // stack: table key value
    lua_settable(L, -3); // stack: table
  }
}

Ну, а как нам быть с пользовательскими типами данных? Очень просто - для каждого типа сделать специализацию двух шаблонных функций: toLua() и fromLua().
Пример. Пользовательский тип:

struct S
{
  string text;
  vector<int> values;
};

Специализации:

template<> void toLua(lua_State* L, const S& arg)
{
  // stack:
  lua_newtable(L); // stack: table
  toLua(L, arg.text); // stack: table text
  lua_setfield(L, -2, "text"); // stack: table
  toLua(L, arg.values); // stack: table values
  lua_setfield(L, -2, "values"); // stack: table
}

template<> bool fromLua(lua_State* L, int index, S& arg)
{
  // stack:
  if(lua_istable(L, index))
  {
    string text;
    vector<int> values;
    if(fromLua(L, index, "text", text) &&
       fromLua(L, index, "values", values))
    {
      arg.text = text;
      arg.values = values;

      return true;
    }
  }

  return false;
}

Теперь мы можем читать и записывать в стек Lua не только одиночные экземпляры пользовательских типов, но также их массивы (std::vector) и карты (std::map).
Отлично! Осталось малое - убедиться, что код работает. Сначала напишем тест, в котором данные читаются из Lua.
Сначала код из файла fromLuaTest.lua:

-- базовые типы данных
bool = true
int = 10
double = 1.5
text = "abcd"

-- массив
array = {3, 2, 1}

-- карта
map = {four = 4, five = 5, ['six'] = 6}

-- пользовательский тип
userType = {text = 'userType', values = {9, 8, 7}}

-- массив пользовательских типов
userTypeArray = {
{text = 'first', values = {1, 11, 111}},
{text = 'second', values = {2, 22, 222}},
{text = 'third', values = {3, 33, 333}},
}

-- карта пользовательских типов
userTypeMap = {
 fourth = {text = 'fourthValue', values = {4, 44, 444}},
 fifth = {text = 'fifthValue', values = {5, 55, 555}},
 sixth = {text = 'sixthValue', values = {6, 66, 666}},
}

Вроде все понятно. В тесте я постарался сделать так, чтобы значения переменных не повторялись (так проще отлавливать баги). Теперь код С++. Добавим метод print() в пользовательский тип S и вспомогательную функцию printArray():

template <typename T>
void printArray(const vector<T>& array)
{
  for(size_t i = 0; array.size() > i; ++i)
  {
    cout << array[i] << " ";
  }
}

struct S
{
  void print() const
  {
    cout << "User Type: ";
    cout << "text[" << text << "] ";
    cout << "values[";
    printArray(values);
    cout << "]" << endl;
  }
  string text;
  vector<int> values;
};

void testFromLua(lua_State* L)
{
  // -- примитивные типы данных
  bool boolVal;
  int intVal;
  double doubleVal;
  string text;
  fromLua(L, 0, "bool", boolVal);
  fromLua(L, 0, "int", intVal);
  fromLua(L, 0, "double", doubleVal);
  fromLua(L, 0, "text", text);
  cout << "Boolean:" << boolVal << endl;
  cout << "Int:" << intVal << endl;
  cout << "Double:" << doubleVal << endl;
  cout << "Text:" << text << endl;

  // массив
  vector<int> array;
  fromLua(L, 0, "array", array);
  cout << "Array:" << endl;
  printArray(array);
  cout << endl;

  // карта
  map<string, int> m;
  fromLua(L, 0, "map", m);
  cout << "Map:" << endl;
  for(map<string, int>::const_iterator i = m.begin(); m.end() != i; ++i)
  {
    cout << "[" << i->first << "] = " << i->second << endl;
  }

  // пользовательский тип
  S s;
  fromLua(L, 0, "userType", s);
  s.print();

  // массив пользовательских типов
  vector<S> vs;
  fromLua(L, 0, "userTypeArray", vs);
  cout << "User Type Array:" << endl;
  for(size_t i = 0; vs.size() > i; ++i)
  {
    vs[i].print();
  }

  // карта пользовательских типов
  map<string, S> ms;
  fromLua(L, 0, "userTypeMap", ms);
  cout << "User Type Map:" << endl;
  for(map<string, S>::const_iterator i = ms.begin(); ms.end() != i; ++i)
  {
    cout << "[" << i->first << "] = ";
    i->second.print();
  }
}

int main()
{
  lua_State* L = lua_open();
  luaL_openlibs(L);
  if(luaL_dofile(L, "fromLuaTest.lua"))
  {
    printf(lua_tostring(L, -1));
    exit(0);
  }
  testFromLua(L);

    return 0;
}

А вот и результат теста:

from_lua_test

Все совпадает, замечательно! Снова пишем тест, но уже для проверки передачи данных из С++ в Lua. Для этого нам придется совершить еще несколько небольших телодвижений. В С++ создадим функцию, которая, будучи вызванной из Lua вернет туда все типы данных, которые мы хотим протестировать. Эта функция должна иметь следующий вид:

int function(lua_State*);

В Lua она регистрируется следующим образом:

// stack:
lua_pushcfunction(L, func); // stack: function
lua_setglobal(L, "func"); // stack:

Теперь из Lua-скрипта эту функцию можно вызывать как обычную функцию Lua:

func()

Код С++ для теста:

struct S
{
  void print() const
  {
    cout << "User Type: ";
    cout << "text[" << text << "] ";
    cout << "values[";
    printArray(values);
    cout << "]" << endl;
  }
  string text;
  vector<int> values;
  bool operator < (const S& other) const
  {
    return text < other.text;
  }
};

int func(lua_State* L)
{
  // примитивные типы данных
  toLua(L, true); // args 1
  toLua(L, 10); // args 2
  toLua(L, 2.3); // args 3
  toLua(L, string("xyz")); // args 4

  // массив
  toLua(L, vector<int>(3, 100)); // args 5

  // карта
  map<string, int> m;
  m["one"]= 1;
  m["two"]= 2;
  m["three"]= 3;
  toLua(L, m); // args 6

  // пользовательский тип
  S s;
  s.text = "userType";
  s.values.push_back(200);
  s.values.push_back(300);
  s.values.push_back(400);
  toLua(L, s); // args 7

  // массив пользовательских типов
  vector<S> vs;
  s.text = "text1";
  s.values = vector<int>(3, 1);
  vs.push_back(s);
  s.text = "text2";
  s.values = vector<int>(3, 2);
  vs.push_back(s);
  s.text = "text3";
  s.values = vector<int>(3, 3);
  vs.push_back(s);
  toLua(L, vs); // args 8

  // карта пользовательских типов
  // здесь сделаем похитрее
  // карта будет индексироваться пользовательским типом
  map<S, string> ms;
  // для того, чтобы это работало,
  // у пользовательского типа должен быть определен оператор <
  s.text = "text4";
  s.values = vector<int>(3, 4);
  ms[s] = "value4";
  s.text = "text5";
  s.values = vector<int>(3, 5);
  ms[s] = "value5";
  s.text = "text6";
  s.values = vector<int>(3, 6);
  ms[s] = "value6";
  toLua(L, ms); // args 9

  return 9;
}

int main()
{
  lua_State* L = lua_open();
  luaL_openlibs(L);
  lua_pushcfunction(L, func); // stack: function
  lua_setglobal(L, "func"); // stack:
  if(luaL_dofile(L, "toLuaTest.lua"))
  {
    printf(lua_tostring(L, -1));
    exit(0);
  }

  return 0;
}

Код Lua из файла toLuaTest.lua:

function printUserType(s, ...)
  print('User Type:', 'text', s.text, 'values', unpack(s.values), unpack(arg))
end

local bool, int, double, text, array, map, s, vs, ms = func()
print('Boolean:', bool)
print('Int:', int)
print('Double:', double)
print('String:', text)
print('Array:', unpack(array))
print('Map:')
for k, v in pairs(map) do
  print(k, v)
end
printUserType(s)
print('User Type Array:')
for i = 1, #vs do
  printUserType(vs[i]) 
end
print('User Type Map:')
for k, v in pairs(ms) do
  printUserType(k, v)
end

Результат теста:

to_lua_test

Все правильно.
Итак, сделан еще один шаг. Код из этого примера будет активно использоваться в следующих уроках, ибо удобен он чрезвычайно.
До встречи!

Файлы урока можно взять здесь.

Назад
Hosted by uCoz