Использование Lua и C++ для обработки и хранения данных

Код статьи можно посмотреть здесь.
Чем так хорош Lua?

Когда-то я разрабатывал свою игру и задался вопросом: а какой формат данных лучше использовать для конфигурационных файлов?
Ведь удобно, когда создаёшь какой-либо объект, задавать различные начальные параметры не в самом коде, а в отдельных файлах. Это позволяет изменять некоторые параметры объектов без рекомпиляции, да и вообще даёт возможность менять их людям далёким от программирования.
Разработчики используют разные форматы: одни используют JSON, другие — XML, либо другие форматы данных. Ну а некоторые вообще хранят данные в .txt файлах или пишут свои парсеры. После рассмотрения различных форматов я остановился на Lua.

Lua можно использовать не только для игр, но и вообще для любых программ, которые используют данные, хранящиеся в других файлах.

Вот, что выделяет Lua на фоне других форматов:
  • Lua легко использовать без дополнительных зависимостей (кроме одной библиотеки Lua и трёх .h файлов).
  • В Lua файлах данные можно инициализировать с помощью математических выражений или функций, написанных на Lua. Например:
    some_variable = math.sqrt(2) * 2
    some_variable2 = 64 * 16 - 32
    

  • Lua — очень быстрый язык, который к тому же не занимает много памяти.
  • У Lua лицензия MIT, которая позволяет использовать этот язык как в бесплатных, так и в коммерческих проектах, причём без всякой возни с бумагами. Как написано на сайте: «просто скачайте и пользуйтесь».
  • Lua комплируется практически везде, т.к. он написан на чистом C без использования дополнительных библиотек.
  • Данные можно хранить и сортировать в приятном глазу виде. Их легко читать и модифицировать в любом текстовом редакторе.

Начнём с простого примера, а затем я перейду к реализации класса.
Пример

Допустим, есть файл Player.lua
player = {
    pos = {
         X = 20,
         Y = 30,
    },
    filename = "res/images/player.png",
    HP = 20,
--  а ещё можно комментарии добавлять
}

С простым классом данные можно будет получать так:
LuaScript script("player.lua");
std::string filename = script.get<std::string>("player.filename");
int posX = script.get<std::string>("player.pos.X");


Внимание, чтобы код был понятен, рекомендуется прочесть информацию о том, как работает стек Lua и посмотреть на простейшие примеры.
Почитать можно здесь.

Начнём с создания класса:
#ifndef LUASCRIPT_H
#define LUASCRIPT_H
 
#include <string>
#include <vector>
#include <iostream>
 
// Lua написан на C, поэтому нужно сообщить компилятору, чтобы он воспринимал хэдеры как код на C
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}
 
class LuaScript {
public:
    LuaScript(const std::string& filename);
    ~LuaScript();
    void printError(const std::string& variableName, const std::string& reason);
 
    template<typename T>
    T get(const std::string& variableName) {
        // реализация функции последует позже в статье
    }
    bool lua_gettostack(const std::string& variableName) { // заносим переменную в вершину стека. Если успешно - возвращаем true
       // реализация позже
    }
    // Возращаем 0 по умолчанию
    template<typename T>
    T lua_get(const std::string& variableName) {
      return 0;
    }
    // Эта функция используется в случае, если не удалось получить значение переменной и нужно вернуть какое-то
    // нулевое стандартное значение
    template<typename T>
    T lua_getdefault(const std::string& variableName) {
      return 0;
    }
private:
    lua_State* L;
    int level; // см. реализацию lug_gettostack
};
 
#endif

Конструктор:
LuaScript::LuaScript(const std::string& filename) {
    L = luaL_newstate();
    if (luaL_loadfile(L, filename.c_str()) || lua_pcall(L, 0, 0, 0)) {
        std::cout<<"Error: script not loaded ("<<filename<<")"<<std::endl;
        L = 0;
    }
}
.
Создаём lua_State, в случае если файл не был найден, либо произошла какая-либо другая ошибка, выводим сообщение об этом.
Деструктор:
LuaScript::~LuaScript() {
    if(L) lua_close(L);
}


Метод printError создан для того, чтобы выводить сообщения об ошибках:
void LuaScript::printError(const std::string& variableName, const std::string& reason) {
    std::cout<<"Error: can't get ["<<variableName<<"]. "<<reason<<std::endl;
}

lua_getdefault используется для того, чтобы вернуть какое-либо нулевое значение, если произошла ошибка. И если для чисел можно вернуть ноль, то для строк, например, это не сработает, поэтому делаем специализацию шаблона (этот код будет в хэдере).
template<>
inline std::string LuaScript::lua_getdefault<std::string>() {
  return "null";
}

