Использовать Lua c С++ легче, чем вы думаете. Tutorial по LuaBridge

Данная статья — перевод моего туториала, который я изначально писал на английском. Однако этот перевод содержит дополнения и улучшения по сравнению с оригиналом.Туториал не требует знания Lua, а вот C++ нужно знать на уровне чуть выше базового, но сложного кода здесь нет.Когда-то я написал статью про использование Lua с C++ с помощью Lua C API. В то время, как написать простой враппер для Lua, поддерживающий простые переменные и функции, не составляет особого труда, написать враппер, который будет поддерживать более сложные вещи (функции, классы, исключения, пространства имён), уже затруднительно.Врапперов для использования Lua и C++ написано довольно много. С многими из них можно ознакомиться здесь.Я протестировал многие из них, и больше всего мне понравился LuaBridge. В LuaBridge есть многое: удобный интерфейс, exceptions, namespaces и ещё много всего.Но начнём по порядку, зачем вообще использовать Lua c С++?

Зачем использовать Lua? Конфигурационные файлы. Избавление от констант, магических чисел и некоторых define’ов Данные вещи можно делать и с помощью простых текстовых файлов, но они не так удобны в обращении. Lua позволяет использовать таблицы, математические выражения, комментарии, условия, системные функции и пр. Для конфигурационных файлов это бывает очень полезно.Например, можно хранить данные в таком виде:

window = { title = «Test project», width = 800, height = 600 } Можно получать системные переменные:

homeDir = os.getenv («HOME») Можно использовать математические выражения для задания параметров:

someVariable = 2 * math.pi Скрипты, плагины, расширение функциональности программы C++ может вызывать функции Lua, а Lua может вызывать функции C++. Это очень мощный функционал, позволяющий вынести часть кода в скрипты или позволить пользователям писать собственные функции, расширяющие функциональность программы. Я использую функции Lua для различных триггеров в игре, которую я разрабатываю. Это позволяет мне добавлять новые триггеры без рекомпиляции и создания новых функций и классов в C++. Очень удобно.

Немного о Lua. Lua — язык с лицензией MIT, которая позволяет использовать его как в некоммерческих, так и в коммерческих приложениях. Lua написан на C, поэтому Lua работает на большинстве ОС, что позволяет использовать Lua в кросс-платформенных приложениях без проблем.

Установка Lua и LuaBridge Итак, приступим. Для начала скачайте Lua и LuaBridgeДобавьте include папку Lua и сам LuaBridge в Include Directories вашего проектаТакже добавьте lua52.lib в список библиотек для линковки.

Создайте файл script.lua со следующим содержанием:

 — script.lua testString = «LuaBridge works!» number = 42 Добавьте main.cpp (этот код лишь для проверки того, что всё работает, объяснение будет чуть ниже):

// main.cpp #include #include extern «C» { # include «lua.h» # include «lauxlib.h» # include «lualib.h» } using namespace luabridge; int main () { lua_State* L = luaL_newstate (); luaL_dofile (L, «script.lua»); luaL_openlibs (L); lua_pcall (L, 0, 0, 0); LuaRef s = getGlobal (L, «testString»); LuaRef n = getGlobal (L, «number»); std: string luaString = s.cast(); int answer = n.cast(); std: cout << luaString << std::endl; std::cout << "And here's our number:" << answer << std::endl; } Скомпилируйте и запустите программу. Вы должны увидеть следующее:

LuaBridge works! And here’s our number:42

Примечание: если программа не компилируется и компилятор жалуется на ошибку «error C2065: «lua_State»: undeclared identifier» в файле LuaHelpers.h, то вам нужно сделать следующее:1) Добавьте эти строки в начало файла LuaHelpers.h

extern «C» { # include «lua.h» # include «lauxlib.h» # include «lualib.h» }

2) Измените 460ую строку Stack.h с этого: lua_pushstring (L, str.c_str (), str.size ()); На это: lua_pushlstring (L, str.c_str (), str.size ()); Готово!

А теперь подробнее о том, как работает код.

Включаем все необходимые хэдеры:

#include #include extern «C» { # include «lua.h» # include «lauxlib.h» # include «lualib.h» } Все функции и классы LuaBridge помещены в namespace luabridge, и чтобы не писать «luabridge» множество раз, я использую эту конструкцию (хотя её лучше помещать в те места, где используется сам LuaBridge)

using namespace luabridge; Создаём lua_State

lua_State* L = luaL_newstate (); Открываем наш скрипт. Для каждого скрипта не нужно создавать новый lua_State, можно использовать один lua_State для множества скриптов. При этом нужно учитывать коллизию переменных в глобальном нэймспейсе. Если в script1.lua и script2.lua будут объявлены переменные с одинаковыми именами, то могут возникнуть проблемы

luaL_dofile (L, «script.lua»); Открываем основные библиотеки Lua (io, math, etc.) и вызываем основную часть скрипта (т.е. если в скрипте были прописаны действия в глобальном нэймспейсе, то они будут выполнены)

luaL_openlibs (L); lua_pcall (L, 0, 0, 0); Создаём объект LuaRef, который может хранить себе всё, что может хранить переменная Lua: int, float, bool, string, table и т.д.

LuaRef s = getGlobal (L, «testString»); LuaRef n = getGlobal (L, «number»); Преобразовать LuaRef в типы C++ легко:

std: string luaString = s.cast(); int answer = n.cast(); Проверка и исправление ошибок Но некоторые вещи могут пойти не так, и стоит производить проверку и обработку ошибок. Рассмотрим наиболее важные и часто встречающиеся ошибки

