Writing a JSON-RPC system for C++: Function wrappers
I’ve found myself in need of a JSON-RPC implementation for a project I’m currently working on in C++. Unfortunately I couldn’t find any existing solutions that met my needs. Most of them implement far too many features, don’t take advantage of modern C++ features, and pull in a ton of dependencies. So I’ve decided to make my own, and document the process.
Overview
In order to make this work, I set my end goal as being able to send and receive JSON-RPC 2.0 messages. Rather than use HTTP for this, I am opting to use plain TCP sockets. To make the library actually useful, I need an easy way to bind functions to the RPC system, and that’s where I am going to start. As a sidenote, I am far from an expert at template programming but I refined it as much as I could.
As far as what I am using to actually make the function wrapper, I will be using JSON for Modern C++ and
C++17 features such as if constexpr
.
The function wrapper should produce an invokable object that will accept a JSON object containing the function paramaters, and return the normal return type of the function. It should have nothing to with routing, response generation, or message processing.
Class structure
Let’s look at the skeleton of the class and break it down from there
We can see that our template class defines the function signature with T
as our return and Args
as our function parameters. Next if
we look at the typedefs, we have ttype
as the tuple type which is applied to the function. After that we have defaults_tup_type
which
is the same as ttype
except all the constituent types are std::optional
. The defaults type is how we define fallbacks if a parameter
is missing, that way we can greatly simplify our RPC and reduce API breakages. We want it to fall back on a default as long as it isn’t
std::nullopt
.
If we look at the constructor, you will see where we pass in our function, an array of parameter names, and the default paramaters. Other than that, our constructor doesn’t do anything.
Here’s an example of how you’d use it:
() Operator
So without further ado, let’s look at at the operator()
function.
As you can see, I’ve implemented a special case here. Sometimes we might want
a function to have more specialized handling of parameters, so if there is only one parameter, and that parameter
is a json
object, the entire params object is passed to it.
After checking for that case, it creates a ttype
tuple and unpacks the params
json object into it, then calls
the function with those arguments using std::apply
.
You can see that it’s starting to take shape now. The last thing to go over is parameter unpacking functions.
Parameter unpacking
In order to unpack the parameters, we basically need to take the json params
object and load it into a tuple, performing all the
necessary conversions so that the function can be called. Let’s look at the implementation:
As with much template programming trickery, we use a recursive function to load the tuple.
The base case can be seen in the ttype unpack_params_
method. It calls the templated version with an empty tuple, which then
recurses until it hits the arg count. The most important thing to note here is the constexpr if
which allows us to avoid having
to do any sort of specializations for the final case.
You can also see that each step calls a separate param_handler_
function. Let’s move on to that.
Param handler
The param handler looks through our params
json for a parameter with param_name
, then converts it to U
and returns it.
You can see we check if the value exists in params
and if not, we check the fallback. If there is no fallback, then
we throw an exception so that the message handler knows how the call failed. We also need to implement special logic for
json
arguments and other types, as the get
function in the JSON library can’t convert to itself.
The most complex part of this is the error handling, as in order to implement the JSON-RPC protocol, we need to be able to get
information back on how exactly a call failed. I created RPCInvalidParamsException
for this, derived from std::runtime_error
,
that way we can catch it specifically for the purposes of correctly generating error messages for the RPC protocol.
And with that, we have a functioning wrapper. At the end of this series I will put a link to the final code, so stay tuned.