А теперь напишем шаблонную функцию get.
Для начала напишем функцию lua_gettostack, которая заносит переменную на вершину стека
Разберём алгоритм на примере. Пусть нужно получить переменную «player.pos.X» из файла Player.lua
Проходим циклом до первой точки, при этом добавляя прочитанные символы в переменную «var».
«player» — таблица, которая является глобальной, поэтому получаем её с помощью lua_getglobal.
«pos» и «X» — это уже данные, которые не являются глобальные, но их можно получить с помощью lua_getfield, т.к. сама таблица player находится в вершине стека.
Затем уже в шаблонной функции get выполняется специфичная для типа данных функция, очищается стек и возвращается искомое значение, а в случае ошибки — вызывается функция lua_getdefault.


bool lua_gettostack(const std::string& variableName) {
      level = 0;
      std::string var = "";
        for(unsigned int i = 0; i < variableName.size(); i++) {
          if(variableName.at(i) == '.') {
            if(level == 0) {
              lua_getglobal(L, var.c_str());
            } else {
              lua_getfield(L, -1, var.c_str());
            }
            
            if(lua_isnil(L, -1)) {
              printError(variableName, var + " is not defined");
              return false;
            } else {
              var = "";
              level++;
            }
          } else {
            var += variableName.at(i);
          }
        }
        if(level == 0) {
          lua_getglobal(L, var.c_str());
        } else {
          lua_getfield(L, -1, var.c_str());
        }
        if(lua_isnil(L, -1)) {
            printError(variableName, var + " is not defined");
            return false;
        }
        // всё ок, возвращаем true
        return true; 
}

Возвращаемся к методу get:
template <typename T>
T get(const std::string& variableName) {
      if(!L) {
        printError(variableName, "Script is not loaded");
        return lua_getdefault<T>();
      }
      
      T result;
      if(lua_gettostack(variableName)) { // всё ок, переменная на вершине стека
        result = lua_get<T>(variableName);
      } else {
        result = lua_getdefault<T>();
      }

      lua_pop(L, level + 1); // очищаем стек
      return result;
    }
}

Осталось лишь добавить специализиации шаблонов(пример для некоторых типов данных):
template <>
inline bool LuaScript::lua_get<bool>(const std::string& variableName) {
    return (bool)lua_toboolean(L, -1);
}
 
template <>
inline float LuaScript::lua_get<float>(const std::string& variableName) {
    if(!lua_isnumber(L, -1)) {
      printError(variableName, "Not a number");
    }
    return (float)lua_tonumber(L, -1);
}
 
template <>
inline int LuaScript::lua_get<int>(const std::string& variableName) {
    if(!lua_isnumber(L, -1)) {
      printError(variableName, "Not a number");
    }
    return (int)lua_tonumber(L, -1);
}
 
template <>
inline std::string LuaScript::lua_get<std::string>(const std::string& variableName) {
    std::string s = "null";
    if(lua_isstring(L, -1)) {
      s = std::string(lua_tostring(L, -1));
    } else {
      printError(variableName, "Not a string");
    }
    return s;
}


На этом всё. Напоминаю, весь код в статье есть здесь. Там же можно найти пример использования класса.

Что дальше?

