The client interface is different from the server interface. Here we don't override an interface. In stead, we call methods on a stub interface, much like we did in the async version.
The generated code from rpcgen for "callbacks" looks like this:
public StubInterface::async_interface ;The unary RPC, GetFeature, has two variants, one taking a std::function<> as argument. We will use that in our example.
The methods we get will initiate an async request to the server. The varrious events that happen then are handled by whatever we put in as tha last argument. For the unary methods where we supply a function, it will be called once when the RPC is complete. That is conveniant. The other methods reqire us to pass a class that overrides the events so we can send/reveive data over the stream.
In the code below, I have wrapped these methods in more conveniant methods, where we can call the RPC and interact with it's events via callbacks. That's well known pattern for most C++ developers.
I have also added methods that shows how to use the wrapper methods.
Let's start with the initialization until we have a usable stub_ where we can call RPC's.
GetFeature
Now, lets look at the simplest, unary wrapper method.
We use a class to keep the buffers and state for the RPC. This could have been handled by the caller in this case. However, if you want to call some RPC's from around in you code, it may add a burden to add shared pointers or other means to own buffers. It's simpler to hide these details from the caller and deal with it in the wrapper.
/// Callback function with the result of the unary RPC call using get_feature_cb_t = std::function<void const ::routeguide::Feature& feature)>; /*! Naive implementation of `GetFeature` with an actual callback. */ void // getFeatureThen an example on how to use this method.
/*! Example on how to use getFeature() */ void Now these unary gRPC calls are becoming conveniant to use even when they are asyncroneous. That's something new ;)
ListFeatures
We continue with a wrapper around ListFeatures.
/*! Data for a callback function suitable for `ListFeatures`. * * Here we use a union that either contains a Feature message received * from the stream, or the final Status message. Alternatively we could have * used two callbacks. However, std::variant (C++ unions) can be useful in * such cases as this. * * We use a pointer to the Feature so that we don't have to make a deep * copy for each received message just for the purpose of "doing the right thing" ;) */ using feature_or_status_t = std::variant< // I would have preferred a reference, but that don't work in C++ 20 in variants const ::routeguide::Feature *, grpc::Status >; /*! A callback function suitable to handle the events following a ListFeatures RPC */ using list_features_cb_t = std::function<void>; /*! Naive implementation of `ListFeatures` with a callback for the events. */ void // listFeaturesThat was more code. Now let's look at how to use this wrapper:
/*! Example on how to use listFeatures() */ void Now, look at that! That's a neat callback interface to a stream RPC!
RecordRoute
This wrapper is similar to the previous one. Just with the stream in the other direction.
/*! Definition of a callback function that need to provide the data for a write. * * The function must return immediately. * * \param point. The buffer for the data. * \return true if we are to write data, false if all data has been written and * we are done. */ using on_ready_to_write_point_cb_t = std::function<bool>; /*! Definition of a callback function that is called when the RPC is complete. * * \param status. The status for the RPC. * \param summary. The reply from the server. Only valid if `staus.ok()` is true. */ using on_done_route_summary_cb_t = std::function< void>; /*! Naive implementation of `RecordRoute` with callbacks for the events. */ void // recordRouteThen an example on how to use the wrapper:
/*! Example on how to use recordRoute() */ void In this example we have some logic to just send n messages. But as you can see, the use of the wrapper is fairly simple.
RouteChat
Finally, the wrapper over a RPC with a bidirectional stream using callbacks.
/*! Definition of a callback function to provide the next outgoing message. * * The function must return immediately. * * \param msg Buffer for the data to send when the function returns. * \return true if we are to send the message, false if we are done * sending messages. */ using on_say_something_cb_t = std::function<bool>; /*! Definition of a callback function regarding an incoming message. * * The function must return immediately. */ using on_got_message_cb_t = std::function<void>; /*! Definition of a callback function to notify us that the RPC is complete. * * The function must return immediately. */ using on_done_status_cb_t = std::function<void>; /*! Naive implementation of `RecordRoute` with callbacks for the events. * * As before, we are prepared for a shouting contest, and will start sending * message as soon as the RPC connection is established. * * \param outgoing Callback function to provide new messages to send. * \param incoming Callback function to notify us about an incoming message. * \param done Callcack to inform us that the RPC is completed, and if * it was successful. */ void // routeChatAnd the example on how to use the wrapper.
/*! Example on how to use routeChat() */ void Now, with this abstraction, it's simple to use a bidirectional stream in an actual project.
I wrote the wrappers to highlight what I believe is a proper way to expose a "callback" interface.
When I write code, I try to keep one method (or class) focused on one thing. If I write a method that insert date into a database, the method should compose the data. The algorithm in that method should be clear for anyone that took a peek at the code. That means that the same method should not deal with the specifics of the database, like opening a handle, deal with connects or re-connects and close the handle in all of the i return paths. When I deal with the data, the database operation should be limited to something like db.write(data);.
Likewise when using RPC's. Some methods may go in detail on how to use the gRPC stubs. But when I need to use one of those RPC's, I don't want the details. I want just what I need to implement my algorithm - in this case for example an event-handler to provide more data, and an event-handler when it's done.
That concludes the dive into the gRPC callback interface for now.