в

Kazan Dev Alliance

Казанское Сообщество Разработчиков Программного Обеспечения

Персональный блог Дмитрия Шмыкова

Блог предназначен для чтения программистами, которые пишут на С++. Публикуются способы использования библиотек С++, решения возникающих по ходу их использования проблем, ответы на часто задаваемые вопросы. Идейной составляющей материалов хочется видеть попытку представить "сложный" на первый взгляд язык С++, как простой язык на каждый день для написания качественного кода.

Библиотека Diluculum для работы со скриптовым языком Lua из С++

Коротко о Lua

Lua — это скриптовый язык. Он используется, в основном, в игровой индустрии. Это связано с тем, что большинство игр написано на С++, а Lua часто используется вместе с С++. Для получения информации по Lua достаточно в Вашей любимой поисковой системе набрать "скриптовый язык Lua". Скорее всего будет найдена статья в википедии. Полезная статья. С нее Вы сможете зайти на несколько полезных сайтов, посвященных Lua и всевозможным утилитам и библиотекам, прочему Lua-инструментарию. Русский перевод официальной доки можно найти здесь.

Коротко о Diluculum

Несмотря на то, что сам по себе Lua хорош, его надо уметь использовать из С++. Чтобы это можно было делать, в Lua были встроены механизмы, основанные на работе со стеком и прочими вещами, которые, вообще говоря, неудобны в использовании. Написано несколько библиотек для удобной работы с Lua. Здесь речь будет идти о библиотеке Diluculum. Мне она понравилась больше всех остальных. На данный момент она используется весьма редко, т.к. работает с Lua 5.1, что интерфейсно не совместимо с Lua 5.0 (там были переделаны некоторые заголовочные файлы). Для достижения совместимости с Lua 5.0 можно воспользоваться советами пользователей Lua из многочисленных форумов. В любом случае, если Вы пишете новую программу, Lua версий ниже 5.1 для Вас уже не существует. Из-за того, что Diluculum является сравнительно новой библиотекой, она оказалась весьма продуманной с точки зрения удобств использования.

Приведу перевод Diluculum Quick User's Guide на русский язык. Постараюсь лично от себя ничего не добавлять, т.к. автор и так хорошо изложил идеи. Надеюсь, что благодаря этой статье богатство Lua станет доступно более широкому кругу пользователей C++. Стоит добавить, что подобные библиотеки для работы с Lua есть и для C#, и для Java.

Diluculum Quick User's Guide

Введение

Diluculum — это библиотека, предназначенная для гармоничного сосуществования кода на С++ и кода на Lua. В этом смысле библиотека может предложить следующее:

  • Удобный доступ к Lua-данным. Это достигается через использование объектов класса Diluculum::LuaState, который инкапсулирует lua_State* (информация по lua_State находится в документации по Lua).

  • Неплохой способ делать доступными функции С++ из Lua через использование макроса DILUCULUM_WRAP_FUNCTION().

  • Ограниченный, однако весьма удобный, способ регистрировать классы на С++ в Lua (чтобы можно было создавать объекты этих классов из Lua [так называемые Lua-managed объекты]). Это можно сделать посредством использования макросов DILUCULUM_BEGIN_CLASS(), DILUCULUM_END_CLASS() и DILUCULUM_REGISTER_OBJECT().

В следующих параграфах описывается каждый из этих трех пунктов более детально. Замечание: Diluculum включает юнит-тесты для каждой его особенности в папке Tests дистрибутива с исходниками. Они могут использоваться как хорошие примеры использования библиотеки.

Получение доступа к данным Lua State из C++

Класс Diluculum::LuaState инкапсулирует lua_State*, хендл, через который идет работа с Lua-интерпретатором. Lua часто используется, как конфигурационный язык. Для этой цели очень хорошо использовать объекты класса Diluculum::LuaState. Пусть у нас имеется конфигурационный файл config.lua какого-нибудь приложения.

-- A configuration file
FavoriveColor = "blue"
FavoritePiApproximation = 22 / 7
UserInfo = {
    Name = "Fulano de Tal",
    Age = 33,
    LikeLargeIcons = true
}
WindowSize = { 456, 234 };
function EmphasizeFunc(s)
    return "<EMPH>"..s.."</EMPH>" 
end

Для того, чтобы получить информацию из этого файла и использовать ее из С++, первым делом создадим объект класса Diluculum::LuaState, как показано в следующем листинге.

#include <Diluculum/LuaState.hpp>
//..
Diluculum::LuaState ls;
ls.doFile("config.lua");

Теперь мы можем использовать operator[] для получения переменных, а затем и их значений.

