Reputation: 933
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
Upvotes: 2
Views: 2648
Reputation: 9335
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.
Upvotes: 2
Reputation: 1
You might use std::function but your main trouble is what is the signature (i.e. number and type of arguments, type of result) of the registered function, and how to know it both at compile-time and at runtime (and also, how to call a function of arbitrary, runtime known, signature). Notice that C++ is generally erasing types (they are "forgotten" at runtime).
Notice that the signature of functions matter a lot in C and C++ (for the compiler), because the calling conventions and the ABI may require different machine code to call them.
You could decide that you have some universal value type (perhaps future std::experimental::any). Or (simpler, but much less general) you could define some abstract superclass MyValue
(it could be QVariant from Qt, or inspired by it) and handle only functions mapping a single std::vector<MyValue>
(conceptually representing the arguments of your RPC) to a MyValue
result. Then you would register only lambda-expressions compatible with std::function<MyValue(std::vector<MyValue>))>
and require them to check the arity and type at runtime.
Alternatively, you could decide to restrict yourself to a few signatures, e.g. only accept functions of no more than 4 arguments each being either a std::string
or an int
(so 31 different signatures that you'll handle one by one).
You also have an issue related to serialization of arbitrary value sharing some common pointer (or subvalue). Look into libs11n.
You could also have some machinery to register the signature itself. You might take advantage of existing metadata machinery (such as Qt meta-object protocol). You could have some textual descriptions of types and signatures, and write some C++ code generator handling them.
You might look into libffi. It could be relevant to call arbitrary raw functions with arbitrary signature.
Your library won't be tiny if it is general enough. You might restrict yourself to e.g. JSON values and representations. See JSONRPC.
You might have a metaprogramming approach, e.g. give the signature of registered functions (in some defined format), generate at runtime (initialization) the C++ code of a plugin for their glue code, and compile and dynamically load that plugin (e.g. using dlopen(3) on Linux).
Look also into CORBA & ONCRPC & Boost.RPC.
PS. I am assuming your C++ is at least C++11. BTW, you are underestimating how difficult your goal is if you want a general solution. You might spend months or years on that.
Upvotes: 5