Pull to refresh

Добавление скриптинга в программу с помощью Lua

Reading time 6 min
Views 19K

Lua это мощный, быстрый, легкий, встраиваемый язык сценариев. С его помощью можно легко и быстро добавить поддержку скриптинга в вашу программу.
Это может понадобиться в тех случаях, когда вы хотите дать возможность пользователям производить самостоятельную донастройку (кастомизацию) вашей программы, когда вы не хотите перекомпилировать весь проект, при внесении каких-либо изменений в логику работы программы, либо хотите разделить работу над движком и работу над логикой между разработчиками (например, при написании игр).

В этой статье, с помощью простой программы, я хочу показать пример встраивания Lua в ваш проект.

Примеров программ, которые используют Lua достаточно много. Далеко не полный список программ, использующих Lua, можно посмотреть здесь Lua Wiki и здесь Wikipedia

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

Пример я создавал в Visual Studio под Windows. Несмотря на это, приведенный код, за исключением нескольких функций (перечисление файлов, переименование файла), специфичных для Windows, после небольшой адаптации будет работать и на других платформах, т.к. Lua является кроссплатформенным языком сценариев.

Начнем с того, что посетим официальный сайт и скачаем Lua для своей платформы. Для Windows подойдет вот этот архив, включающий в себя библиотеки линковщика, динамические библиотеки и заголовочные файлы Lua.

Распаковываем, указываем в настройках Visual Studio пути к заголовочным файлам и библиотекам линковщика (Tools -> Options -> Projects and Solutions -> VC++ Directories).

Если кратко, по шагам, то добавление Lua скриптинга в проект происходит в несколько шагов:
  • Добавление в проект заголовочных файлов и библиотеки компоновщика
  • Инициализация виртуальной машины Lua
  • Реализация и регистрация экспортируемых функций

Создадим консольный проект. Назвать его можно как угодно. Первым делом добавим заголовочные файлы:

extern "C"
{
  #include <lua.h>
  #include <lualib.h>
  #include <lauxlib.h>
}

Почему extern «C»? Lua написан на ANSI C, если попытаться включить файлы без extern «C» то мы получим множество ошибок, таких как:

unresolved external symbol "char const * __cdecl lua_tolstring(struct lua_State *,int,unsigned int *)" (?lua_tolstring@@YAPBDPAUlua_State@@HPAI@Z)

Это вызвано тем, что соглашения о вызовах в C отличаются от соглашений в C++.

Не забудем подключить библиотеку линковщика:

#pragma comment(lib, "lua51.lib")

Теперь необходимо объявить и инициализировать экземпляр Lua интерпретатора.

// Объявляем
lua_State *g_LuaVM = NULL;

int _tmain(int argc, _TCHAR* argv[])
{
  // Инициализируем экземпляр
  g_LuaVM = lua_open();
  ...
  // Закрываем
  lua_close(g_LuaVM);
}

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

int LuaMatchString(lua_State *luaVM)

Вторая — переименовывает файл:

int LuaRenameFile(lua_State *luaVM)

Остановимся подробнее на реализации второй:

// В качестве параметров принимает текущий путь к файлу и новое имя файла
// В случае успеха возвращает 1, иначе 0

int LuaRenameFile(lua_State *luaVM)
{
  // Получаем число переданных из Lua скрипта аргументов
  int argc = lua_gettop(luaVM);

  // Если аргументов меньше двух - возвращаем ошибку
  if(argc < 2)
  {
    cerr << "RenameFile - wrong number of arguments!" << endl;
    // Вернем 0 в Lua скрипт
    lua_pushnumber(luaVM, 0);

    // Количество возвращаемых значений
    return 1;
  }

  // Проверяем типы аргументов, если это не строки
  // возвращаем 0 (признак ошибки)

  if(!lua_isstring(luaVM, 1) || !lua_isstring(luaVM, 2))
  {
    cout << "RenameFile - invalid arguments!" << endl;
    lua_pushnumber(luaVM, 0);

    // Количество возвращаемых значений
    return 1;
  }

  // Получаем значение первого аргумента, путь к файлу
  // Обратите внимание - индекс первого элемента - 1, а не 0, как принято в С++

  string strSource = lua_tostring(luaVM, 1);
  // Получаем значение второго аргумента, новое имя файла
  string strDestination = lua_tostring(luaVM, 2);

  strDestination = strSource.substr(0, strSource.rfind('\\') + 1) + strDestination;

  int nResult = (int)::MoveFileEx(strSource.c_str(), strDestination.c_str(), MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH);

  // Возвращаем в Lua скрипт результат выполнения MoveFileEx
  lua_pushnumber(luaVM, nResult);

  // Количество возвращаемых значений
  return 1;
}

