Internals

This chapter describes the internal design of rpclib and provides some insight into the engineering tradeoffs considered.

Dependencies of rpclib

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 headers even).

This means that as a user, you don't have to worry about linker problems if you integrate rpclib 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 outside rpclib.

Info

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 RPCLIB_ASIO as asio (or 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

The dispatcher

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 the output. 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.

this_handler, this_session, this_server

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; so 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 a 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 rpclib!