This chapter describes the internal design of
rpclib and provides some insight into the
engineering tradeoffs considered.
rpclib is self-contained, but it does use third party code. These are the following libraries:
- asio (used for networking and async capabilities)
- fmtlib (used for string formatting in log and exception messages)
- msgpack (used for encoding and decoding the protocol)
These dependencies are stored inside the repository of
rpclib, but they are hidden both during
compilation and linking. This is achieved by using the pimpl pattern and changing the names of the
namespaces in their source files (apart from
msgpack, none of the dependencies are visible in the
This means that as a user, you don't have to worry about linker problems if you integrate
into your project; and you don't need to gather its dependencies. This reduces friction. The tradeoff
is that the size of your binary will increase if you use one of these dependencies in your project
How can I compile
rpclib using dependencies outside its repository? While not officially
supported, it's possible that you will want to link to a system-installed
asio because you
are using it in your application anyway and want to avoid code bloat. To do this, delete the
library from the dependencies subfolder of the repository, and define
boost::asio). This will cause
rpclib to find the system-wide installed
asio and use
the namespace name provided. You might also need to change some of the preprocessor definitions
in the CMakeLists.txt if you want to use the boost-flavored asio, not the standalone one.
The internals of the server
rpclib maintains a registry of exposed functions in a
dispatcher. The dispatcher is a class with tfunction templates and this is the part which pulls in most of the template metaprogramming in the library. The primary purpose of the metaprogramming is to generate wrappers that can manage calling an arbitrary functor from a msgpack-encoded message; then encode the result of the function (if any) in msgpack.
The generated wrappers have a uniform signature (
dispatcher::adaptor_type) which allows storing
them in a map. The dispatching is performed by looking up the right functor by name.
The server loop
The call to
server::run starts an
asio-loop. Everything that the server does is performed in
this loop. This includes not only executing the handlers, but also parsing the input and writing
async_run will spawn multiple worker threads that all
run the loop. Thanks to the
great design of
asio, this makes them act like a thread pool, i.e. waiting in line to take the
next available work item. This scales pretty well for networked applications.
The server provides the above objects as a means of interacting with the library. Their
implementation relies on the realization that one thread executes at most one handler at any time;
thread_local objects are accessible both by the handler and the server loop. The server may
set properties of these objects that the handler can query; and likewise, the handler can also set
properties that the server can query.
The internals of the client
The client is fundamentally asynchronous in nature, even though this might not be readily apparent on the surface. The reasone for this is that responses from the server are not required to arrive right away, and responses to multiple requests may come in any order.
To address this, the client maintains a registry of ongoing calls. A "call" refers to
std::promise holding a
msgpack::object_handle, which is the future result of the call. When
the client reads a response, it will look up the promise and set the value.
On the public interface,
async_call returns a future that is bound to this promise. User code can
wait for the result using this future.
call is simply implemented as a call to
async_call and waiting for the result right away.
How and why the pimpl pattern is used
rpclib uses a variant of the fast pimpl idiom. The reason for
this is that one of the goals of the library is to provide a dependable rpc solution for projects
and make an effort to be easily upgrade-able when new versions come out. This is also one of the
reasons why the library is not header-only.
Instead of a
unique_ptr for the pimpl pointer, the library uses a pointer-like class which stores
its data in a
std::aligned_storage. This increases the data locality during the calls and reduces
dynamic allocation. The tradeoff is that the size of the storage is fixed, so adding extra data in an update is only possible with some bounds (the sizes used are a bit bigger than needed, so there is some room to do this without breaking binary compatibility).
Where to go from here
As a user, there isn't much else to learn about this library. However, if you are interested, you may
want to check out the contribution guidelines, the issue tracker, and roadmap and start hacking on