Как видно — ничего сложного: проверяем количество переданных аргументов, проверяем тип аргументов, извлекаем, выполняем переименование файла и возвращаем результат.

Теперь нам необходимо дать Lua знать, о экспортируемых функциях, делается это просто:

lua_register(g_LuaVM, "RenameFile", LuaRenameFile);
lua_register(g_LuaVM, "MatchString", LuaMatchString);

RenameFile и MatchString это имена функций, которые будут «видны» в скрипте.

Создадим скрипт, выполняющий всю работу:

-- Вызывается для обработки имени файла
-- В качестве параметра передается полный путь к файлу
-- и да, -- это комментарии в Lua

function onFileFound(fileName)
  patt = MatchString('103([0-9]{1,12}).txt', fileName)

  -- Если совпадение найдено patt будет содержать
  -- найденно совпадение - ([0-9]{1,12})
  -- иначе nil

  if patt ~= nil then
    -- Переименовываем файл, заменяя 103 на XXX
    -- Пример: было 103180998.txt, станет XXX180858.txt

    RenameFile(fileName, 'XXX' .. patt .. '.txt')
  end
end

Чтобы совсем стало понятно, привожу кусок кода, который вызывает эту функцию

// Переместить на начало стека функцию onFileFound
lua_getglobal(g_LuaVM, "onFileFound");
// Поместить следующим элементом в стек путь к найденному файлу (fileName в скрипте)
lua_pushstring(g_LuaVM, strFilePath.c_str());

// Вызвать функцию onFileFound
if(lua_pcall(g_LuaVM, 1, 0, 0) != 0)
{
  // Проверить возвращенное значение,
  // если это не 0, сообщить об ошибке
  // lua_tostring(g_LuaVM, -1) содержит описание ошибки

  cerr << "Error calling function onFileFound: " << lua_tostring(g_LuaVM, -1) << endl;
}

Осталось только загрузить скрипт из нашей программы:

BOOL LoadScript()
{
  // Получение полного пути к скрипту
  CHAR szScriptPath[1024] = {0};
  GetModuleFileName(NULL, szScriptPath, _countof(szScriptPath) - 1);
  TCHAR *szSlashPos = strrchr(szScriptPath, '\\');
  if(szSlashPos != NULL)
  {
    szScriptPath[szSlashPos - szScriptPath + 1] = '\0';
  }
  strcat_s(szScriptPath, _countof(szScriptPath), _T("script.lua"));

  // Загружаем скрипт
  int s = luaL_loadfile(g_LuaVM, szScriptPath);

  // Если результат загрузки не 0 - произошла ошибка
  // lua_tostring(g_LuaVM, -1) содержит описание ошибки

  if(s != 0)
  {
    cout << "Error while loading script file!" << endl << "Error: " << lua_tostring(g_LuaVM, -1) << endl;

    return FALSE;
  }

  // Выполняем крипт
  s = lua_pcall(g_LuaVM, 0, LUA_MULTRET, 0);

  return TRUE;
}

Как видно встраивание Lua скриптинга в программу является, по сути, тривиальной задачей и вместе с тем дает широкое поле для творчества.

Исходники проекта

Ниже приведен список ресурсов, на которых можно почитать о Lua более подробно.

Официальный сайт
Live Demo
Русская документация
Я люблю Lua. I love Lua
Создание встраиваемых сценариев на языке Lua
Tags:
Hubs:
+48
Comments 120
Comments Comments 120

Articles