Что, если скрипт Lua не найден? if (luaL_loadfile (L, filename.c_str ()) || lua_pcall (L, 0, 0, 0)) { … // скрипт не найден } Что, если переменная не найдена? Переменная может быть не объявлена, либо её значение — nil. Это легко проверить с помощью функции isNil ()

if (s.isNil ()) { std: cout << "Variable not found!" << std::endl; } Переменная не того типа, который мы ожидаем получить

Например, ожидается, что переменная имет тип string, тогда можно сделать такую проверку перед тем как делать каст:

if (s.isString ()) { luaString = s.cast(); } Таблицы Таблицы — это не просто массивы: таблицы — замечательная структура данных, которая позволяет хранить в них переменные Lua любого типа, другие таблицы и ставить ключи разных типов в соответствие значениям и переменным. Таблицы позволяют представлять и получать конфигурационные файлы в красивом и легкочитаемом виде.

Создайте script.lua с таким содержанием:

window = { title = «Window v. 0.1», width = 400, height = 500 } Код на C++, позволяющий получить данные из этого скрипта:

LuaRef t = getGlobal (L, «window»); LuaRef title = t[«title»]; LuaRef w = t[«width»]; LuaRef h = t[«height»]; std: string titleString = title.cast(); int width = w.cast(); int height = h.cast(); std: cout << titleString << std::endl; std::cout << "width = " << width << std::endl; std::cout << "height = " << height << std::endl; Вы должны увидеть на экране следующее:Window v.0.1width = 400height = 500

Как видите, можно получать различные элементы таблицы, используя оператор []. Можно писать короче:

int width = t[«width»].cast(); Можно также изменять содержимое таблицы:

t[«width»] = 300 Это не меняет значение в скрипте, а лишь значение, которое содержится в ходе выполнения программы. Т.е. происходит следующее:

int width = t[«width»].cast(); // 400 t[«width»] = 300 width = t[«width»].cast(); // 300 Чтобы сохранить значение, нужно воспользоваться сериализацией таблиц (table serialization), но данный туториал не об этом.

Пусть теперь таблица выглядит так:

window = { title = «Window v. 0.1», size = { w = 400, h = 500 } } Как можно получить значение window.size.w? Вот так:

LuaRef t = getGlobal (L, «window»); LuaRef size = t[«size»]; LuaRef w = size[«w»]; int width = w.cast(); Функции Давайте напишем простую функции на C++

void printMessage (const std: string& s) { std: cout << s << std::endl; } И напишем вот это в скрипте на Lua:

printMessage («You can call C++ functions from Lua!») Затем мы регистрируем функцию в C++

getGlobalNamespace (L). addFunction («printMessage», printMessage); Примечание 1: это нужно делать до вызова «luaL_dofile», иначе Lua попытается вызвать необъявленную функциюПримечание 2: Функции на C++ и Lua могут иметь разные имена

Данный код зарегистрировал функцию в глобальном namespace Lua. Чтобы зарегистрировать его, например, в namespace «game», нужно написать следующий код:

getGlobalNamespace (L). beginNamespace («game») .addFunction («printMessage», printMessage) .endNamespace (); Тогда функцию printMessage в скриптах нужно будет вызывать данным образом:

game.printMessage («You can call C++ functions from Lua!») Пространства имён в Lua не имеют ничего общего с пространствами имён C++. Они скорее используются для логического объединения и удобства.

Теперь вызовем функцию Lua из C++

 — script.lua sumNumbers = function (a, b) printMessage («You can still call C++ functions from Lua functions!») return a + b end // main.cpp LuaRef sumNumbers = getGlobal (L, «sumNumbers»); int result = sumNumbers (5, 4); std: cout << "Result:" << result << std::endl; Вы должны увидеть следующее:You can still call C++ functions from Lua functions!Result:9

Разве не замечательно? Не нужно указывать LuaBridge сколько и каких аргументов у функции, и какие значения она возвращает.Но есть одно ограничение: у одной функции Lua не может быть более 8 аргументов. Но это ограничение легко обойти, передав таблицу, как аргумент.

Если вы передаёте в функцию больше аргументов, чем требуется, LuaBridge молча проигнорирует их. Однако, если что-то пойдёт не так, то LuaBridge сгенерирует исключение LuaException. Не забудьте словить его! Поэтому рекомендуется окружать код блоками try/catch

Вот полный код примера с функциями:

 — script.lua printMessage («You can call C++ functions from Lua!») sumNumbers = function (a, b) printMessage («You can still call C++ functions from Lua functions!») return a + b end // main.cpp #include #include extern «C» { # include «lua.h» # include «lauxlib.h» # include «lualib.h» } using namespace luabridge; void printMessage (const std: string& s) { std: cout << s << std::endl; } int main() { lua_State* L = luaL_newstate(); luaL_openlibs(L); getGlobalNamespace(L).addFunction("printMessage", printMessage); luaL_dofile(L, "script.lua"); lua_pcall(L, 0, 0, 0); LuaRef sumNumbers = getGlobal(L, "sumNumbers"); int result = sumNumbers(5, 4); std::cout << "Result:" << result << std::endl; system("pause"); } Что? Есть ещё что-то? Да. Есть ещё несколько замечательных вещей, о которых я напишу в последующих частях туториала: классы, создание объектов, срок жизни объектов… Много всего!Также рекомендую прочитать этот dev log, в котором я рассказал о том, как использую скрипты в своей игре, практические примеры всегда полезны.

© Habrahabr.ru