[Из песочницы] Универсальная метасистема на C++

Привет, Хабрхабр! Хочу поделиться своим опытом разработки метасистемы для C++ и встраивания различных скриптовых языков.Сравнительно недавно начал писать свой игровой движок. Разумеется, как и в любом хорошем движке встал вопрос о встраивании скриптового языка, а лучше даже нескольких. Безусловно, для встраивания конкретного языка уже есть достаточно инструментов (например, luabind для Lua, boost.python для Python), и свой велосипед изобретать не хотелось.Начал со встраивания простого и шустрого Lua, а для биндинга использовал luabind. И он выглядит действительно неплохо.

Убедитесь сами class_(«BaseComponent») .def (constructor<>()) .def («start», &BaseScript: start, &ScriptComponentWrapper: default_start) .def («update», &BaseScript: update, &ScriptComponentWrapper: default_update) .def («stop», &BaseScript: stop, &ScriptComponentWrapper: default_stop) .property («camera», &BaseScript: getCamera) .property («light», &BaseScript: getLight) .property («material», &BaseScript: getMaterial) .property («meshFilter», &BaseScript: getMeshFilter) .property («renderer», &BaseScript: getRenderer) .property («transform», &BaseScript: getTransform) Читается легко, класс регистрируется просто и без проблем. Но это решение исключительно для Lua.Вдохновившись скриптовой системой Unity, понял, что однозначно должно быть несколько языков в системе, а также возможность их взаимодействия между собой. И тут такого рода инструменты, как luabind, дают слабину: в большинстве своем они написаны с использованием шаблонов C++ и генерируют код только для специфического языка. Каждый класс нужно зарегистрировать в каждой системе. При этом необходимо добавить множество заголовочных файлов и вручную вписать все в шаблоны.

А ведь хочется, чтобы была общая база типов для всех языков. А также возможность загрузить информацию о типах из плагинов прямо в рантайме. Для этих целей binding библиотеки не подходят. Нужна настоящая метасистема. Но тут тоже оказалось не все гладко. Готовые библиотеки оказались довольно громоздкими и неудобными. Существуют и весьма изящные решения, но они тянут за собой дополнительные зависимости и требуют использования специальных инструментов (например, Qt moc или gccxml). Есть, конечно же, и довольно симпатичные варианты, такие как, например, библиотека для рефлексии Camp. Выглядит она почти также, как и luabind:

Пример camp: Class: declare(«FunctionAccessTest: MyClass») // ***** constant value ***** .function («f0», &MyClass: f).callable (false) .function («f1», &MyClass: f).callable (true)

// ***** function ***** .function («f2», &MyClass: f).callable (&MyClass: b1) .function («f3», &MyClass: f).callable (&MyClass: b2) .function («f4», &MyClass: f).callable (boost: bind (&MyClass: b1, _1)) .function («f5», &MyClass: f).callable (&MyClass: m_b) .function («f6», &MyClass: f).callable (boost: function(&MyClass: m_b)); } Правда производительность подобных «красивых» решений оставляет желать лучшего. Конечно же, как и любой «нормальный» программист, я решил написать свою метасистему. Так появилась библиотека uMOF.Знакомство с uMOF uMOF — кроссплатформенная open source библиотека для метапрограммирования. Концептуально напоминает Qt, но выполнена с помощью шаблонов, от которых в свое время отказались сами Qt. Они это сделали ради читаемости кода. И так реально быстрее и компактнее. Но, использование moc компилятора приводит в полную зависимость от Qt. Это не всегда оправдано.Перейдем все же к делу. Чтобы сделать доступной для пользователя метаинформацию в классе наследнике Object нужно прописать макросы OBJECT с иерархией наследования и EXPOSE для объявления функций. После этого становится доступен API класса, в котором хранится информация о классе, функцияx и публичных свойствах.

Пример class Test: public Object { OBJECT (Test, Object) EXPOSE (Test, METHOD (func), METHOD (null), METHOD (test) )

public: Test () = default;

float func (float a, float b) { return a + b; }

int null () { return 0; }

void test () { std: cout << "test" << std::endl; } };

