Implementing a gRPC Server API in modern C++ — Devlog #5

Uditha Atukorala
3 min readAug 14, 2023

--

This devlog is about the journey of grpcxx — an attempt to build a better gRPC Server API using modern C++ (C++20).

In the previous devlogs, I described how I used template classes to define C++ types for RPC methods and gRPC services (devlog #3) and how I used those templates classes in the gRPC server implementation to dispatch RPC requests (devlog #4).

Let’s look into how to put it all together and implement a basic Hello World gRPC service…

If you are keen to look at the code first, it’s in the examples/helloworld folder.

Hello World 👋

A gRPC service starts with a Protobuf definition. Taking inspiration from the official helloworld.proto example but also considering a production use-case with package versioning I defined the following service;

Listing 1 — greeter.proto

syntax = "proto3";

package helloworld.v1;

service Greeter {
rpc Hello(GreeterHelloRequest) returns (GreeterHelloResponse) {}
}

message GreeterHelloRequest {
string name = 1;
}

message GreeterHelloResponse {
string message = 1;
}

Now I can use the above .proto file and the protobuf compiler (protoc) to generate the C++ classes for the GreeterHelloRequest and GreeterHelloResponse messages. Since I’m using CMake, I used a custom build rule to generate the necessary code as part of the build process.

If you are curious, Protocol Buffers basics tutorial for C++ explains the message definitions and code generation in more detail.

Normally, the protobuf compiler would also generate code for the gRPC service (Reference: gRPC C++ basics tutorial). But I haven’t implemented a generator (yet) so let’s write the service code.

namespace helloworld {
namespace v1 {
namespace Greeter {
using rpcHello =
grpcxx::rpc<"Hello", helloworld::v1::GreeterHelloRequest, helloworld::v1::GreeterHelloResponse>; // [1]

using Service = grpcxx::service<"helloworld.v1.Greeter", rpcHello>; // [2]

struct ServiceImpl { // [3]
template <typename T> typename T::result_type call(const typename T::request_type &) {
return {grpcxx::status::code_t::unimplemented, std::nullopt};
}
};
}; // namespace Greeter
} // namespace v1
} // namespace helloworld

[1] — Type alias to identify the Hello RPC method.

[2] — Type alias to identify the Greeter service (with one RPC method identified by rpcHello type alias).

[3] — A convenient class for the service implementation, which returns an UNIMPLEMENTED gRPC status for all calls.

It’s only a few lines of code, which in my opinion is much easier to read an understand compared to around 330 lines of code generated by the official grpc_cpp_plugin.

The code to run a gRPC server would be as simple as;

using namespace helloworld::v1::Greeter;

int main() {
ServiceImpl greeter;
Service service(greeter); // instantiate a service using the convenient implementation

grpcxx::server server;
server.add(service);
server.run("127.0.0.1", 7000);

return 0;
}
❯ grpcurl -proto examples/helloworld/proto/helloworld/v1/greeter.proto -plaintext localhost:7000 helloworld.v1.Greeter/Hello
ERROR:
Code: Unimplemented
Message:

But it’s only returning an UNIMPLEMENTED response for the moment since it’s missing the application code that would implement the RPC methods.

The application code implementing RPC methods can simply specialise the call<>() template function in ServiceImpl{}.

using namespace helloworld::v1::Greeter;

// Implement rpc application logic using template specialisation for `ServiceImpl`
template <> rpcHello::result_type ServiceImpl::call<rpcHello>(const GreeterHelloRequest &req) {
GreeterHelloResponse res;
res.set_message("Hello `" + req.name() + "` 👋");
return {grpcxx::status::code_t::ok, res};
}

Or,

Since not all applications are cut from the same cloth, there’s also the freedom to use an application defined implementation.

using namespace helloworld::v1::Greeter;

// Application defined implementation
struct GreeterImpl {
template <typename T> typename T::result_type call(const typename T::request_type &) {
return {grpcxx::status::code_t::unimplemented, std::nullopt};
}

template <>
rpcHello::result_type call<rpcHello>(const helloworld::v1::GreeterHelloRequest &req) {
helloworld::v1::GreeterHelloResponse res;
res.set_message("Hello `" + req.name() + "` 👋");
return {grpcxx::status::code_t::ok, res};
}
};

int main() {
GreeterImpl greeter;
Service service(greeter);

grpcxx::server server;
server.add(service);
server.run("127.0.0.1", 7000);

return 0;
}
❯ grpcurl -proto examples/helloworld/proto/helloworld/v1/greeter.proto -plaintext -d '{"name": "World"}' localhost:7000 helloworld.v1.Greeter/Hello
{
"message": "Hello `World` 👋"
}

Releasing the application code from the shackles of official gRPC C++ API has been my main motivation for this project and I’m quite happy how it has turned out so far.

Next time, Is it too slow? 🐌

--

--

Uditha Atukorala

Helping tech startups to succeed 🚀 / Founder / CTO / Advisor