Start with a type bundle:
template<class...Ts>struct types{
using type=types;
enum{count = sizeof...(Ts)};
};
template<class T> struct tag{using type=T;};
Now we define all of our supported types in one global types
bundle.
The index into that global types
bundle is sent over the wire, and used to look up the deserialization code.
There should be a function boost::any read( wire_data, T* unused )
defined in your protocol namespace (for basic types and std
types) and in the namespace of T
(for other types) that reads wire data into a boost::any
. wire_data
is just a placeholder for whatever stuff you get off the wire that you turn into the T
.
We turn the type index into a call to read
via the magic switch technique:
template<size_t n> using index=std::integral_constant<size_t, n>;
template<class types, class T>
struct index_in;
template<class...Ts, class T>
struct index_in<types<T, Ts...>, T>:index<0> {};
template<class T0, class...Ts, class T1>
struct index_in<types<T0, Ts...>, T1>:index<
index_in<types<Ts...>, T1>::value+1
> {};
gives us the offset of a type T
in types<Ts...>
. Use that on the sending side to map your type to an index into the list.
On the other side, we have:
template<class types, size_t n>
struct type_at;
template<class types, size_t n>
using type_at_t=typename type_at<types,n>::type;
template<class T0, class...Ts>
struct type_at<types<T0, Ts...>,0>: tag<T0> {};
template<class T0, class...Ts, size_t n>
struct type_at<types<T0, Ts...>,n>:
type_at<types<Ts...>, n-1>
{};
which takes a types<Ts...>
and an index and returns a type.
template<class types>
struct to_any {
template<size_t n>
struct worker {
boost::any operator()( wire_data w )const{
using protocol_ns::read;
return read( w, (type_at_t<types,n>*)nullptr );
}
};
};
which dispatches to read
using ADL.
Now we write our quick magic switch:
namespace details {
template<template<size_t>class action, class indexes>
struct magic_switch;
template<template<size_t>class action, size_t... Is>
struct magic_switch<action, std::index_sequences<Is...>>
{
template<class...Ts, class R=std::result_of_t< action<max>(Ts...) >>
R operator()(size_t i, Ts&&... ts)const {
using entry = R(*)(std::remove_reference<Ts>*...);
entry table[] = {
[](std::remove_reference<Ts>*...args)->R{
return action<Is>{}( std::forward<Ts>(*args)... );
}...
};
if (i > sizeof(table)/sizeof(entry))
throw std::out_of_range("i");
return table[i]( (&ts)... );
}
};
}
template<template<size_t>class action, size_t max>
struct magic_switch:
details::magic_switch<action,std::make_index_sequence<max>>
{};
Then
magic_switch<
to_any<all_types_supported>::template worker,
all_types_supported::count
>
is the type of a stateless function object that, when passed n
and wire_data
, will call the appropriate read
function for that type and return a boost::any
.
Ok, now we are half way there.
The second half involves taking our function of signature Z(Args...)
, and writing a type eraser that takes a std::vector<boost::any>
storing Args...
and returns a boost::any
storing a Z
.
std::function<boost::any(std::vector<boost::any>)> erased_func_t;
template<class... Args, class F>
erased_func_t erase_func(F&& f) {
// TODO
}
Once we have written that, we can store a map from string to an erased_func_t
for our table of functions.
We lookup the erased_func_t
. We use the above deserialization infrastructure to generate a std::vector<boost::any>
from the passed in parameters. We invoke it, having an exception thrown if it fails.
And bob is your uncle.
If you want to send the answer back, you'll need to type-erase going back to the wire format, and change erased_func_t
to return the wire_data
required to send it back over the wire instead of a boost::any
. That would probably be best.
None of the above code has been tested. Some of it requires C++14 (not that much, mostly _t
aliases), and some compilers who claim to support C++11 don't support the magic_switch
implementation I wrote (it is almost pure C++11, except the _t
aliases, I believe). But an equivalent one can be written, if more verbose.
Finally, like many things, it is usually not a good idea to write a RPC protocol from scratch. Odds are I missed an important step above.