I am developing a very tiny RPC library in C++. I would like to register RPC functions like this:
void foo(int a, int b) {
std::cout << "foo - a: " << a << ", b: " << b << std::endl;
}
myCoolRpcServer->registerFnc("foo", foo(int,int))
The client requests will arrive as function names and arrays of arguments. The server will check if it has corresponding function registered and if so, the function will be executed.
MyCoolRpcServer::handleRequest(string fnc, vector<FncArg> args)
{
// Check if we have the function requested by the client:
if (!this->hasFunction(fnc)) {
throw ...
}
if (!this->function(fnc).argCnt != args.count()) {
throw ...
}
// I think this is the hardest part - call an arbitrary function:
this->function(fnc)->exec(args); // Calls the function foo()
}
My question is how to store the function reference (including parameter types) and how to call it again. I know that something similar must be used in Qt when I call SLOT(...) macro, but it is quite tricky to find it in such a big library...
Thank you for your suggestions.
Klasyc
The basic idea is that you want to encapsulate your functions in some wrapper object which would handle some generic input/output and map them to what your underlying function expects.
First of all let's create a type for storing any value:
// Dummy implementation which only works for some type.
class Value {
long value_;
public:
template<class T>
T get()
{
return (T) value_;
}
template<class T>
Value& operator=(T const& x)
{
value_ = x;
return *this;
}
};
Let's hide our function using generic arguments:
typedef std::function<Value(std::vector<Value>&)> Function;
We now want to wrap any function pointer, in order to conform to this signature. The wrapper function should unwrap the arguments, call the real function and wrap the result in a Value:
template<class F> class FunctionImpl;
template<class R, class... T>
class FunctionImpl<R(*)(T...)>
{
R(*ptr)(T... args);
template<std::size_t... I>
Value call(std::vector<Value>& args, integer_sequence<std::size_t, I...>)
{
Value value;
value = ptr(args[I].get< typename std::tuple_element<I, std::tuple<T...>>::type >()...);
return value;
}
public:
FunctionImpl(R(*ptr)(T... args)) : ptr(ptr) {}
Value operator()(std::vector<Value>& args)
{
constexpr std::size_t count = std::tuple_size<std::tuple<T...>>::value;
if (args.size() != count)
throw std::runtime_error("Bad number of arguments");
return call(args, make_integer_sequence<std::size_t, std::tuple_size<std::tuple<T...>>::value>());
}
};
integer_sequence
and make_integer_sequence
are part of the standard C++17 library but you can write your own implementation.
We now define a type for registering the callable functions:
class Functions {
private:
std::unordered_map<std::string, Function> functions_;
public:
template<class F>
void add(std::string const& name, F f)
{
functions_[name] = FunctionImpl<F>(std::move(f));
}
Value call(std::string name, std::vector<Value>& args)
{
return functions_[name](args);
}
};
And we can use it:
int foo(int x, int y)
{
std::printf("%i %i\n", x, y);
return x + y;
}
int main()
{
Functions functions;
functions.add("foo", &foo);
std::pair<std::string, std::vector<Value>> request = parse_request();
Value value = functions.call(request.first, request.second);
generate_answer(value);
return 0;
}
with the dummy RPC communication functions:
std::pair<std::string, std::vector<Value>> parse_request()
{
std::vector<Value> args(2);
args[1] = 8;
args[0] = 9;
return std::make_pair("foo", std::move(args));
}
void generate_answer(Value& value)
{
std::printf("%i\n", value.get<int>());
}
We get:
8 9
17
Of course, this is highly simplified and you'll face many issues if you want to generalize it:
you might want to propagate exceptions as well;
integer types (eg. long
) do not have the same size on different platforms;
it starts gettings complicated if you want to handle pointers and references (you should probably not);
you'll have to add code for serialization/deserialization of all the types you're using.
On way to handle the serialization, would be to use generic programming for serialization/deserialization:
template<class T> class Type {};
typedef std::vector<char> Buffer;
// I'm clearly not claiming this would be efficient, but it gives
// the idea. In pratice, you might want to consume some streaming I/O
// API.
class Value {
Buffer buffer_;
public:
template<class T>
T get()
{
return deserialize(Type<T>(), buffer_);
}
template<class T>
Value& operator=(T const& x)
{
serialize(x, buffer_);
return *this;
}
};
inline std::uint32_t deserialize(Type<std::uint32_t>, Buffer const& buffer)
{
if (buffer.size() != sizeof(std::uint32_t))
throw std::runtime_error("Could not deserialize uint32");
std::uint32_t res;
memcpy(&res, buffer.data(), sizeof(std::uint32_t));
return be32toh(res);
}
inline void serialize(std::uint32_t value, Buffer const& buffer)
{
buffer.resize(sizeof(std::uint32_t));
value = htobe32(value);
memcpy(buffer.data(), &value, sizeof(std::uint32_t));
}
Another possibility is to use generic programming and let the Function
do the serialization/deserialization.