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
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
if (ctx.args.size () > 1 && ctx.args[0].is
auto k = ctx.registry.store (ctx.upvalues[1]); // decltype (k) == int ctx.registry [k] = nil; // Ключ k освободился и может снова стать результатом store Функции Я только что упомянул о том, что Lua позволяет создавать замыкания. В объекте Context для этого применяется функция closure, которая получает CFunction и те значения, которые будут храниться в замыкании. Результат — временный объект, то есть полноценное Lua-значение.Вместо CFunction мы можем указать сразу LFunction, но у этой лёгкости есть своя цена. В получившемся замыкании будет зарезервировано первое upvalue (там хранится адрес функции, поскольку обёртка одна и та же для любой LFunction). Эта же функция применяется и для прозрачной миграции LFunction с теми же последствиями. В этом состоит отличие от шаблона mkcf, который ничего не резервирует, но зато создаёт отдельную функцию-обёртку для каждой функции.А ещё можно создавать чанки: скомпилированный код Lua. Непосредственно текст компилируется методом chunk, а содержимое файла при помощи load. Для случаев «выполнил и забыл» есть runString и runFile, точно такие же, как и в State. С точки зрения использования чанк — обычная функция.
Замыкания можно создавать и из несовместимых функций при помощи метода wrap. Он автоматически создаёт обёртку, которая берёт аргументы со стека Lua, преобразует их в значения, принимаемые нашей функцией, производит вызов и размещает результат на стеке Lua в качестве возвращаемого значения. По умолчанию это работает со всеми поддерживаемыми типами, включая пользовательские данные. А если этого мало (например, нам надо вытворять что-то со строками, хранящимися в vector
Миграция значений
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
В версии 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); }); Результатом iterate будет количество обработанных записей.Однако самые полезные функции в Table — статические методы array и records. Они позволяют сразу создавать заполненные таблицы просто указав их содержимое.
fn (Table: array (ctx, «one», 42, Table: array (ctx, 1, 2, 3))); // Вложенные таблицы? Легко! Поскольку все значения должны быть привязаны к контексту, в данном случае на него приходится ссылаться явным образом. В остальном вполне очевидно, что array ассоциирует переданные значения с последовательными целочисленными индексами, начиная с 1. Это ещё одно из тех мест, где раскрываются выражения вызова и Valset.Метод records аналогичен, но принимает пары ключ-значение. В этом случае раскрытие вызовов уже было бы неправильным шагом.
x.mt () = Table: records (ctx, »__index», xRead, »__newindex», xWrite, »__gc», xDestroy ); Пользовательские данные Поддержка пользовательских данных достаточно прямолинейна. После регистрации в этом качестве какого-либо типа он получает равные права с поддерживаемыми нативными значениями, за одним исключением: преобразование в нативный тип должно быть только явным, через метод cast, причём такое преобразование возвращает ссылку.Регистрация осуществляется в два этапа. Сначала при помощи макроса LUAPP_USERDATA мы связываем имя типа с его строковым идентификатором. Затем, во время настройки окружения, необходимо задать соответствующую данному типу метатаблицу. Это можно сделать, проиндексировав registry строкой-идентификатором, но выразительнее сделать специально предназначенным для этого способом: LUAPP_USERDATA (MyType, «MyType Lua ID»)
Retval setup (Context& ctx)
{
ctx.mt
Механизм обёртывания функций позволяет справиться с передачей пользовательских данных по значению и по ссылке. Что особенно приятно, этот механизм работает на функциях-членах, в этом случае подразумевается, что первый аргумент всегда будет ссылкой на наш пользовательский тип. Посмотрим, как это всё работает на примере добавления в Lua числового массива фиксированного размера, проверяющего индексы:
#include
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
Внешних зависимостей у библиотеки нет, ей нужны только совместимый со стандартом C++11 компилятор, заголовочные файлы Lua и STL. А вот тесты потребуют ещё Boost Unit Test Framework.
По умолчанию библиотека рассчитана на Lua версии 5.2 (а после выхода 5.3 будет переориентирована на новую версию), но есть и режим совместимости с 5.1, полностью совместимый и с LuaJIT.
Распространяется Lua API++ под лицензией MIT — та же самая, что у Lua, так что никакой юридической путаницы не возникнет. Библиотека укомплектована полной документацией в формате HTML, включая полный справочник и объяснение основных понятий.
Надеюсь, что моя работа принесёт пользу кому-то из вас.