У Lua ещё много возможностей, которые я опишу во второй части статьи в ближайшем будущем. Например, получение массива данных неопределённой длины, а также получение списка ключей таблицы (например для таблицы Player из примера он был бы таким:[«pos», «filename», «HP»])
А ещё из Lua можно вызывать C++ функции, так же как и из C++ можно вызывать функции Lua, о чём я напишу в третьей части.
Удачного скриптинга!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 28

    0
    Используем на сервере ( C ) и на клиенте (Actionscript) очень удобно. Храним формулы один раз и передаём с сервера на клиент, вычисляем независимо. Очень просто. Внедрили на клиенте за один вечер.
      +5
      Когда-то давно использовал Lua в качестве встроенного скриптового языка в одном продукте. Потом понял, что зря я это сделал. В простых примерах всё выглядит отлично, но чуть шаг в сторону — и понимаешь, что Python или Javascript (со всей мощью их библиотек и сообществ) сделали бы разработку более быстрой.
        0
        Вы можете показать таковые примеры?
          +4
          До тех пор, пока скриптовый язык в продукте нужен чисто чтобы получить функционал макросов для внутреннего движка (грубо говоря — дать возможность пользователю написать вызов какой-то внутренней функции в цикле) — Lua подходит хорошо. Но потом от юзеров начинают приходить хотелки в духе «а тут мы хотим скриптом распарсить JSON, выбрать данные, вызвать внутреннюю функцию продукта, а потом результаты упаковать в Protobuf, потом в zip и отправить по почте. И твитнуть об этом.». И вот тут в голову приходит понимание, что Lua как-то мимо кассы.
            –1
            Это все надуманные проблемы. Тех же json модулей для lua пруд пруди.
              +2
              Пора-бы уже вводить какие-то универсальные индексы языков, включающие охват реальных задач библиотеками, наличие литературы, программистов, вопросов на Stackoverflow — ну чтобы можно было в подобных дискуссиях ссылаться. А так получается непредметный спор.
              0
              Огромное количество модулей можно взять здесь: luarocks.org/repositories/rocks/
          0
          Не всегда нужны дополнительные библиотеки, особенно учитывая тот факт, что lua встраеваемый язык, который выполняется в том окружении, которое вы ему создадите. С помощью swig великолепно создается привязка к вашему коду на с / с++ (если правильно писать этот код конечно же ). А самое главное порог входа значительно ниже, ибо попытка хоть как -то заставить функционировать boost python не для развлечений, а реального проекта равносильна стрельбе по ногам teammate 'ов и своим заодно.
            +1
            Случаи когда «дополнительные библиотеки не нужны» заканчиваются на первом же реальном внедрении продукта. По поводу lua и boost \ python — да ладно, специалистов, знакомых с boost и python — больше.
              0
              Да, не нужны, ибо реальный проект имеет API, который достаточен для решения задач, возложенных на скрипты. Lua без экспорта в него boost успешно используется в Crytek, Blizzard, etc. Там экспортирутся только то, что реально нужно, без каких-либо сторонних библиотек. Некоторые библиотеки вообще нельзя экспортировать — опасно для жизни. К тому же базовых библиотек в lua более, чем достаточно.
          0
          Представьте что у вас есть много (на пример 1000) Lua скриптов. Они статичные, т.е. уже существуют и по мере работы ПО новые загружать не нужно. У вас есть обычный такой многопоточный сервер. Вам нужно по запросам пользователей выполнять тот или иной скрипт (само собой передаются туда какие данные и этот скрипт должен отработать какую то бизнес логику, т.е. в свою очередь, что то изменить в структурах данных ПО). Теперь задумайтесь над тем что вы не можете на каждый запрос пользователя загружать скрипт с ФС, а еще перед использованием state машины Lua вам ее нужно инициализировать (сделать всякие биндинги). А теперь вопрос, как такое сделать с помощью Lua?
            0
            Я думаю, что уже инициализированные lua_state могут храниться на сервере. О том, как запускать код Lua с помощью C++, я напишу в следующей части туториала, но суть в том, что конкретные функции можно запускать на сервере, при этом lua_state перезагружать или как-то изменять не нужно. Схема такая: у вас есть какой-то список Lua файлов. Вы загружаете каждый из них в отдельных lua_state. Затем, когда пользователь использует какую-либо функцию, вы находите соответствующий lua_state и просто получаете функцию с помощью
            lua_getglobal(L, "some_func"); // где L - lua_state отвечающий файлу, в котором находится some_func
            

            А затем эту функцию можно вызывать с помощью
            lua_call(0, 1); // сейчас в стеке Lua на вершине находится функция, поэтому вызовется именно она.
            // Первый аргумент - кол-во аргументов
            // второй - кол-во возвращаемых значений
            

            А затем удалить функцию из стека:
            lua_getglobal(L, -1); 
            
              0
              Сударь, учите матчасть:
              Делаете loadfile, подгружаете код в память, выполняете нужный по мере нужды. Но зачем такая трудная стратегия?
              А то что перед использованием lua нужно подгрузить биндинги это тлен и суета, все делается лишь один раз, используется указатель не одну и ту же стековую машину.
              Но зачем так сложно? зачем 1000 файлов?
                0
                Да, этот метод будет лучше того, который описан мной в комментарии.
                  0
                  я может быть не достаточно хорошо изучил Lua, но для того, что бы работали биндинги их нужно записать (таблицы) в конкретный state, притом в тот же где и будут они использоваться. Далее, имея один такой state мы его не можем вызвать одновременно из двух потоков. Так как state это по сути стек (ну или со state связан ровно один стек). Так же мы не может сделать копию уже существуюшего state. Т.е. мы должны обращаться к state как к общему ресурсу, а это налагает некоторые ограничения.

                  На счет загрузки биндингов в стейт. Это конечно плевая операция, но в размерах нормального приложения, это достаточно много биндингов, и делать это для каждого отдельного вызова, накладно.

                  На счет 1000 файлов. Имелось в виду 1000 отдельных функций. Они конечно могут быть и в одном файле. Но это означает что этот файл загрузится в один стейт либо эти 1000 функций загрузятся сразу в кучу стейтов. А по сути для каждого вызова нужно только по одной функции. Поэтому мне показалось логичнее разделить их по одной функции на файл. А конкретно на счет 1000, это просто для того что бы показать что речь идет не о нескольких, а о достаточно большом числе функций.
                    0
                    Никто не предлагает загружать для каждого вызова, предлагается предзагрузить перед стартом. Когда вы работаете с программой на Lua, при загрузке она инициализирует все библиотеки, подгружая их в память, а не вызывая каждый раз loadstring/loadfile/load.
                    Ситуация с многопоточностью требует отдельного рассмотрения, но можно иметь один стейт и устроить виртуальную многопоточность, что, замечу, предпочтительно для луа. Плюс к тому можно устроить небольшой пул с проинициализированными стейтами. Комбинация последних двух вещей с асинхронной моделью сервера — самое то.

                    Должен заметить, что ваше предположение, что копирование луа-стейта невозможно — полнейшая ересь:) Как и предполагается — стейт это структура на C с набором полей.
                    > вот прекрасный пример того, что делать это возможно.
                      0
                      > Ситуация с многопоточностью требует отдельного рассмотрения
                      Так а я вам о чем? Что то я не понял при виртуальную многопоточность. Я имел в виду мне не треды в Lua нужно поднимать, мне нужно одну и туже функцию выполнять независимо в разных потоках. По сути я клоню к тому что мне нужно асинхронно дергать Lua функции по запросу. Понятно дело в один момент времени одну и туже функцию могут запросит либо никто либо сразу все. Но я с вами солидарен что это можно сделать только синхронизовав доступ к каждому конкретному стейту. Ну и далее например каждый стейт хранить в X экземплярах.

                      >Должен заметить, что ваше предположение, что копирование луа-стейта невозможно — полнейшая ересь:)
                      Привидите мне ссылку на функцию lua API или хотя бы описание как это сделать. memcpy не в счет, это хак. Я искал, и все что я нашел, что как раз таки нельзя такого сделать, без хаков.
                        0
                        Про синхронизацию — иметь пул стейтов и функцию «зачистки» спейса.
                        lua_State можно шарить на много потоков, «копировать» с помощью lper.
                        Lper.
                        не всегда хаки это плохо. линукса бы не было, если бы не грязные хаки:)
                        p.s. хотя, может я вас и не понимаю, а тогда наша речь идет в разных направлениях
                0
                Когда-то я разрабатывал свою игру и задался вопросом: а какой формат данных лучше использовать?

                После рассмотрения различных форматов я остановился на Lua.

                Шутка про гвоздь и микроскоп.

                Данные (в геймдеве) нужно хранить в бинарном виде, а не подключать для этого скриптовый язык. Де Сад шокирован.
                Lua должен оперировать с объектами C++ через обёртки и описывать лишь бизнес-логику. У вас же с точностью да наоборот.
                  0
                  Бинарные данные пусть и быстрее, но они их не так легко модифицировать (с Lua я могу просто редактировать файлы в любом текстовом редакторе), а читать diff вообще невозможно.
                    0
                    Ну а какие данные вы собрались хранить в Lua? mp3? dds? collada?
                    0
                    Возможно, вы просто не так поняли суть статьи. Моя ошибка, признаю, наверное пример выбрал сомнительный.
                    Я не использую Lua для хранения тех данных, которые используются самой программой, я использую Lua для хранения некоторых начальных данных, используемых на этапе загрузки. И «player.position» нужно воспринимать не как текущую позицию игрока(было бы очень глупо хранить её в отдельном скрипте), а как начальную позицию. Далее Lua не используется, объект класса Player получает начальные position.x и position.y из скрипта, а дальше эти переменные изменяются лишь с помощью C++.
                      0
                      Возможно вам стоит перефразировать начало статьи, дабы не вводить никого в заблуждение.
                      У вас получился Lua-конфиг. Не самое оптимальное решение.
                        0
                        Попробую перефразировать. И почему же решение не самое оптимальное?
                    0
                    Я не так давно тоже подключал lua — проекту требовалась возможность без перекладки менять алгоритм нарезки фотографий (фильтры, размеры и немного промежуточной логики). Были высокие требования к скорости, поэтому решил остановиться на lua, как на одном из самых быстрых скриптовых языков.
                    Заюзал самую вроде как быструю его реализацию — luajit+ffi.
                    Так вот, оно действительно очень шустрое получилось… Но общее субъективное впечатление от применения lua осталось не самое приятное — как язык в разы понятнее, проще и логичнее был бы обычный javascript. Чего стоит хотя бы тернарная условная операция на lua…
                    Для себя на будущее вынес вывод: если погоня за тактами не очень важна, то лучше привинтить какой-нибудь V8
                      0
                      Сравните на досуге количество страниц в мануале по Lua (который включает в себя все вплоть до полного описания API для встраивания) и в аналогичном по полноте описании JS. Тогда наверно Вы согласитесь, что в Вашем «понятнее, проще и логичнее» все слова следовало бы заменить на одно «привычнее» (для Вас). На самом деле есть совсем немного языков, которые проще и логичнее Lua.
                      +1
                      Главное помнить о безопасности 8)

                      Only users with full accounts can post comments. Log in, please.