const std::string favColor = ls["FavoriteColor"].value().asString();
const double favApprox = ls["FavoritePiApproximation"].value().asNumber();
const std::string userName = ls["UserInfo"]["Name"].value().asString();
const int userAge = static_cast<int>(ls["UserInfo"]["Age"].value().asNumber());
const bool userLikesLargeIcons = ls["UserInfo"]["LikesLargeIcons"].asBoolean();
const int winWidth = static_cast<int>(ls["WindowSize"][1].value().asNumber());
const int winHeight = static_cast<int>(ls["WindowSize"][2].value().asNumber());

Вызовы функций value(), asNumber() и asString() выглядят немного громоздко, но это совсем не является недостатком (не забудем, что переменные в Lua не являются типизированными, так что из одной и той же переменной можно получить как, скажем, строку, так и число, если преобразование позволяет {на самом деле типы есть, просто они спрятаны от пользователя [вспоминаем VARIANT]}). Перегружены все операторы для основных типов С++, поддерживаемых Lua, чтобы пользователи библиотеки могли писать нечто подобное.

if (ls["UserInfo"]["LikesLargeIcons"])
{
    //...
}
if (ls["FavoriteColor"] != "blue")

{

   cout << "You have bad taste for colors" << endl;
}

Вы могли заметить, чтов файле config.lua была определена функция. Она может быть вызвана из С++ без особых затруднений. Однако есть один важный момент. Функции в Lua могут возвращать произвольное количество значений. Поэтому возвращаемым значением будет объект класса Diluculum::LuaValueList. Код, приведенный ниже, показывает как вызывается Lua-функция из С++.

Diluculum::LuaValueList ret = ls["EmphasizeFunc"]("String to be emphasized");
cout << ret[0].asString() << endl;

Заметьте, что никакого вызова функции value() здесь не требуется. Вызовы Lua-функции из С++ уже возвращают значения (список объектов класса Diluculum::LuaValue), а не переменные (Diluculum::LuaVariable), из которых еще нужно получать значения (как следствие, нетипизированное использование значений в этом случае повлечет LuaException).

Перегрузки оператора operator[], которые определены и используются в библиотеке, позволяют записывать значения в Lua-переменные (имеется доступ на запить в Lua State). Можно определять новые переменные из кода на С++. Эта функциональность, конечно, не будет полезной, когда Lua используется, как конфигурационный язык, но все таки это иногда бывает полезным. Код, приведенный ниже, показывает как это делается.

ls["newNumber"] = -123.456;
ls["newString"] = "Ahhh!";
ls["FavoritePiApproximation"] = 3.14159265;

Обертки для С++ функций

Прежде всего, если так случилось что у нас есть lua_CFunction (Lua-функция, как она определяется в Lua API для С), мы можем попросту присвоить объекту Diluculum::LuaState это значение, как показывается ниже.

int aLuaCFunction(lua_State * ls)
{
    //...
}
//...
// Create a Lua state and register the function there
Diluculum::LuaState ls;
ls["Func"] = aLuaCFunction;
//Call the function from Lua
ls.doString("Func(1, 2, 3, 'four')");

Присваивание значений объекту типа lua_CFunction может вызвать нарушения доступа с использованием lua_State*, и может быть не совсем тем, что Вам необходимо (я бы сказал, что подобное использование противоречит принципу инкапсуляции ООП). Поэтому Diluculum позволяет создавать lua_CFunction автоматически, если ей подготовить функцию на С++, принимающую в качестве параметра и возвращающую в качестве значения объект класса Diluculum::LuaValueList (инкапсуляция списка Lua-значений). Затем такая функция должна быть зарегистрирована в объекте Diluculum::LuaState, как показано ниже.

#include <Diluculum/LuaWrappers.hpp>
using namespace Diluculum;
//...
/* In Lua, this will take one number parameter and return three numbers, which are equal the parameter, two times the parameter and three times the parameter. Not very useful, uh? */
LuaVauleList MyFunction(const LuaValueList & params)
{
    if ( (params.size() != 1) || (params[0].type() != LUA_TNUMBER) )
        throw LuaError("Bad parameters");
    LuaValueList ret;
    double dVal = params[0].asNumber();
    ret.push_back(dVal);
    ret.push_back(dVal * 2);
    ret.push_back(dVal * 3);
    return ret;
}
// Create a 'lua_CFunction'
DILUCULUM_WRAP_FUNCTION(MyFunction);
//...
// Create Lua State and register the function.
LuaState ls;
ls["MyFunction"] = DILUCULUM_WRAPPER_FUNCTION(MyFunction);
// Now 'MyFunction' can be called from Lua
ls.doString("a, b, c = MyFunction(4.5)");

