Hlavní navigace

Proměnlivé šablony a RPC v C++

2. 4. 2014 14:33 zboj

RPC je sice technologie včerejška (nebo úzce specializovaných aplikací s těžkotonážními rozhraními à la Corba), nicméně v reakci na poznámku na jednom vývojářském fóru jsem se zamyslel nad možností transparentního volání vzdálených procedur (tak nějak by se asi RPC přeložilo do čestiny) v C++. Přímočaré řešení je volat metodu v proxy oklikou podle jména funkce a argumenty předat jako seznam (nebo pole nebo vektor nebo něco podobného). Toto implementovat je triviální, ale pro uživatele to není moc transparentní. Druhá často užívaná možnost je využití nějakého preprocesoru (u C++ by to byl spíše prepreprocesor), jenž podle definice rozhraní vygeneruje proxy a stuby, jež se následně postarají o síťovou komunikaci a volání příslušných metod na serveru. To je sice transparentní, ale je tam krok (a trochu práce) navíc.

Nejjednodušší řešení (poprvé použité v ObjC a následně, po přidání podpůrných prostředků do JVM, i v Javě) je spolehnout se na introspekci, kterou zajistí runtime. Jenže v C++ introspekci implicitně nemáme, takže se musíme poohlédnout po jiných prostředcích, které ji (v rozsahu dostatečném pro naše účely) nahradí. Ve zbytku článku se zaměřím jen na (un)marshalling argumentů metod, protože zbytek je (díky standardnímu preprocesoru) sice ne přímo triviální, ale relativně bezproblémový.

Níže uvedený kód jsem neoptimalizoval na rychlost, nicméně je celkem přímočarý a přitom názorný. Navíc při RPC je zdaleka nejpomalejší přenos po síti, takže ani značně pomalejší introspekce v Javě (nemluvě o .NET) v tomto případě nehraje roli. Na straně proxy musíme z metody dostat všechny argumenty ve formě seznamu, k němuž lze přistupovat programaticky. Například argumenty metody test(int,float,string) převedeme na seznam o třech prvcích. Zde je zřejmé, že nemůžeme použít vector, list ani žádnou analogickou kolekci, protože argumenty jsou různých typů. Mohli bychom použít tuple, nicméně pohodlnější bude vytvořit si vlastní spojový seznam:

template<typename...Args> struct List {}; template<> struct List<> { List() {} }; template<typename H, typename...T> struct List<H,T...> { H head; List<T...> tail; List() {} List(const H& h, const T&... t) : head(h), tail(t...) {} void iterate(const std::function<void(const H&)>& callback) const { callback(head); tail.iterate(callback); } }; template<typename H> struct List<H> { H head; List<> tail; List() {} List(const H& h) : head(h) {} void iterate(const std::function<void(const H&)>& callback) const { callback(head); } };

K pohodlnější práci (abychom nemuseli uvádět při vytváření seznamu typy v šabloně) si ještě napíšeme tyto pomocné funkce:

template<typename...Args> List<Args...> createList(const Args&...args) { return List<Args...>(args...); } template<typename H,typename...T> auto createList(const H& h, const List<T...>& t) { List<H,T...> list; list.head = h; list.tail = t; return list; }

Díky takto vytvořenému seznamu můžeme nyní provádět marshalling argumentů (pomocí std::forward<Args>(args)…) a přenášet je na server.

Předpokládejme nyní, že máme na serveru seznam argumentů a chceme zavolat příslušnou funkci. Nejprve si napíšeme pomocné funkce k přístupu k n-tému prvku seznamu:

template<int N> struct getel { template<typename H,typename...T> static auto value(const List<H,T...>& list) { return getel<N-1>::value(list.tail); } }; template<> struct getel<0> { template<typename H,typename...T> static auto value(const List<H,T...>& list) { return list.head; } };

Teď již můžeme napsat třídu, jež dostane ukazatel na funkci a seznam argumentů a tuto funkci zavolá.

template<typename Ret,typename...Args> class Caller { private: Ret(*func)(Args...); List<Args...> list; public: Caller(decltype(func) f, const decltype(list)& l) : func(f), list(l) {} template<typename...A> typename std::enable_if<sizeof...(A) != sizeof...(Args),Ret>::type invoke(const A&...a) { return invoke(a..., getel<sizeof...(A)>::value(list)); } Ret invoke(const Args&...args) { return (*func)(args...); } }; template<typename Ret,typename...Args> auto createCaller(Ret(*func)(Args...), const List<Args...>& list) { return Caller<Ret,Args...>(func, list); }

A to je vše. Kód lze jednoduše vyzkoušet. Mějme například funkci

int sum(int x, int y) { return x + y; }

Následující kód vytvoří (dvouprvkový) seznam a následně na něj zavolá funkci sum:

const auto& list = createList(2, 3); std::cout << createCaller(&sum, list).invoke() << std::endl;

Pro jednoduchost kód pracuje pouze s funkcemi (ne metodami objektů), taková změna je ovšem jednoduchá (ukazatel na metodu bude typu Ret(T::*func)(Args…)). Kód nepatří k nejjednodušším, ale vše výše uvedené by bylo někde hluboko v knihovně a uživatel by pracoval s objekty zcela transparentně.