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

eliasdaler 20 сентября 2014 в 19:11 38,6k
Данная статья — перевод моего туториала, который я изначально писал на английском. Однако этот перевод содержит дополнения и улучшения по сравнению с оригиналом.
Туториал не требует знания 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, в котором я рассказал о том, как использую скрипты в своей игре, практические примеры всегда полезны.
Проголосовать:
+41
Сохранить: