Данная статья — перевод моего туториала, который я изначально писал на английском. Однако этот перевод содержит дополнения и улучшения по сравнению с оригиналом.
Туториал не требует знания 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 <LuaBridge.h>
#include <iostream>
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<std::string>();
int answer = n.cast<int>();
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 <LuaBridge.h>
#include <iostream>
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<std::string>();
int answer = n.cast<int>();
Проверка и исправление ошибок
Но некоторые вещи могут пойти не так, и стоит производить проверку и обработку ошибок. Рассмотрим наиболее важные и часто встречающиеся ошибки
Что, если скрипт 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<std::string>();
}
Таблицы
Таблицы — это не просто массивы: таблицы — замечательная структура данных, которая позволяет хранить в них переменные 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<std::string>();
int width = w.cast<int>();
int height = h.cast<int>();
std::cout << titleString << std::endl;
std::cout << "width = " << width << std::endl;
std::cout << "height = " << height << std::endl;
Вы должны увидеть на экране следующее:
Window v.0.1
width = 400
height = 500
Как видите, можно получать различные элементы таблицы, используя оператор []. Можно писать короче:
int width = t["width"].cast<int>();
Можно также изменять содержимое таблицы:
t["width"] = 300
Это не меняет значение в скрипте, а лишь значение, которое содержится в ходе выполнения программы. Т.е. происходит следующее:
int width = t["width"].cast<int>(); // 400
t["width"] = 300
width = t["width"].cast<int>(); // 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<int>();
Функции
Давайте напишем простую функции на 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 <LuaBridge.h>
#include <iostream>
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, в котором я рассказал о том, как использую скрипты в своей игре, практические примеры всегда полезны.
Автор: eliasdaler