The hpp_proto::grpc helpers provide a seamless bridge between hpp-proto's generated message types and the gRPC C++ asynchronous API. They wrap the *.service.hpp descriptors generated by the protoc-gen-hpp plugin, allowing you to implement services and issue RPCs without writing manual serialization or deserialization glue code.
This guide walks you through building a simple unary client and server, then provides references for more advanced streaming scenarios.
The adapter is built around two key components:
hpp_proto::grpc::Stub: A client-side stub that takes agrpc::Channeland your service's generated method tuple (YourService::_methods). It provides acall()method for unary RPCs and anasync_call()method for streaming RPCs.hpp_proto::grpc::CallbackService: A server-side helper that wires your service implementation into agrpc::ServerBuilder. You implement ahandle()method for each RPC, which returns a request handler struct.
Let's build a full "Hello, World" client and server.
First, define the service and messages in a .proto file, like helloworld.proto:
// helloworld.proto
syntax = "proto3";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}When you generate code with protoc-gen-hpp, this will produce helloworld.msg.hpp, helloworld.pb.hpp, and importantly, helloworld.service.hpp, which defines helloworld::Greeter::_methods.
The server implements the Greeter service using hpp_proto::grpc::CallbackService. For each RPC, you define a handler struct that contains the logic to execute when a request is received.
// greeter_server.cpp
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <condition_variable>
#include "helloworld.service.hpp" // Generated by hpp-proto
#include <hpp_proto/grpc/server.hpp>
#include <grpcpp/grpcpp.h>
namespace helloworld {
// Service implementation
class GreeterService : public ::hpp_proto::grpc::CallbackService<GreeterService, Greeter::_methods> {
public:
// Handler for the SayHello RPC
struct SayHelloHandler {
SayHelloHandler(GreeterService&, ::hpp_proto::grpc::ServerRPC<Greeter::SayHello>& rpc,
::hpp_proto::grpc::RequestToken<Greeter::SayHello> token) {
std::cout << "Server received SayHello RPC\n";
HelloRequest request;
auto status = token.get(request); // Deserialize request
if (!status.ok()) {
rpc.finish(status);
return;
}
if (request.name.empty()) {
rpc.finish({::grpc::StatusCode::INVALID_ARGUMENT, "Name is not specified."});
return;
}
using namespace std::string_literals;
HelloReply reply{.message = "Hello, "s + request.name};
// Serialize reply and finish the RPC
rpc.finish(reply);
}
};
// Tell the service to use SayHelloHandler for the SayHello RPC
auto handle(Greeter::SayHello) -> SayHelloHandler;
};
} // namespace helloworld
void RunServer() {
std::string server_address("0.0.0.0:50051");
helloworld::GreeterService service;
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service); // Register the hpp-proto service
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << "\n";
server->Wait();
}
int main() {
RunServer();
return 0;
}The client uses hpp_proto::grpc::Stub to make RPC calls. For unary RPCs, the call() method provides a simple, blocking interface.
// greeter_client.cpp
#include <iostream>
#include <memory>
#include <string>
#include "helloworld.service.hpp" // Generated by hpp-proto
#include <hpp_proto/grpc/client.hpp>
#include <grpcpp/grpcpp.h>
// Define the stub type using the generated methods
using GreeterStub = ::hpp_proto::grpc::Stub<helloworld::Greeter::_methods>;
int main() {
std::string server_address("localhost:50051");
auto channel = grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials());
GreeterStub stub{channel, grpc::StubOptions{}};
// Prepare request and reply messages
helloworld::HelloRequest request;
request.name = "hpp-proto";
helloworld::HelloReply reply;
// The ClientContext allows you to set deadlines, metadata, etc.
::grpc::ClientContext context;
std::cout << "Client sending SayHello RPC to server...\n";
// Make the unary RPC call
auto status = stub.call<helloworld::Greeter::SayHello>(context, request, reply);
if (status.ok()) {
std::cout << "Server replied: \"" << reply.message << "\"\n";
} else {
std::cerr << "RPC failed: " << status.error_code() << ": " << status.error_message() << "\n";
return 1;
}
return 0;
}The adapter also provides powerful, low-level callback reactors for handling client, server, and bidirectional streams.
hpp-proto supports three serialization modes that trade off speed vs. memory when using the gRPC adapter. These modes
apply only to out_sink-based serialization (i.e., when the gRPC adapter writes into grpc::ByteBuffer). Buffer-based
serialization is always contiguous and ignores these options.
- Contiguous (
hpp_proto::contiguous_mode): serialize into a single contiguous slice when possible. Fastest, but requires a full contiguous buffer. - Adaptive (
hpp_proto::adaptive_mode): default behavior; tries contiguous first and falls back to chunked when the message doesn't fit. - Chunked (
hpp_proto::chunked_mode): always emit chunked output. Lowest peak memory, but more overhead.
At the gRPC adapter layer, pass the mode as an option to your request/response handling:
// Server side: choose mode when finishing a response.
rpc.finish(reply, hpp_proto::contiguous_mode);
// Client side: choose mode for a unary call.
auto status = stub.call<helloworld::Greeter::SayHello>(
context, request, reply, hpp_proto::adaptive_mode);The gRPC callback reactors enforce sequencing rules:
- Client streaming: Only one
write()orwrite_last()can be in flight. Wait forOnWriteDone()before issuing the next write. If you callwrite_last(), do not callwrite_done()afterward—write_lastalready marks completion. (Seetests/grpc/client_stream_tests.cpp.) - Bidirectional streaming:
ClientBidiReactorlacksTryCancel(). Store thegrpc::ClientContextyou passed toasync_calland callcontext->TryCancel()when needed. (Seetests/grpc/bidi_stream_tests.cpp.) - Server streaming:
ServerRPC::writeserializes immediately, so you can reuse buffers between writes. - Server read-close handling: For server-side client/bidi streams,
OnReadDone(false)means no more inbound reads. Inhpp_proto::grpc::CallbackService, handlers can distinguish terminal read states with:on_read_eof(rpc_t&) -> boolon_read_cancel(rpc_t&) -> boolIf the callback returnstrue, the handler fully handled the event (for example by callingrpc.finish(...)). If it returnsfalse(or callback is absent), the framework applies fallback finish behavior.
For handlers used by hpp_proto::grpc::CallbackService:
on_read_ok(rpc_t&, RequestToken<Method>)Called for each successful inbound read on client-streaming/bidi methods.on_read_eof(rpc_t&) -> boolOptional. Called on non-cancel terminal read (OnReadDone(false)and!context.IsCancelled()).on_read_cancel(rpc_t&) -> boolOptional. Called on canceled terminal read (OnReadDone(false)andcontext.IsCancelled()).on_write_ok(rpc_t&)Called after a successful async write on server-streaming/bidi methods.on_write_error(rpc_t&) -> boolOptional. Called when write completion fails (OnWriteDone(false)).
Return semantics for boolean callbacks:
true: handler fully handled terminal event.false: framework fallback applies (Finish(CANCELLED/OK/UNKNOWN)depending on state/path).
| Scenario | Reactor | Example |
|---|---|---|
| Unary RPC | (none) | tests/grpc/unary_tests.cpp |
| Client streaming | ClientCallbackReactor<ClientStreamAggregate> |
tests/grpc/client_stream_tests.cpp |
| Server streaming | ClientCallbackReactor<ServerStreamFanout> |
tests/grpc/server_stream_tests.cpp |
| Bidirectional streaming | ClientCallbackReactor<BidiStreamChat> |
tests/grpc/bidi_stream_tests.cpp |
| Method type | Inherits from | Extras | User Overrides |
|---|---|---|---|
| Unary | ::grpc::ClientUnaryReactor |
start_call(), get_response() |
OnReadInitialMetadataDone, OnDone, OnCancel |
| Client streaming | ::grpc::ClientWriteReactor<...> |
write(), write_last(), write_done() |
OnReadInitialMetadataDone, OnWriteDone, OnDone, OnCancel |
| Server streaming | ::grpc::ClientReadReactor<...> |
start_call(), start_read(), get_response() |
OnReadInitialMetadataDone, OnReadDone, OnDone, OnCancel |
| Bidi streaming | ::grpc::ClientBidiReactor<...> |
start_call(), start_read(), write(), etc. |
OnReadInitialMetadataDone, OnReadDone, OnWriteDone, OnDone, OnCancel |
| Method type | Inherits from | Extras | Handler Hooks |
|---|---|---|---|
| Unary | ::grpc::ServerUnaryReactor |
RequestToken::get(), finish(response) |
on_send_initial_metadata_done, on_done, on_cancel |
| Client streaming | ::grpc::ServerReadReactor<...> |
start_read(), RequestToken::get(), finish(response) |
on_send_initial_metadata_done, on_read_ok, on_read_eof (optional, bool), on_read_cancel (optional, bool), on_done, on_cancel |
| Server streaming | ::grpc::ServerWriteReactor<...> |
write(response), finish(status) |
on_send_initial_metadata_done, on_write_ok, on_write_error (optional, bool), on_done, on_cancel |
| Bidirectional streaming | ::grpc::ServerBidiReactor<...> |
start_read(), write(response), finish(status) |
on_send_initial_metadata_done, on_read_ok, on_read_eof (optional, bool), on_read_cancel (optional, bool), on_write_ok, on_write_error (optional, bool), on_done, on_cancel |
tutorial/grpc/greeter_client.cpp: Synchronous unary and asynchronous streaming clients.tutorial/grpc/greeter_server.cpp: Callback handlers with examples of streaming hooks.