Test t;

Method m = t.api ()→method («func (int, int)»); int i = any_cast(m.invoke (&t, args));

Any res = Api: invoke (&t, «func», {5.0f,»6.0»}); Пока определение метаинформации инвазивно, но планируется и внешний вариант для более удобной обертки стороннего кода.Из-за использования продвинутых шаблонов uMOF получился очень быстрым, при этом довольно компактным. Это же привело и к некоторым ограничениям: т.к. активно используются возможности C++11, не все компиляторы подойдут (например, чтобы скомпилировать на Windows, нужен самый последний Visual C++ November CTP). Также использование шаблонов в коде не всем понравится, поэтому все завернуто в макросы. Между тем макросы скрывают большое количество шаблонов и код выглядит довольно аккуратно.

Дабы не быть голословным дальше привожу результаты бенчмарков.

Результаты тестирования Я сравнивал метасистемы по трем параметрам: время компиляции/линковки, размер исполняемого файла и время вызова функции в цикле. В качестве эталона я взял пример с нативным вызовом функций. Испытуемые тестировались на Windows под Visual Studio 2013.Framework Compile/Link time, ms Executable size, KB Call time spent*, ms Native 371/63 12 2 (45**) uMOF 406/78 18 359 Camp 4492/116 66 6889 Qt 1040/80 (129***) 15 498 cpgf 2514/166 71 1184 Сноски * 10.000.000 calls** Force no inlining*** Meta object compiler

Для наглядности тоже самое в виде графиков.image

image

image

Я также рассматривал еще несколько библиотек:

Boost.Mirror; XcppRefl; Reflex; XRtti. Но они не попали на роль испытуемых по разным причинам. Boost.Mirror и XcppRefl выглядят перспективно, но пока находятся на стадии активной разработки. Reflex требует GCCXML, какой либо адекватной замены для Windows я не нашел. XRtti опять же в текущем релизе не поддерживает Windows.Что под капотом Итак, как это все работает. Скорость и компактность библиотеке дают шаблоны с функциями в качестве аргументов, а также variadic шаблоны. Вся мета информация по типам организована как набор статических таблиц. Никакой дополнительно нагрузки в рантайме нет. А простая структура в виде массива указателей не дает коду сильно распухнуть.Пример шаблона описания метода template struct Invoker { typedef Return (Class::*Fun)(Args…);

inline static int argCount () { return sizeof…(Args); }

inline static const TypeTable **types () { static const TypeTable *staticTypes[] = { Table:: get (), getTable()… }; return staticTypes; }

template inline static Any invoke (Object *obj, F f, const Any *args, unpack: indices) { return (static_cast(obj)→*f)(any_cast(args[Is])…); }

template static Any invoke (Object *obj, int argc, const Any *args) { if (argc!= sizeof…(Args)) throw std: runtime_error («Bad argument count»); return invoke (obj, fun, args, unpack: indices_gen()); } }; Немаловажную роль в эффективности также играет класс Any, который позволяет достаточно компактно хранить типы и информацию о них. Основой послужил класс hold_any из библиотеки boost spirit. Здесь также активно используются шаблоны, чтобы эффективно оборачивать типы. Типы меньше указателя по размеру хранятся непосредственно в void*, для более крупных типов указатель ссылается на объект типа.Пример template struct AnyHelper { typedef Bool:: value> is_pointer; typedef typename CheckType:: type T_no_cv;

inline static void clone (const T **src, void **dest) { new (dest)T (*reinterpret_cast(src)); } };

template struct AnyHelper { typedef Bool:: value> is_pointer; typedef typename CheckType:: type T_no_cv;

inline static void clone (const T **src, void **dest) { *dest = new T (**src); } };

