[Из песочницы] Универсальная метасистема на C++
Привет, Хабрхабр! Хочу поделиться своим опытом разработки метасистемы для C++ и встраивания различных скриптовых языков.Сравнительно недавно начал писать свой игровой движок. Разумеется, как и в любом хорошем движке встал вопрос о встраивании скриптового языка, а лучше даже нескольких. Безусловно, для встраивания конкретного языка уже есть достаточно инструментов (например, luabind для Lua, boost.python для Python), и свой велосипед изобретать не хотелось.Начал со встраивания простого и шустрого Lua, а для биндинга использовал luabind. И он выглядит действительно неплохо.
Убедитесь сами
class_
А ведь хочется, чтобы была общая база типов для всех языков. А также возможность загрузить информацию о типах из плагинов прямо в рантайме. Для этих целей binding библиотеки не подходят. Нужна настоящая метасистема. Но тут тоже оказалось не все гладко. Готовые библиотеки оказались довольно громоздкими и неудобными. Существуют и весьма изящные решения, но они тянут за собой дополнительные зависимости и требуют использования специальных инструментов (например, Qt moc или gccxml). Есть, конечно же, и довольно симпатичные варианты, такие как, например, библиотека для рефлексии Camp. Выглядит она почти также, как и luabind:
Пример
camp: Class: declare
// ***** 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
Пример 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
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
Для наглядности тоже самое в виде графиков.
Я также рассматривал еще несколько библиотек:
Boost.Mirror;
XcppRefl;
Reflex;
XRtti.
Но они не попали на роль испытуемых по разным причинам. Boost.Mirror и XcppRefl выглядят перспективно, но пока находятся на стадии активной разработки. Reflex требует GCCXML, какой либо адекватной замены для Windows я не нашел. XRtti опять же в текущем релизе не поддерживает Windows.Что под капотом
Итак, как это все работает. Скорость и компактность библиотеке дают шаблоны с функциями в качестве аргументов, а также variadic шаблоны. Вся мета информация по типам организована как набор статических таблиц. Никакой дополнительно нагрузки в рантайме нет. А простая структура в виде массива указателей не дает коду сильно распухнуть.Пример шаблона описания метода
template
inline static int argCount () { return sizeof…(Args); }
inline static const TypeTable **types ()
{
static const TypeTable *staticTypes[] =
{
Table
template
template
inline static void clone (const T **src, void **dest)
{
new (dest)T (*reinterpret_cast
template
inline static void clone (const T **src, void **dest) { *dest = new T (**src); } };
template
return nullptr; } Как этим пользоваться Встраивание скриптовых языков стало легким и приятным. Например, для Lua достаточно определить обобщённую функцию вызова, которая проверит количество аргументов и их типы и разумеется вызовет саму функцию. Биндинг тоже не представляет сложности. Для каждой функции в Lua достаточно сохранить MetaMethod в upvalue. Кстати все объекты в uMOF «тонкие», то есть просто обертка над указателем, который ссылается на запись в статической таблице. Поэтому можно копировать их без опасения насчет производительности.Пример биндинга Lua:
Пример, много кода
#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
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: Поддерживается не всеми компиляторами; Вспомогательные макросы довольно неказисты. Библиотека пока достаточно сырая, хотелось бы еще много чего интересного сделать — функции переменной арности (читай, параметры по умолчанию), неинвазивная регистрация типов, сигналы об изменении свойств объекта. И все это обязательно появится, ведь метод показал весьма хорошие результаты.Всем спасибо за внимание. Надеюсь библиотека окажется для кого-то полезной.
Проект можно найти по ссылке. Пишите свои отзывы и рекомендации в комментариях.