Шаг 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(). В результате в я получил
следующее сообщение:
Для отладки - то что надо!
Внутри функции я предпочитаю самостоятельно следить за стеком. Однако
можно написать простой класс, который сам восстанавливает стек. Я
назвал его 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;
}
А вот и результат теста:
Все совпадает, замечательно! Снова пишем тест, но уже для проверки
передачи данных из С++ в 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
Результат теста:
Все правильно.
Итак, сделан еще один шаг. Код из этого примера будет активно использоваться в следующих уроках, ибо удобен он чрезвычайно.
До встречи!
Файлы урока можно взять здесь.
Назад