Lua API++

    Здравствуйте, коллеги.
    Хочу познакомить вас с моим небольшим проектом, который, надеюсь, сможет пригодиться и вам.

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

    Не могу сказать, что я недоволен Lua API: это отличный набор функций, удобный и простой в использовании. Интеграция языка в своё приложение и добавление собственных расширений не вызвали трудностей, никаких «подводных камней» тоже не возникло. Но всё же при использовании этого API, ориентированного на Си, меня не оставляла мысль, что этот процесс мог бы быть и поудобнее. Первая попытка сделать удобную объектно-ориентированную обёртку потерпела неудачу: имеющимися средствами мне не удалось создать что-то заслуживающее существования, всё выходило чересчур громоздко и неочевидно.

    А потом появился C++11, который снял все мешавшие мне препятствия (точнее говоря — добавил то, чего не хватало), и головоломка постепенно начала складываться. Второй заход оказался удачным, и в результате я сумел создать достаточно легковесную библиотеку-обёртку с естественным синтаксисом большинства операций. Эта библиотека, которую я назвал Lua API++, призвана служить удобной заменой для Lua API. Этой статья, написанная по мотивам моего выступления на Lua Workshop, поможет познакомиться с основными понятиями Lua API++ и предоставляемыми ей возможностями.



    Основные действующие лица


    Знакомство следует начинать с основных понятий, используемых библиотекой, и взаимоотношений между ними. Как и следовало ожидать, эти понятия отражены в соответствующих типах.

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

    LFunction
    Всё остальное в библиотеке происходит внутри LFunction, функций специального формата, совместимых с Lua API++. Это аналог Lua-совместимых функций, которым в свою очередь было дано название CFunction. Особый формат функции понадобился в основном для того, чтобы впустить нашего следующего персонажа:

    Context
    Context — это контекст функции, а также центр доступа ко всем возможностям Lua. С его помощью можно получить доступ к глобальным переменным, аргументам функции, реестру и upvalues. Можно управлять сборщиком мусора, сигнализировать об ошибках, передавать множественные возвращаемые значения и создавать замыкания. Проще говоря, через Context делается всё то, что не относится непосредственно к операциям над значениями, которые являются прерогативой нашего замыкающего:

    Value
    В отличие от предыдущих понятий, которым однозначно соответствовал одноимённый класс, «значение» в Lua API++ несколько расплывчато (хотя класс Value, конечно же, есть). В первую очередь это связано с политикой «открытых границ», которая позволяет свободную миграцию нативных значений в Lua и наоборот. Везде, где ожидаются значения Lua, можно подставлять нативные значения поддерживаемых типов и они автоматически «переедут» на стек Lua. Операторы неявного преобразования типов помогут переезду значения в обратном направлении, а в случае несовместимости реального и ожидаемого типа известят нас об этом при помощи исключения.
    Кроме этого, значения в Lua в зависимости от их происхождения могут быть представлены разными типами, поддерживающими общий интерфейс. Этот интерфейс реализует все допустимые операции над значениями: явное и неявное преобразование в нативные типы, вызовы функций, индексацию, арифметические операции, сравнение, проверку типов, запись и чтение метатаблиц.

    Valref
    Это ссылка на размещёное на стеке значение, а если точнее — то даже не столько на значение, сколько на конкретный слот на стеке Lua. Valref не занимается размещением или удалением значений на стеке, а сосредоточен исключительно на операциях над значением. В документации к Lua API++ Valref служит образцом, которому следует интерфейс других типов, представляющих значения.

    Temporary
    С временными значениями, которые являются результатом операций, несколько сложнее. Это значения, которые будут помещены (а может, и не будут) на стек в результате операции, использованы единожды, а затем удалены. К тому же, аргументы операции сами по себе могут быть результатами других операций, да ещё и без гарантий успеха. Да и использование бывает разное: при индексации в результате чтения на стеке создаётся новое значение взамен ключа, а в результате записи — со стека удаляются ключ и записанное значение. А как насчёт необходимости строго соблюдать очерёдность размещения аргументов операций? И что делать с неиспользованными объектами?
    Многие, вероятно, уже догадались, к чему я клоню. Временные значения представлены proxy-типами. Они незримо для пользователя конструируются при помощи шаблонов и воспроизводят интерфейс Valref. Пользоваться ими легко, просто и удобно, но допустите ошибку, и компилятор «порадует» вас объёмистым сочинением, изобилующим угловыми скобками.

    Якори
    Якори названы так потому, что позволяют «прикнопить» к стеку одно или несколько значений. Value — универсальный «якорь» для одного значения, Table специализирован для таблиц, а Valset хранит несколько значений.



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

    State

    У State есть конструктор по умолчанию, выполняющий все необходимые для инициализации контекста действия. Альтернативный конструктор позволяет задействовать пользовательскую функцию управления памятью. Можно запросить «сырой» указатель на объект состояния, используемый в Lua API, функцией getRawState.
    В комплекте идут функции runFile, runString и call, которые позволяют смастерить простейший интерпретатор:

    Простейший интерпретатор
    #include <iostream>
    #include <luapp/lua.hpp>
    using namespace std;
    using namespace lua;
    
    void interpretLine(State& state, const string& line)
    {
      try {
        state.runString(line);  // Пытаемся выполнить строку
      } catch(std::exception& e) { // О неудаче и её причинах нам сообщат в исключении
        cerr << e.what() << endl;
      }
    }
    
    void interpretStream(State& state, istream& in)
    {
      string currentLine;
      while(!in.eof()) {  // Читаем поток по строкам и каждую интерпретируем
        getline(in, currentLine);
        interpretLine(state, currentLine);
      }
    }
    
    int main()
    {
      State state;
      interpretStream(state, cin);
    }




    Обработка ошибок


    Подход, используемый библиотекой, заключается в том, чтобы не путаться под ногами у Lua, поэтому диагностируются либо те ошибки, которые связаны с работой самой библиотеки, вроде попыток создать Table не из таблицы, либо те, которые понадобится (возможно) перехватывать в пользовательском коде, вроде ошибок приведения типов. Библиотека не пытается диагностировать заранее те ошибки, которые могут обнаружиться при вызове Lua API. Поэтому попытка, например, использовать вызов функции на значении, которое на самом деле является числом, не вызовет исключения. Она будет обнаружена внутри вызова lua_call и вызовет ошибку в стиле Lua (прерывание выполнения и возврат к ближайшей точке защищённого вызова).



    LFunction


    Вообще-то библиотека поддерживает «прозрачную» обёртку функций, оперирующих поддерживаемыми типами (и даже функций-членов). Достаточно просто упомянуть имя функции там, где ожидается Lua-значение. Но если мы хотим получить доступ ко всем удобствам Lua, предоставляемым Lua API++, надо писать L-функции в соответствии с таким прототипом:
    Retval myFunc(Context& c);

    Здесь всё просто: наша функция получает Context, а Retval — специальный тип, помогающий с удобством возвращать произвольное количество значений через функцию Context::ret.

    Шаблон mkcf позволяет сделать из LFunction то, с чем подружится Lua:
    int (*myCfunc)(lua_State*) = mkcf<myFunc>;

    Таким образом мы можем явно создавать обёртки для нашей функции. «Прозрачная» обёртка тоже сработает, но накладные расходы будут чуть выше. С другой стороны, mkcf будет создавать отдельную функцию-обёртку в каждом случае.
    Так или иначе, но в любом случае «обёртка» будет создавать объект Context, передавать его нашей функции, а по завершении работы передавать в Lua возвращаемые через Retval значения. Все исключения, вышедшие за пределы оборачиваемой функции, будут перехвачены и преобразованы в ошибку Lua.
    Функция, возвращающая себя саму? Дайте две!
    Retval retSelf(Context& c)
    {
      return c.ret(retSelf, mkcf<retSelf>);  // Вернём сразу две функции, созданные по-разному
    }




    Context


    Контекст функции — это центральный узел доступа к Lua. Всё, что не связано непосредственно с работой со значениями, выполняется через Context. Не стану отвергать намёки на явное сходство с god object, но в данном случае такое решение диктуется архитектурой Lua API. Через Context можно управлять сборщиком мусора, можно узнать номер версии и количество размещённых на стеке значений. Он неявно преобразуется в lua_State* на случай, если надо поколдовать с Lua API напрямую. На этот же случай предусмотрено волшебное слово (точнее, статическая константа сигнального типа) initializeExplicitly, позволяющее создавать Context явно, за пределами LFunction.

    Возврат значений
    Как бы ни было приятно просто указывать в операторе return возвращаемые из функции значения, это невозможно. Пришлось сделать выбор между двумя ближайшими альтернативами: хитрый «стартер» с перегрузкой оператора запятой или вызов функции. Победила дружба функция. Поэтому LFunction и требует возвращать Retval, создать который можно лишь обращением к методу Context со скромным названием ret. Это особенная функция: после её вызова прекращается работа со стеком, чтобы не скинуть с него наши значения, поэтому употреблять её следует лишь непосредственно в операторе return. В вызове ret можно перечислить столько возвращаемых значений, сколько понадобится.
    Сравнение
    return ctx.ret(1, "two", three);

    Эквивалентный код:
    lua_pushinteger(ctx, 1);
    lua_pushstring(ctx, "two");
    lua_pushvalue(ctx, three);
    return 3;
    



    Сигнализация об ошибках
    Утверждая, что единственный способ создать Retval — обратиться к функции ret, я не погрешил против истины, но есть один нюанс… С формальной точки зрения имеется ещё и функция error, которая тоже возвращает этот тип. Только на самом деле до создания Retval не доходит, потому что из этой функции не происходит возврата. Максимум, на который можно рассчитывать — передать своё сообщение механизму обработки ошибок Lua. Документация по Lua API рекомендует применять вызов lua_error в операторе return, чтобы обозначить тот факт, что исполнение функции прерывается во время вызова. Тот же самый подход применяется и в Lua API++, потому-то error и объявлена возвращающей Retval.
    В качестве аргумента принимается Lua-значение с сообщением об ошибке, и конкатенация тут будет вполне уместна, тем более что зачинателем может выступить функция where, создающая строку, описывающую текущую функцию. Это же значение используется, если сообщение вообще не указывать.
    if(verbose)
      return ctx.error(ctx.where() & " intentional error " & 42);
    else
      return ctx.error();  // То же самое, что return ctx.error(ctx.where());
    

    Эквивалентный код
    if(verbose) {
        luaL_where(ctx, 0);
        lua_pushstring(ctx, " intentional error ");
        lua_pushinteger(ctx, 42);
        lua_concat(ctx, 3);
        return lua_error(ctx);
    } else {
        luaL_where(ctx, 0);
        return lua_error(ctx);
    }
    


    Доступ к окружению
    Наш Context, очевидно, является первоисточником значений. В самом деле, откуда ещё им взяться?
    Нам в пользование предоставляются объекты доступа, оформленные как открытые члены класса Context, позволяющие дотянуться до разных интересных мест окружения. Все они позволяют осуществлять как чтение, так и запись значений.

    В первую очередь это args, аргументы функции. В отличие от других объектов доступа, для каждого из которых создавался специальный недоступный для пользователя тип, здесь применяется обычный константный Valset. Его константность означает лишь то, что мы не можем изменять его размер, а вот переписывать значения аргументов — на здоровье. Поскольку Valset создавался как STL-совместимый контейнер, нумерация элементов в нём начинается с 0. В остальных случаях библиотека придерживается правил Lua и подразумевает, что индексация начинается с 1.
    if(ctx.args.size() > 1 && ctx.args[0].is<string>()) {...};
    

    Эквивалентный код
        if(nArgs > 1 && lua_isstring(ctx, 1) ) {...};
    

    На втором месте стоит доступ к глобальным переменным. Объект global индексируется строками.
    ctx.global["answer"] = 42;  // Если такой глобальной переменной не было, теперь появится

    Эквивалентный код
    lua_pushinteger(ctx, 42);
    lua_setglobal(ctx, "answer");
    

    Если наша LFunction по совместительству является замыканием, то к хранящимся в ней значениям мы можем обратиться через upvalues с целочисленным индексом (начиная с 1, всё правильно). Способа узнать количество хранимых значений нет: предполагается, что это и так известно.

    Реестр Lua, доступный через registry, используется двумя способами. По строковым ключам там хранятся метатаблицы для пользовательских данных. Целочисленные ключи задействуются при использовании реестра в качестве хранилища значений. Ключ создаётся вызовом registry.store и в дальнейшем применяется для чтения и записи в registry, стирание значения и освобождение ключа происходит при записи nil.
    auto k = ctx.registry.store(ctx.upvalues[1]);  // decltype(k) == int
    ctx.registry [k] = nil;  // Ключ k освободился и может снова стать результатом store
    

    Эквивалентный код
    lua_pushvalue(ctx, lua_upvalueindex(1));
    auto k = luaL_ref(ctx, LUA_REGISTRYINDEX);
    luaL_unref(ctx, LUA_REGISTRYINDEX, k);
    

    Функции
    Я только что упомянул о том, что Lua позволяет создавать замыкания. В объекте Context для этого применяется функция closure, которая получает CFunction и те значения, которые будут храниться в замыкании. Результат — временный объект, то есть полноценное Lua-значение.
    Вместо CFunction мы можем указать сразу LFunction, но у этой лёгкости есть своя цена. В получившемся замыкании будет зарезервировано первое upvalue (там хранится адрес функции, поскольку обёртка одна и та же для любой LFunction). Эта же функция применяется и для прозрачной миграции LFunction с теми же последствиями. В этом состоит отличие от шаблона mkcf, который ничего не резервирует, но зато создаёт отдельную функцию-обёртку для каждой функции.

    А ещё можно создавать чанки: скомпилированный код Lua. Непосредственно текст компилируется методом chunk, а содержимое файла при помощи load. Для случаев «выполнил и забыл» есть runString и runFile, точно такие же, как и в State. С точки зрения использования чанк — обычная функция.

    Замыкания можно создавать и из несовместимых функций при помощи метода wrap. Он автоматически создаёт обёртку, которая берёт аргументы со стека Lua, преобразует их в значения, принимаемые нашей функцией, производит вызов и размещает результат на стеке Lua в качестве возвращаемого значения. По умолчанию это работает со всеми поддерживаемыми типами, включая пользовательские данные. А если этого мало (например, нам надо вытворять что-то со строками, хранящимися в vector, то можно и самому указать преобразование в ту или другую сторону при помощи специальных макросов.
    Именно wrap срабатывает при неявной миграции функций. Братский метод vwrap делает почти всё то же самое, только игнорирует возвращаемое оборачиваемой функцией значение.



    Миграция значений


    Lua API++ поддерживает следующие нативные типы:
    Числовые
    int
    unsigned int
    long long
    unsigned long long
    float
    double
    Строковые
    const char*
    std::string
    Функции
    CFunction: int (*) (lua_State*)
    LFunction: Retval (*) (Context&)
    Произвольные функции
    Функции-члены
    Разное
    Nil
    bool
    LightUserData: void*
    зарегистрированные пользовательские типы

    Значения перечисленных в таблице типов могут мигрировать на стек Lua и обратно (за исключением, естественно, Nil и «обёрнутых» функций, которые остаются указателями на обёртки).
    Обратная миграция осуществляется при помощи встроенных в Value-типы операторов неявного преобразования и при помощи шаблонной функции cast.Если в Lua-значении содержатся данные, которые невозможно преобразовать в то, во что мы пытаемся, будет выброшено исключение. Функция optcast вместо исключений вернёт «запасное» значение.
    int a = val;
    auto b = val.cast<int>();
    auto c = val.optcast<int>(42);
    

    Эквивалентный код
    if(!lua_isnumber(ctx, valIdx)){
        lua_pushstring(ctx, "Conversion error");
        return lua_error(ctx);
    }
    int a = lua_tointeger(ctx, valIdx);
    if(!lua_isnumber(ctx, valIdx)){
        lua_pushstring(ctx, "Conversion error");
        return lua_error(ctx);
    }
    auto b = lua_tointeger(ctx, valIdx);
    auto c = lua_isnumber(ctx, valIdx) ? lua_tointeger(ctx, valIdx) : 42;
    


    Проверить совместимость с нужным типом можно функцией is, а при помощи type — узнать тип хранимого значения непосредственно.
    if(val.is<double>()) ...;
    if(val.type() == ValueTypes::Number) ...;
    




    Операции с одиночным значением


    Присваивание
    Если у нас есть Value, то в общем случае ему можно что-нибудь присвоить, как другие Value, так и нативные. Но это не относится к некоторым временным значениям, например, к результату вызова функции или длине: оказавшись по левую сторону от знака =, они выдадут мудрёную ошибку. А вот другие временные значения, вроде индексации или метатаблицы, присваивание вполне допускают. По смыслу выполняемого действия несложно догадаться, чему присваивать можно, а чему нельзя.

    Метатаблицы
    Метод mt даёт доступ к метатаблице значения, которую можно читать и записывать.
    {
      Table mt = val.mt();
      val.mt() = nil;
    }
    

    Эквивалентный код
    if(!lua_getmetatable(ctx, valIdx)){
        lua_pushstring(ctx, "The value has no metatable");
        return lua_error(ctx);
    }
    int mtIdx = lua_gettop(ctx);
    lua_pushnil(ctx);
    lua_setmetatable(ctx, valIdx);
    lua_pop(ctx, 1);
    


    Длина
    Работа функции len различается в разных версиях Lua: в режиме совместимости с 5.1 она возвращает нативный size_t, а в режиме 5.2 временное значение.

    Индексация
    Доступ к элементам таблицы по ключу осуществляется индексированием, ключ может иметь любой поддерживаемый тип. Но надо помнить о том, что при оборачивании функций будут создаваться новые замыкания:
    void myFunc();
    Retval example(Context& c)
    {
        Table t(c);
        t[myFunc] = 42;  // Здесь myFunc порождает одно замыкание...
        assert(t[myFunc].is<Nil>());  // а здесь - уже второе, не равное первому.
        t[mkcf<example>] = 42.42;  // А это у нас CFunction, она идёт "налегке"
        assert(t[mkcf<example>] == 42.42);
    }

    Эквивалентный код
    void myFunc();
    
    int wrapped_void_void(lua_State* s)
    {
        if(!lua_islightuserdata(ctx, lua_upvalueindex(1))) {
            lua_pushstring(ctx, "Conversion error");
            return lua_error(ctx);
        }
        void (*fptr) () = reinterpret_cast<void(*)()>(lua_touserdata(ctx, lua_upvalueindex(1)));
        fptr();
        return 0;
    }
    
    int mkcf_myFunc(lua_State* s)
    {
        myFunc();
        return 0;
    }
    
    int example(lua_State* ctx)
    {
        lua_createtable(ctx, 0, 0);
        int t = lua_gettop(ctx);
        lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&myFunc));
        lua_pushcclosure(ctx, &wrapped_void_void, 1);
        lua_pushinteger(ctx, 42);
        lua_settable(ctx, t);
        lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&myFunc));
        lua_pushcclosure(ctx, &wrapped_void_void, 1);
        lua_gettable(ctx, t);
        assert(lua_isnil(ctx, -1));
        lua_pushcfunction(ctx, &mkcf_myFunc);
        lua_pushnumber(ctx, 42.42);
        lua_settable(ctx, t);
        lua_pushcfunction(ctx, &mkcf_myFunc);
        lua_gettable(ctx, t);
        lua_pushnumber(ctx, 42.42);
        int result = lua_compare(ctx, -1, -2, LUA_OPEQ);
        lua_pop(ctx, 2);
        assert(result == 1);
    }
    


    Вызов функций
    Вызов Lua-функций осуществляется обычными круглыми скобками. Есть метод call для явного вызова, на тот случай, если надо как-то повыразительнее. Защищённый вызов pcall предохраняет от ошибок.
    int x = fn(1);
    int y = fn.call(1);  // То же самое
    int z = fn.pcall(1); // А тут лучше было проверить, возникла ли ошибка
    

    Множественные возвращаемые значения
    Мы только что видели, что результат вызова можно смело использовать как обычное значение. Но как быть с функциями, которые возвращают сразу несколько значений? Возьмём для примера такую функцию на Lua:
    function mrv() return 2, 3, 4; end
    

    У нас есть сразу несколько вариантов.
    Во-первых, можно вообще игнорировать результаты вызова, никак не используя выражение вызова. Все возвращённые значения будут просто выкинуты.
    mrv();
    

    Эквивалентный код
    lua_pushvalue(ctx, mrvIdx);
    lua_call(ctx, 0, 0);
    

    Во-вторых, можно использовать выражение вызова в качестве обычного Lua-значения. Тогда будет использовано только первое возвращённое значение (или nil, если функция вообще ничего не вернула), а остальные опять же будут выкинуты.
    Value x = mrv(); // x == 2
    

    Эквивалентный код
    lua_pushvalue(ctx, mrvIdx);
    lua_call(ctx, 0, 1);
    int x = lua_gettop(ctx);
    

    В-третьих, в контексте, подразумевающем последовательность значений (например, параметры функции) выражение вызова подвергнется раскрытию: возвращённые значения станут частью последовательности.
    print(1, mrv(), 5);  // Напечатает 1 2 3 4 5
    

    Эквивалентный код
    lua_pushvalue(ctx, printIdx);
    int oldtop = lua_gettop(ctx);
    lua_pushinteger(ctx, 1);
    lua_pushvalue(ctx, mrvIdx);
    lua_call(ctx, 0, LUA_MULTRET);
    lua_pushinteger(ctx, 5);
    int nArgs = lua_gettop(ctx) - oldtop;
    lua_call(ctx, nArgs, 0);
    

    В-четвёртых, можно просто сохранить всё в Valset, придуманный специально для этого.
    Valset vs = mrv.pcall();  // vs.size() == 3, vs.success() == true
    

    Эквивалентный код
    int vsBegin = lua_gettop(ctx) + 1;
    lua_pushvalue(ctx, mrvIdx);
    bool vsSuccess = lua_pcall(ctx, 0, LUA_MULTRET) == LUA_OK;
    int vsSize = lua_gettop(ctx) + 1 - vsBegin;
    

    Valset запомнит и то, был ли защищённый вызов удачным (это единственный способ получить эту информацию). В случае неудачи он будет содержать сообщение об ошибке. Что приятно, Valset способен раскрываться в списке значений точно так же, как и выражение вызова.
    print(1, vs, 5);  // Напечатает 1 2 3 4 5
    

    Эквивалентный код
    lua_pushvalue(ctx, printIdx);
    int oldTop = lua_gettop(ctx);
    lua_pushInteger(ctx, 1);
    lua_pushvalue(ctx, mrvIdx);
    for(auto i = 0; i < vsSize; ++i)
        lua_pushvalue(ctx, vsBegin + i);
    lua_pushinteger(ctx, 5);
    int nArgs = lua_gettop(ctx) - oldtop;
    lua_call(ctx, nArgs, 0);
    

    Однако этим польза Valset не исчерпывается. Он является STL-подобным контейнерам и к нему можно применять алгоритмы STL, а в качестве «хранимых» значений выступают Valref. Если Valset расположен на стеке последним, то к нему применимы операции добавления и удаления значений push_back и pop_back. Обычно при использовании целикового Valref происходит дублирование содержащихся значений, но если мы возвращаем его из функции (но только один Valset), то его содержимое используется непосредственно. Так можно накапливать возвращаемые значения, количество которых становится известно во время выполнения.



    Операции с двумя значениями


    Для выполнения бинарных операций достаточно, чтобы Value-тип присутствовал хотя бы с одной стороны, что позволяет вовлекать в преобразования нативные значения по принципу домино:
    string s = "The answer to question " & val & " is " & 42;
    

    Эквивалентный код
    lua_pushstring(ctx, "The answer to question ");
    lua_pushvalue(ctx, valIdx);
    lua_pushstring(ctx, " is ");
    lua_pushinteger(ctx, 42);
    lua_concat(ctx, 4);
    string s = lua_tostring(ctx, -1);
    lua_pop(ctx, 1);
    

    Знаком & обозначается конкатенация. Цепные конкатенации оптимизируются в единый вызов, «склеивающий» сразу несколько значений. Заодно конкатенация является одним из тех мест, в которых раскрываются выражения вызова и Valset.

    Сравнения тоже выполняются через Lua, но производят нативные булевские значения.

    В версии 5.2 доступна и арифметика, включая возведение в степень, под которую был «угнан» символ ^ вместе со своим низким приоритетом.



    Таблицы


    У таблиц, представляемых типом Table, интерфейс по сравнению с Valref несколько урезан. Оставлена индексация, проверка длины, метатаблицы, но убраны не относящиеся к таблицам операции вроде вызова функции. Взамен имеется объект доступа raw, осуществляющий прямой доступ к данным, без задействования метатаблиц, а также функция iterate для перебора содержимого таблицы, аналог for_each. Прямой доступ выглядит как обычная индексация и ничем особо не примечателен, а вот iterate принимает функцию (точнее говоря, сойдёт что угодно, лишь бы вело себя как функция), применяемую к парам ключ-значение. Эта функция получает ключ и значение в виде Valref и возвращает true, чтобы продолжить перебор и false, чтобы остановить. А можно ничего не возвращать и просто пройтись по всему содержимому:
    Table t = ctx.global["myTable"];
    t.iterate([] (Valref k, Valref v)
    {
        cout << int(k) << int(v);
    });
    

    Эквивалентный код
    lua_getglobal(ctx, "myTable");
    if(!lua_istable(ctx, -1)){
        lua_pushstring(ctx, "Conversion error");
        return lua_error(ctx);
    }
    int t = lua_gettop(ctx);
    lua_pushnil(ctx);
    size_t visited = 0;
    while(lua_next(ctx, t)) {
        ++ visited;
        if(!lua_isnumber(ctx, -2) || !lua_isnumber(ctx, -1)){
            lua_pushstring(ctx, "Conversion error");
            return lua_error(ctx);
        }
        cout << lua_tointeger(ctx, -2) << lua_tointeger(ctx, -1);
        lua_pop(ctx, 1);
    }
    

    Результатом iterate будет количество обработанных записей.

    Однако самые полезные функции в Table — статические методы array и records. Они позволяют сразу создавать заполненные таблицы просто указав их содержимое.
    fn(Table::array(ctx, "one", 42, Table::array(ctx, 1, 2, 3)));  // Вложенные таблицы? Легко!
    

    Эквивалентный код
    lua_pushvalue(ctx, fn);
    lua_createtable(ctx, 3, 0);
    lua_pushinteger(ctx, 1);
    lua_pushstring(ctx, "one");
    lua_settable(ctx, -3);
    lua_pushinteger(ctx, 2);
    lua_pushinteger(ctx, 42);
    lua_settable(ctx, -3);
    lua_pushinteger(ctx, 3);
    lua_createtable(ctx, 3, 0);
    lua_pushinteger(ctx, 1);
    lua_pushinteger(ctx, 1);
    lua_settable(ctx, -3);
    lua_pushinteger(ctx, 2);
    lua_pushinteger(ctx, 2);
    lua_settable(ctx, -3);
    lua_pushinteger(ctx, 3);
    lua_pushinteger(ctx, 3);
    lua_settable(ctx, -3);
    lua_settable(ctx, -3);
    lua_call(ctx, 1, 0);
    

    Поскольку все значения должны быть привязаны к контексту, в данном случае на него приходится ссылаться явным образом. В остальном вполне очевидно, что array ассоциирует переданные значения с последовательными целочисленными индексами, начиная с 1. Это ещё одно из тех мест, где раскрываются выражения вызова и Valset.

    Метод records аналогичен, но принимает пары ключ-значение. В этом случае раскрытие вызовов уже было бы неправильным шагом.
    x.mt() = Table::records(ctx,
        "__index", xRead,
        "__newindex", xWrite,
        "__gc", xDestroy
    );

    Эквивалентный код
    lua_createtable(ctx, 0, 3);
    lua_pushstring(ctx, "__index");
    lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&xRead));
    lua_pushcclosure(ctx, &wrapped_signature_1, 1);
    lua_settable(ctx, -3);
    lua_pushstring(ctx, "__newindex");
    lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&xWrite));
    lua_pushcclosure(ctx, &wrapped_signature_2, 1);
    lua_settable(ctx, -3);
    lua_pushstring(ctx, "__gc");
    lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&xDestroy));
    lua_pushcclosure(ctx, &wrapped_signature_3, 1);
    lua_settable(ctx, -3);
    lua_setmetatable(ctx, x);
    



    Пользовательские данные


    Поддержка пользовательских данных достаточно прямолинейна. После регистрации в этом качестве какого-либо типа он получает равные права с поддерживаемыми нативными значениями, за одним исключением: преобразование в нативный тип должно быть только явным, через метод cast, причём такое преобразование возвращает ссылку.
    Регистрация осуществляется в два этапа. Сначала при помощи макроса LUAPP_USERDATA мы связываем имя типа с его строковым идентификатором. Затем, во время настройки окружения, необходимо задать соответствующую данному типу метатаблицу. Это можно сделать, проиндексировав registry строкой-идентификатором, но выразительнее сделать специально предназначенным для этого способом:
    LUAPP_USERDATA(MyType, "MyType Lua ID")
    
    Retval setup(Context& ctx)
    {
        ctx.mt<MyType>() = Table::records(ctx);  // То же самое, как если слева написать ctx.registry["MyType Lua ID"]
    }
    

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

    Размещение пользовательских данных в Lua подразумевает, что память для хранения объекта Lua выделяет своими силами и отслеживает через сборщик мусора. Lua API++ использует placement new для корректного конструирования объекта в выделенной памяти, задействуя конструктор перемещения, если таковой имеется. Так что не обязательно ограничиваться POD-типами. Более того, можно даже задействовать динамический полиморфизм, зарегистрировав базовый тип и его потомков под одним и тем же строковым идентификатором.

    Механизм обёртывания функций позволяет справиться с передачей пользовательских данных по значению и по ссылке. Что особенно приятно, этот механизм работает на функциях-членах, в этом случае подразумевается, что первый аргумент всегда будет ссылкой на наш пользовательский тип. Посмотрим, как это всё работает на примере добавления в Lua числового массива фиксированного размера, проверяющего индексы:
    #include <vector>
    using dvec = std::vector<double>;  // Используем вектор для хранения данных
    LUAPP_USERDATA(dvec, "Number array")  // Связь типа со строковым идентификатором
    
    dvec aCreate(size_t size)    // Создание массива заданного размера. 
    {  // Конструктор - специальная функция и его нельзя обернуть автоматически.
        return dvec(size);  // Благодаря RVO и конструктору перемещения не произойдёт перевыделения хранилища
    }
    
    void aDestroy(dvec& self)   // Деструктор - тоже специальная функция и его тоже нельзя обернуть.
    {
        self.~dvec();
    }
    
    void aWrite(dvec& self, size_t index, double val)  // Запись данных в массив в соответствии с порядком вызова __newindex
    { 
      self.at(index) = val;  // Для контроля доступа используем at, исключение преобразуется в ошибку Lua
    }
    
    Retval setup(Context& c) {  // Настройка окружения
        c.mt<dvec>() = Table::records(c,  // Метатаблица для нашего типа
            "__index", static_cast<double& (dvec::*)(size_t)>  (&dvec::at),   // Для чтения с контролем индекса используем родную функцию at
                // поскольку их две (const и не-const), явно выберем одну из перегрузок
            "__newindex", aWrite,
            "__len",      dvec::size,   // а вот size в vector один, тут совсем просто
            "__gc",       aDestroy  
        );
        c.global["createArray"] = aCreate;   // Функция создания массива будет глобальной
        return c.ret();
    }




    Заключение


    Когда я создавал эту библиотеку, то целью ставил создание удобного способа использования Lua без чрезмерных накладных расходов. Она является просто отображением Lua API на С++, без излишеств. В ней даже отсутствует поддержка некоторых возможностей Lua и Lua API (coroutine, string buffers, отладочных функций).

    Библиотека рассчитана на то, что её будут использовать вместо Lua API, поэтому по умолчанию она не подключает заголовочные файлы Lua. Большинство функций объявлено как inline, а все непосредственные обращения к Lua API вынесены в отдельный исходный файл, который надо компилировать вместе с библиотекой, желательно с использованием Link time code generation (LTO в GCC). Однако есть возможность переключить библиотеку в режим header-only. При этом все до единой функции окажутся объявленными в заголовках как inline и будут подключены заголовки Lua.

    Внешних зависимостей у библиотеки нет, ей нужны только совместимый со стандартом C++11 компилятор, заголовочные файлы Lua и STL. А вот тесты потребуют ещё Boost Unit Test Framework.

    По умолчанию библиотека рассчитана на Lua версии 5.2 (а после выхода 5.3 будет переориентирована на новую версию), но есть и режим совместимости с 5.1, полностью совместимый и с LuaJIT.

    Распространяется Lua API++ под лицензией MIT — та же самая, что у Lua, так что никакой юридической путаницы не возникнет. Библиотека укомплектована полной документацией в формате HTML, включая полный справочник и объяснение основных понятий.

    Надеюсь, что моя работа принесёт пользу кому-то из вас.
    Продолжить ли тему рассказом о внутреннем устройстве Lua API++?

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    • +30
    • 13,8k
    • 4
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 4
    • 0
      github.com/Ingener74/Selene не рассматривали? Тоже С++11 API для Lua. Правда мне показалась немного недоделанной но вроде ничего так, время от времени тоже делаю такую обёртку решил отталкиваться от Selene
      • 0
        Когда я начинал писать эту библиотеку, и даже когда довёл до рабочего состояния и стал пользоваться, Selene ещё не существовало. Я изначально ориентировался на некоторый набор возможностей, в который, к примеру, входил контроль стека и не входил экспорт классов.
      • 0
        При просмотре статьи создается ощущение, что Вы не продемонстрировали самого главного — какова ценность внедрения Вашего творения?
        В дополнение к q1 хотелось бы увидеть 3-5 сравнительных примеров использования С API и Lua API++, чтобы наглядно продемонстрировать ценность библиотеки читателю.

        q1: «Но всё же при использовании этого API, ориентированного на Си, меня не оставляла мысль, что этот процесс мог бы быть и поудобнее.»
        • 0
          Хорошая идея, напишу эквивалентный код для нескольких примеров.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.