汎用C ++メタシステム

ハブラーハロー!



C ++のメタシステムを開発し、さまざまなスクリプト言語を埋め込む経験を共有したいと思います。

最近、私は自分のゲームエンジンを書き始めました。 もちろん、他の優れたエンジンと同様に、スクリプト言語を埋め込むことについて、またはそれ以上の、さらには少数の質問についても疑問が生じました。 もちろん、特定の言語(たとえば、Luaのluabind、Pythonのboost.pythonなど)を埋め込むための十分なツールが既にあり、自転車を発明したくありませんでした。



シンプルで高速なLuaの埋め込みから始め、バインディングにluabindを使用しました。 そして彼は本当によく見えます。

自分で見てください
class_<BaseScript, ScriptComponentWrapper>("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 ++テンプレートを使用して記述され、特定の言語のみのコードを生成します。 各クラスは各システムに登録する必要があります。 この場合、多くのヘッダーファイルを追加し、テンプレートにすべてを手動で入力する必要があります。



ただし、すべての言語に共通の型ベースが必要です。 実行時にプラグインからタイプ情報を直接ダウンロードする機能もあります。 バインディングライブラリは、これらの目的には適していません。 本当のメタシステムが必要です。 しかし、ここでも、すべてが順調に進んでいませんでした。 準備が整ったライブラリはかなりかさばって不便であることが判明しました。 非常にエレガントなソリューションがありますが、追加の依存関係を引き出し、特別なツール(たとえば、Qt mocやgccxml)を使用する必要があります。 もちろん、キャンプリフレクションライブラリなど、非常に優れたオプションがあります。 それはluabindとほとんど同じに見えます:

 camp::Class::declare<MyClass>("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<bool (MyClass&)>(&MyClass::m_b)); }
      
      







確かに、そのような「美しい」ソリューションのパフォーマンスには、多くの要望が残されています。 もちろん、「通常の」プログラマと同様に、メタシステムを作成することにしました。 そこで、uMOFライブラリが登場しました。



uMOFの紹介


uMOFは、メタプログラミング用のクロスプラットフォームオープンソースライブラリです。 概念的にはQtを連想させますが、Qt自体が一度に放棄したテンプレートを使用して実行されました。 彼らはコードを読みやすくするためにこれを行いました。 そのため、非常に高速でコンパクトになりました。 ただし、mocコンパイラの使用はQtに完全に依存しています。 これは常に正当とは限りません。



ビジネスに取り掛かろう。 オブジェクト継承者のクラスでユーザーがメタ情報を利用できるようにするには、継承階層とEXPOSEを使用してOBJECTマクロを登録し、関数を宣言する必要があります。 その後、クラスAPIが使用可能になり、クラス、functionsx、およびパブリックプロパティに関する情報が保存されます。

 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<int>(m.invoke(&t, args)); Any res = Api::invoke(&t, "func", {5.0f, "6.0"});
      
      







これまでのところ、メタ情報の定義は侵襲的ですが、サードパーティのコードのより便利なラッパーのための外部オプションも計画されています。



高度なテンプレートの使用により、uMOFは非常に高速であることがわかりましたが、非常にコンパクトです。 これにより、いくつかの制限も生じました。 C ++ 11の機能が積極的に使用されていますが、すべてのコンパイラが適しているわけではありません(たとえば、Windowsでコンパイルするには、最新のVisual C ++ November CTPが必要です)。 また、誰もがコードでテンプレートを使用することを好むわけではないため、すべてがマクロでラップされています。 一方、マクロは多数のテンプレートを非表示にし、コードは非常にきれいに見えます。



さらに根拠がないように、ベンチマークの結果を紹介します。



試験結果


メタシステムを3つの方法で比較しました。コンパイル/リンク時間、実行可能ファイルのサイズ、ループ内で関数が呼び出された時間です。 参考として、ネイティブ関数呼び出しの例を取り上げました。 被験者はWindowsでVisual Studio 2013の下でテストされました。

枠組み コンパイル/リンク時間、ms 実行可能サイズ、KB 呼び出し時間*、ms
ネイティブ 371/63 12 2(45 **)
uMOF 406/78 18 359
キャンプ 4492/116 66 6889
Qt 1040/80(129 ***) 15 498
cpgf 2514/166 71 1184


脚注
* 10.000.000コール

**インライン化を強制しない

***メタオブジェクトコンパイラ



わかりやすくするため、グラフ形式でも同じです。



画像



画像



画像



また、いくつかのライブラリも調べました。





しかし、彼らはさまざまな理由で被験者の役割には落ちませんでした。 Boost.MirrorとXcppReflは有望に見えますが、まだ活発に開発されています。 ReflexにはGCCXMLが必要ですが、Windowsの適切な代替品は見つかりませんでした。 XRttiは、現在のリリースでもWindowsをサポートしていません。



ボンネットの下にあるもの


それで、それはどのように機能しますか。 ライブラリの速度とコンパクトさは、引数としての関数を含むテンプレートと可変引数テンプレートを提供します。 タイプごとのすべてのメタ情報は、静的テーブルのセットとして編成されます。 実行時に追加の負荷はありません。 そして、ポインターの配列という形の単純な構造は、コードが大きく膨らむのを防ぎます。

メソッド記述テンプレートの例
 template<typename Class, typename Return, typename... Args> struct Invoker<Return(Class::*)(Args...)> { typedef Return(Class::*Fun)(Args...); inline static int argCount() { return sizeof...(Args); } inline static const TypeTable **types() { static const TypeTable *staticTypes[] = { Table<Return>::get(), getTable<Args>()... }; return staticTypes; } template<typename F, unsigned... Is> inline static Any invoke(Object *obj, F f, const Any *args, unpack::indices<Is...>) { return (static_cast<Class *>(obj)->*f)(any_cast<Args>(args[Is])...); } template<Fun fun> 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<sizeof...(Args)>()); } };
      
      







