Skip to content

Latest commit

 

History

History
253 lines (188 loc) · 11 KB

File metadata and controls

253 lines (188 loc) · 11 KB

Using hpp-proto with gRPC

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.

Core Concepts

The adapter is built around two key components:

  • hpp_proto::grpc::Stub: A client-side stub that takes a grpc::Channel and your service's generated method tuple (YourService::_methods). It provides a call() method for unary RPCs and an async_call() method for streaming RPCs.
  • hpp_proto::grpc::CallbackService: A server-side helper that wires your service implementation into a grpc::ServerBuilder. You implement a handle() method for each RPC, which returns a request handler struct.

A Complete Unary RPC Example

Let's build a full "Hello, World" client and server.

1. The .proto File

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.

2. The Server Implementation

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;
}

3. The Client Implementation

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;
}

Advanced Usage: Streaming RPCs

The adapter also provides powerful, low-level callback reactors for handling client, server, and bidirectional streams.

Serialization Modes (out_sink only)

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);

Streaming Best Practices

The gRPC callback reactors enforce sequencing rules:

  • Client streaming: Only one write() or write_last() can be in flight. Wait for OnWriteDone() before issuing the next write. If you call write_last(), do not call write_done() afterward—write_last already marks completion. (See tests/grpc/client_stream_tests.cpp.)
  • Bidirectional streaming: ClientBidiReactor lacks TryCancel(). Store the grpc::ClientContext you passed to async_call and call context->TryCancel() when needed. (See tests/grpc/bidi_stream_tests.cpp.)
  • Server streaming: ServerRPC::write serializes 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. In hpp_proto::grpc::CallbackService, handlers can distinguish terminal read states with:
    • on_read_eof(rpc_t&) -> bool
    • on_read_cancel(rpc_t&) -> bool If the callback returns true, the handler fully handled the event (for example by calling rpc.finish(...)). If it returns false (or callback is absent), the framework applies fallback finish behavior.

Server Handler Callback Contract

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&) -> bool Optional. Called on non-cancel terminal read (OnReadDone(false) and !context.IsCancelled()).
  • on_read_cancel(rpc_t&) -> bool Optional. Called on canceled terminal read (OnReadDone(false) and context.IsCancelled()).
  • on_write_ok(rpc_t&) Called after a successful async write on server-streaming/bidi methods.
  • on_write_error(rpc_t&) -> bool Optional. 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).

Streaming Reference (see tests/grpc for full examples)

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

ClientCallbackReactor API Summary

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

ServerRPC API Summary

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 References

  • tutorial/grpc/greeter_client.cpp: Synchronous unary and asynchronous streaming clients.
  • tutorial/grpc/greeter_server.cpp: Callback handlers with examples of streaming hooks.