template Any: Any (T const& x) : _table (Table:: get ()), _object (nullptr) { const T *src = &x; AnyHelper:: is_small>:: clone (&src, &_object); } От RTTI тоже пришлось отказаться, слишком медленно. Проверка типа идет исключительно сравнением указателей на таблицу типа. Все модификаторы типа предварительно очищаются, иначе, например, int и const int окажутся разными типами. Но на самом деле их размер одинок, и вообще это один и тот же тип.Еще пример template inline T* any_cast (Any* operand) { if (operand && operand→_table == Table:: get ()) return AnyHelper:: is_small>:: cast (&operand→_object);

return nullptr; } Как этим пользоваться Встраивание скриптовых языков стало легким и приятным. Например, для Lua достаточно определить обобщённую функцию вызова, которая проверит количество аргументов и их типы и разумеется вызовет саму функцию. Биндинг тоже не представляет сложности. Для каждой функции в Lua достаточно сохранить MetaMethod в upvalue. Кстати все объекты в uMOF «тонкие», то есть просто обертка над указателем, который ссылается на запись в статической таблице. Поэтому можно копировать их без опасения насчет производительности.Пример биндинга Lua:

Пример, много кода #include #include #include #include

class Test: public Object { OBJECT (Test, Object) EXPOSE ( METHOD (sum), METHOD (mul) )

public: static double sum (double a, double b) { return a + b; }

static double mul (double a, double b) { return a * b; } };

int genericCall (lua_State *L) { Method *m = (Method *)lua_touserdata (L, lua_upvalueindex (1)); assert (m);

// Retrieve the argument count from Lua int argCount = lua_gettop (L); if (m→parameterCount () != argCount) { lua_pushstring (L, «Wrong number of args!»); lua_error (L); }

Any *args = new Any[argCount]; for (int i = 0; i < argCount; ++i) { int ltype = lua_type(L, i + 1); switch (ltype) { case LUA_TNUMBER: args[i].reset(luaL_checknumber(L, i + 1)); break; case LUA_TUSERDATA: args[i] = *(Any*)luaL_checkudata(L, i + 1, "Any"); break; default: break; } }

Any res = m→invoke (nullptr, argCount, args); double d = any_cast(res); if (! m→returnType ().valid ()) return 0;

return 0; }

void bindMethod (lua_State *L, const Api *api, int index) { Method m = api→method (index); luaL_getmetatable (L, api→name ()); // 1 lua_pushstring (L, m.name ()); // 2 Method *luam = (Method *)lua_newuserdata (L, sizeof (Method)); // 3 *luam = m; lua_pushcclosure (L, genericCall, 1); lua_settable (L, -3); // 1[2] = 3 lua_settop (L, 0); }

void bindApi (lua_State *L, const Api *api) { luaL_newmetatable (L, api→name ()); // 1

// Set the »__index» metamethod of the table lua_pushstring (L,»__index»); // 2 lua_pushvalue (L, -2); // 3 lua_settable (L, -3); // 1[2] = 3 lua_setglobal (L, api→name ()); lua_settop (L, 0);

for (int i = 0; i < api->methodCount (); i++) bindMethod (L, api, i); }

int main (int argc, char *argv[]) { lua_State *L = luaL_newstate (); luaL_openlibs (L); bindApi (L, Test: classApi ());

int erred = luaL_dofile (L, «test.lua»); if (erred) std: cout << "Lua error: " << luaL_checkstring(L, -1) << std::endl;

lua_close (L);

return 0; } Заключение Итак, что мы имеем: Достоинства uMOF: Компактный; Быстрый; Не требует сторонних инструментов, только современный компилятор. Недостатки uMOF: Поддерживается не всеми компиляторами; Вспомогательные макросы довольно неказисты. Библиотека пока достаточно сырая, хотелось бы еще много чего интересного сделать — функции переменной арности (читай, параметры по умолчанию), неинвазивная регистрация типов, сигналы об изменении свойств объекта. И все это обязательно появится, ведь метод показал весьма хорошие результаты.Всем спасибо за внимание. Надеюсь библиотека окажется для кого-то полезной.

Проект можно найти по ссылке. Пишите свои отзывы и рекомендации в комментариях.

© Habrahabr.ru