効率における重要な役割はAnyクラスによっても果たされます。これにより、タイプとタイプに関する情報をコンパクトに保存できます。 基礎は、ブーストスピリットライブラリのhold_anyクラスです。 ここでは、型を効率的にラップするためにテンプレートも積極的に使用されます。 サイズがポインターよりも小さい型はvoid *に直接格納され、大きな型の場合、ポインターは型オブジェクトを参照します。

 template<typename T> struct AnyHelper<T, True> { typedef Bool<std::is_pointer<T>::value> is_pointer; typedef typename CheckType<T, is_pointer>::type T_no_cv; inline static void clone(const T **src, void **dest) { new (dest)T(*reinterpret_cast<T const*>(src)); } }; template<typename T> struct AnyHelper<T, False> { typedef Bool<std::is_pointer<T>::value> is_pointer; typedef typename CheckType<T, is_pointer>::type T_no_cv; inline static void clone(const T **src, void **dest) { *dest = new T(**src); } }; template<typename T> Any::Any(T const& x) : _table(Table<T>::get()), _object(nullptr) { const T *src = &x; AnyHelper<T, Table<T>::is_small>::clone(&src, &_object); }
      
      







また、RTTIも放棄しなければなりませんでした。 型チェックは、型テーブルへのポインタを比較することによってのみ行われます。 すべての型修飾子は事前にクリアされています。そうでない場合、たとえば、intとconst intは異なる型になります。 しかし、実際にはサイズは単一で、一般的には同じタイプです。

別の例
 template <typename T> inline T* any_cast(Any* operand) { if (operand && operand->_table == Table<T>::get()) return AnyHelper<T, Table<T>::is_small>::cast(&operand->_object); return nullptr; }
      
      







使い方


スクリプト言語の埋め込みが簡単で楽しくなりました。 たとえば、Luaの場合、引数の数とその型を確認し、もちろん関数自体を呼び出す汎用呼び出し関数を定義するだけで十分です。 バインドも難しくありません。 Luaの各関数について、MetaMethodを上位値に保存するだけです。 ところで、uMOFのすべてのオブジェクトは「薄い」、つまり、静的テーブルのエントリを参照するポインターの単なるラッパーです。 したがって、パフォーマンスを恐れることなくコピーできます。



Luaバインディングの例:

例、大量のコード
 #include <lua/lua.hpp> #include <object.h> #include <cassert> #include <iostream> 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<double>(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の欠点:



ライブラリはまだ十分に未加工です。私は多くの興味深いことをしたいと思います-変数アリティの関数(読み取り、デフォルトのパラメータ)、タイプの非侵襲的な登録、オブジェクトのプロパティの変更に関する信号。 そして、この方法は非常に良い結果を示したので、これは確かに現れます。



ご清聴ありがとうございました。 ライブラリが誰かに役立つことを願っています。



このプロジェクトはここにあります 。 コメントにフィードバックや推奨事項を記入してください。



All Articles