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ě.
Jak je to s typovou kontrolou? Pokud nechceme jen tak střílet od boku a doufat, že trefíme ty správné typy, tak je potřeba nějak předem dostat tu informaci o typech a názvech metod/procedur z API (jakou formou bude toto API deklarované?) serveru (resp. vzdálené strany) na stranu klienta. Tedy nějaký generátor je nutný, ne?
Generátor mi může vyrobit klientský kód typu:
int sum(int x, int y) { /** tady pošleme data po síti a vrátíme výsledek */ }
A pak už nepotřebuji žádný „Caller“ a „invoke()“ – prostě zavolám sum() – ideálně jako metodu na objektu, který ví, kdo je ta vzdálená strana, má s ní navázané spojení nebo ho aspoň umí navázat.
Ad „volání vzdálených procedur (tak nějak by se asi RPC přeložilo do čestiny)“
Ne „asi“ – tohle je běžný překlad :-)
Ale ta implementace metody:
int sum(int x, int y) { return x + y; }
je přece někde jinde, na vzdáleném stroji, ne? Od ní ani zdrojáky mít nemusíme, je to vzdálená metoda a nám stačí jen její rozhraní. Takže jak je toto rozhraní definované? A jak z něj preprocesor dostane typy?
Abych mohl zavolat createCaller(), tak stejně potřebuji to &sum - kde ho na klientovi vezmu?
Tohle jsem resil 1000x dokonce i tuple umim generovat tak, ze to vypada jako viceparametrove volani: foo((tuple,1,2,"ahoj")) - dvojite zavorky a klicove slovo tuple delaji tu magii a to funguje v C++03
Nakonec jsem se na to vykaslal a pouzivam jsonrpc. Klient umi rpccall(metoda, jsonarray) a server dostane parametry taky jako jsonarray. Vracet muze libovolny json. Ono to ma mnoho vyhod. Json array vyrabim pomoci JSON::create().array()(a)(b)(c). Json object pak podobne JSON::create().object(k,v)(k,v)(k,v)...
slo by to udelat i lip, ale uz jsem se tim nechtel vice zabyvat.
Já na tohle používám opačný přístup, a to že volání RPC je funktor (pro uvedený příklad funkce sum by to bylo RPC(server, "sum")). Výhodu to má, že se to chová jako úplně obyčejný funktor kompatibilní se std::function. Pokud bych chtěl automatickou dedukci parametrů, můžu vybrat jinou specializaci té třídy a zadat jen RPC, operátor () je potom šablonovaný a detekuje typy (tedy zde by to bylo std::cout << RPC(server, "sum")(2, 3) << std::endl). Delayed call řeším jednoduše lambdami (std::function delayed = [=]{ RPC(server, "sum")(2, 3); };). (Btw. interně je to stejně jenom obal JSON-RPC)
[7] Paráda, Root sežral všechny špičaté závorky :-/
Tak tedy, to první (kompatibilní se std::function) je RPC‹int(int, int)›(server, "sum") a to druhé (s detekcí typů v operátoru ()) je RPC‹int›(server, "sum").
Ta lambda by vypadala std::function‹void()› delayed = [=]{ RPC‹int›(server, "sum")(2, 3); };
Autor se zabývá vývojem kompilátorů a knihoven pro objektově-orientované programovací jazyky.
Přečteno 36 200×
Přečteno 25 361×
Přečteno 23 795×
Přečteno 20 177×
Přečteno 17 874×