Тут есть один важный момент в том, что выбрасывание Diluculum::LuaError является корректным (я бы сказал, единственно корректным) способом сообщить об ошибочной ситуации наружу из обернутой таким способом функции. Для тех, кто уже знает Lua C API, сообщаем что это исключение обрабатывается и преобразуется в вызов lua_error(). (Тем, кто не знает Lua C API, можно особо не волноваться. Более гадкое, чем Lua C API, лично я встречаю редко [тьфу-тьфу-тьфу, по дереву постучать...]. Этим как раз и объясняется, что все более менее здоровые люди используют Lua через подобные библиотеки).

Обертки для классов и объектов С++

Diluculum имеет ограниценную поддержку для обертывания классов С++ и их объектов в Lua-совместимые, регистрируя их в Lua State соответствующим образом. Поддержка названа ограниченной, т.к. не поддерживается перенос наследования, например. Т.е. нет переноса объектной ориентированности, грубо говоря. Конструкции могут показаться громоздкими. Однако ж они могут пригодиться в хозяйстве.

Diluculum не может справиться с произвольным классом. Как и в случае с функциями, классы надо подготовить. Оборачиваемые классы должны подчиняться нескольким правилам для того, чтобы быть использованными через Diluculum. Во-первых, должен существовать конструктор копирования, принимающий Diluculum::LuaValueList. Во-вторых, методы которые экспортируются в Lua, должны принимать Diluculum::LuaValueList в качестве параметра, а также возвращать объект того же типа. Ниже показан пример корректно обернутого класса.

 // A class that stores one value. Duh.
class ValueBox
{
    private:
        Diluculum::LuaValue value_;
    public:
        // The constructor taking a 'Diluculum::LuaValueList' parameter.
        ValueBox( const Diluculum::LuaValueList & params)
        {
            if (params.size() > 0)
                value_ = params[0];
        }
        /* Stores the value passed as parameter in the box and returns the value previously stored there */
        Diluculum::LuaValueList swap(const Diluculum::LuaValueList & params)
        {
            if (params.size() != 1)
                throw Diluculum::LuaError("Exactly one parameter was expected.");
            Diluculum::LuaValueList ret;
            ret.push_back(value_);
            value_ = params[0];
            return ret;
        }
};

Как и в случае обернутых функций, единственным правильным способом донести об ошибочной ситуации является выброс Diluculum::LuaError. Класс экспортируется в Lua через использование макросов, подставляющих необходимый код на Lua C API и определяющих некоторые структуры с данными. (Для читателей, интересующихся механизмом преобразования: библиотека создает глобальный объект Diluculum::LuaValueMap, представляющий класс и являющийся метатаблицей для объектов этого класса в Lua). Вот как выглядит регистрация класса на С++.

DILUCULUM_BEGIN_CLASS(ValueBox);
    DILUCULUM_CLASS_METHOD(ValueBox, swap);
DILUCULUM_END_CLASS(ValueBox);

Имеется только одно использование макроса DILUCULUM_CLASS_METHOD(), т.к. в классе, что описан выше, имеется только один экспортируемый в Lua метод. Разумеется, можно добавлять произвольное количество методов. Теперь мы готовы к регистраци этого класса в Lua State, чтобы потом его использовать (это необходимо, иначе Lua "не увидит" Вашего класса, будет писать о синтаксических ошибках). Для этого требуется использовать еще один макрос. После этого объекты обернутого класса могут создаваться из Lua-кода. Код ниже демонстрирует сказанное.

Diluculum::LuaState ls;
DILUCULUM_REGISTER_CLASS(ls["ValueBox"], ValueBox);
ls.doString("box = ValueBox.new(3)");
ls.doString("print(box:swap('foo'))");       // prints '3'
ls.doString("print(box:swap(789.987))"); // prints 'foo'

Управление памятью

В Lua имеется сборщик мусора, подобно .NET и Java. Когда объект С++, созданный из Lua, очищается из памяти сборщиком, вызывается его деструктор С++. Таким образом, ресурсы корректно освобождаются (конечно же, в предположении, что программист правильно написал деструктор). Тем не менее, имеется возможность заставить Lua вызвать деструктор явно, чтобы самому управлять процессом очистки. Ниже показывается соответствующий код.

Diluculum::LuaState ls;
DILUCULUM_REGISTER_CLASS(ls["ValueBox"], ValueBox);
ls.doString("box = ValueBox.new()");
// ... use 'box' ...
ls.doString("box:delete()");

