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
можна перерахувати стільки значень, скільки знадобиться.

Сигналізація про помилки
Стверджуючи, що єдиний спосіб створити
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());

Доступ до оточення
Наш
Context
, очевидно, є першоджерелом значень. Справді, звідки їм взятися?
Нам в користування надаються об'єкти доступу, оформлені як відкриті члени класу
Context
, що дозволяють дотягнутися до різних цікавих місць оточення. Всі вони дозволяють здійснювати як читання, так і запис значень.

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

На другому місці стоїть доступ до глобальних змінних. Об'єкт
global
індексується рядками.
ctx.global["answer"] = 42; // Якщо такої глобальної змінної не було, тепер з'явиться

Якщо наша 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 дозволяє створювати замикання. В об'єкті
Context
для цього застосовується функція
closure
, яка отримує
CFunction
і ті значення, які будуть зберігатися в замиканні. Результат — тимчасовий об'єкт, тобто повноцінне Lua-значення.
Замість
CFunction
ми можемо вказати одразу
LFunction
, але у цієї легкості є своя ціна. В отриманому замиканні буде зарезервовано перше upvalue (там зберігається адреса функції, оскільки обгортка одна і та ж для будь LFunction). Ця ж функція застосовується і для прозорої міграції
LFunction
з тими ж наслідками. У цьому полягає відмінність від шаблону
mkcf
, який нічого не резервує, але зате створює окрему функцію-обгортку для кожної функції.

А ще можна створювати чанкі: скомпільований код Lua. Безпосередньо текст компілюється методом
chunk
, а вміст файлу за допомогою
load
. Для випадків «виконав і забув» є
runString
та
runFile
, точно такі ж, як і в
State
. З точки зору використання чанк — звичайна функція.

Замикання можна створювати і з несумісних функцій за допомогою методу
wrap
. Він автоматично створює обгортку, яка бере аргументи зі стека Lua, перетворює їх у значення, прийняті нашою функцією, виробляє виклик і розміщує результат на стеку Lua як значення, що повертається. За замовчуванням це працює зі всіма підтримуваними типами, включаючи користувацькі дані. А якщо цього мало (наприклад, нам треба робити що-то з рядками, що зберігаються у
vector<char>
, то можна і самому вказати перетворення в ту або іншу сторону за допомогою спеціальних макросів.
Саме
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);

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




Операції з одиночним значенням

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

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


Довжина
Робота функції
len
розрізняється в різних версіях Lua: в режимі сумісності з 5.1 вона повертає нативний
size_t
, а в режимі 5.2 тимчасове значення.

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


Виклик функцій
Виклик 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-значення. Тоді буде використано лише перше обчислене значення (або
nil
, якщо функція взагалі нічого не повернула), а решта знову ж будуть викинуті.
Value x = mrv(); // x == 2

По-третє, в контексті, подразумевающем послідовність значень (наприклад, параметри функції) вираз виклику піддасться розкриття: повернені значення стануть частиною послідовності.
print(1, mrv(), 5); // Надрукує 1 2 3 4 5

По-четверте, можна просто зберегти все в
Valset
, придуманий спеціально для цього.
Valset vs = mrv.pcall(); // vs.size() == 3, vs.success() == true
Valset
запам'ятає і те, чи захищений виклик вдалим (це єдиний спосіб отримати цю інформацію). У разі невдачі він буде містити повідомлення про помилку. Що приємно,
Valset
може розкриватися у списку значень точно так само, як і вираз виклику.
print(1, vs, 5); // Надрукує 1 2 3 4 5

Однак цим користь
Valset 
не вичерпується. Він є STL-подібних контейнерах і до нього можна застосовувати алгоритми STL, а в якості «збережених значень виступають
Valref
. Якщо
Valset 
розташований на стеку останнім, то до нього застосовні операції додавання і видалення значень
push_back
та
pop_back
. Зазвичай при використанні целикового
Valref
відбувається дублювання містяться значень, але якщо ми повертаємо його функції (але тільки один
Valset
), то його вміст використовується безпосередньо. Так можна накопичувати значення, що повертаються, кількість яких стає відомо під час виконання.



Операції з двома значеннями

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

Знаком & позначається конкатенація. Ланцюгові конкатенації оптимізуються в єдиний виклик, «склеює» відразу кілька значень. Заодно конкатенація є одним з тих місць, в яких розкриваються вираження виклику і
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);
});

Результатом
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<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, включаючи повний довідник і пояснення основних понять.

Сподіваюся, що моя робота принесе користь комусь із вас.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.