Как раз время обсудить то, когда лучше самому заниматься очитсткой памяти. Проблема в том, что Lua не знает, сколько именно памяти занимает объект обернутого класса. Для каждого объекта, созданного с использованием возможностей Diluculum, Lua располагает лишь малой частью памяти (для соответствующего указателя и некоторых флагов). Если объект сам будет требовать выделения памяти (например, по ходу работы с ним), Lua не будет знать об этом, и сборщик мусора не будет знать как правильно и сколько именно очищать. Итак, если Вы создаете некий подобный большой объект, Вам лучше освобождать его вручную из Lua, когда он Вам больше не нужен. Аналогично, если Вы создаете много объектов, лучше освобождать их вручную.

Регистрация объектов

Иногда требуется создать объект в С++ и сделать его доступным в Lua State. Другими словами, некто может создать объект в С++ и вызывать его методы из Lua. Это можно сделать с помощью Diluculum, используя еще один макрос, как показано ниже.

Diluculum::LuaState ls;
Diluculum::LuaValueList params;
ValueBox vb(params);
DILUCULUM_REGISTER_OBJECT(ls["box"], ValueBox, vb);
ls.doString("box:swap(123)");
cout << vb.swap("abc") << endl; // prints '123'

Заметим, что когда объект "box" обрабатывается сборщиком мусора, соответствующий объект С++ не будет удаляться (здесь он стековый). Удаление объекта из памяти является зоной ответственности программиста С++. (Другими словами, регистрация объектов только привязывает Lua-переменные с объетками С++ на время).

Динамически загружаемые модули

Начиная с версии 0.4, Diluculum имеет возможности для создания динамически загружаемых Lua-модулей. Это модули, компилируемые в DLL и автоматически загружаемые по Lua-вызовам, как ниже.

require "MyModule"

На самом деле Diluculum проделывает совсем немного работы в этом плане, несильно упрощая жизнь программисту, если бы он использовал само Lua C API. В любом случае, т.к. эта документация для пользователей, то предполагается демонстрация того, как это можно использовать. Итак, поехали.

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

DILUCULUM_BEGIN_MODULE(MyFineModule);
    DILUCULUM_MODULE_ADD_CLASS(ValueBox, "ValueBox");
    DILUCULUM_MODULE_ADD_FUNCTION(DILUCULUM_WRAPPER_FUNCTION(MyFunction), "MyFunction");
DILUCULUM_END_MODULE();

Так, если все это было скомпилировано в правильно названную DLL (см. Lua's Reference Manual чтобы знать, что образует правильное имя модуля). Программист на Lua может написать нечто подобное.

require "MyFineModule"
local v = MyFineModule.ValueBox.new(4321)
v.swap("Weeee!")
local x, y, z = MyFineModule.MyFunction(4.567)
Published Nov 16 2007, 11:06 PM by dmitryshm
Теги: , ,

Комментарии

 

doctorsolberg сказал:

Где ж собственно документ?

November 20, 2007 8:08 AM
 

dmitryshm сказал:

Что-то перепутал я тогда. Вот еще проблема, возникшая при загрузке файла на сервер. "Only files with the following extensions are allowed: zip,gif,jpg,jpeg,png,bmp,txt,xml. Please select a valid file." Хочется еще как минимум 3 расширения файлов: xls, doc, pdf

November 21, 2007 8:45 AM
 

doctorsolberg сказал:

Добавил

November 21, 2007 9:33 AM
 

Dennis сказал:

Спасибо, Дима, за перевод. Но что-то убого смотрится этот Diluculum... Не в С++-стиле, в отличие от luabind. Спрашивается, что ж в этом Diluculum такого хорошего, чтобы предпочесть его luabind-у?

March 2, 2008 12:05 PM
 

dmitryshm сказал:

Когда я искал lua-адаптеры, то, конечно, просмотрел luabind. Помнится он не поддерживал lua 5.1. Может сейчас поддерживает, не знаю. Дело было год назад.

Про убогость Diluculum не могу ничего сказать. По-моему лишнего там нет. Diluculum хорошо представляет lua в С++, отражает его нетипизированность и ориентированность на таблицы. Позволяет работать с сессиями lua, управлять ими.

Выбрал Diluculum и работаю с ним. Очень хорошая библиотека на мой взгляд.

Я забыл многое что есть в luabind. Там можно принимать несколько значений из lua-функции? А экспортировать такую фукнцию в lua?

Просто у меня еще тогда сложилось впечатление, что luabind своей ориентированностью на С++ забывает про многие фичи lua, которые просто не переносятся на С++.

March 13, 2008 4:58 AM

Оставить комментарий

(required)  
(optional)
(required)  

About dmitryshm

Ведущий программист в студии FunZai. Среди интересов --- парадигмы современного программирования, новые технологии и инновации, фреймворки и библиотеки. Программирую под Windows. Пишу, в основном, на С++ и C#.NET
© 2007 Kazan Developers Community and Post`s Authors