A modern C++26 header-only gRPC wrapper library with type-safe client and server APIs and generator-based streaming, built on top of the official gRPC C++ runtime.
- Header-only library — depends only on
kmx-grpc-libQBS product std::generator-based streaming — server and client streaming via C++26 coroutines- Type-safe templates —
grpc_stubandserviceconcepts with strong result types - Four RPC modes — unary, client-streaming, server-streaming, bidirectional
- Protobuf-free — samples use custom
grpc::SerializationTraitsspecializations - Options sub-namespace —
server::options::*structs forServerBuilderconfiguration - Modern C++26 — pack indexing,
std::expected,std::generator, RTTI disabled
source/
library/
inc/kmx/grpc/
common.hpp # Concepts, result<T>, status_t
client.hpp # client::manager<Stub>
client_types.hpp # Client-side type aliases
server/
manager.hpp # server::manager, server::options::*
types.hpp # Server-side type aliases
stream.hpp # Umbrella for stream wrappers
stream/
reader.hpp # server::stream::reader<Req>
writer.hpp # server::stream::writer<Res>
bidirectional.hpp # server::stream::bidirectional<Req, Res>
lib.qbs # Header-only library QBS product
library-test/
unit-test/ # Static concept and type checks (Catch2)
integration-test/ # In-process four-mode RPC tests (Catch2)
sample/
common/inc/sample_wire.hpp # Shared address, paths, ByteBuffer codec
server/ # Standalone async server process
client/ # Standalone gRPC client process
rpc_modes/ # Standalone combined in-process sample
- C++26-compatible compiler (tested with GCC 15.2.0)
- QBS 2.1.2+
- gRPC C++ libraries installed (tested at
/home/io/.local) - Catch2 v3 for tests
Builds all targets: library, unit tests, integration tests, sample server, and sample client.
qbs build -f kmx-grpc.qbs -d build/debug config:debugAn independent in-process sample that does not use the default project file:
qbs build -f source/sample/rpc_modes/rpc-modes-sample.qbs -d build/rpc-modes config:debug# Unit tests — static concept and type alias checks
./build/debug/debug/kmx-grpc-unit-test/kmx-grpc-unit-test
# Integration tests — four-mode in-process RPC round-trips
./build/debug/debug/kmx-grpc-integration-test/kmx-grpc-integration-test# Terminal 1 — start the async generic server
./build/debug/debug/kmx-grpc-sample-server/kmx-grpc-sample-server
# Terminal 2 — run the client loop
./build/debug/debug/kmx-grpc-sample-client/kmx-grpc-sample-clientBoth processes communicate at 127.0.0.1:50051 using the generic gRPC async API with a text-encoded int32 wire format (no protobuf).
#include <kmx/grpc/client.hpp>
auto channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
kmx::grpc::client::manager<YourStub> rpc(channel);
// All RPC operations are asynchronous (coroutine-based)
// Unary — async via generic stream path
auto result = co_await rpc.unary<Request, Response>("/pkg.Service/Method", request);
// Unary — typed method pointer
auto result = co_await rpc.unary<Request, Response>(&YourStub::Method, request);
// Server-streaming — typed method pointer
auto reader = co_await rpc.stream_in(&YourStub::ServerStream, request);
while (auto item = co_await reader.read()) {
if (item) process(*item);
}
// Client-streaming — typed method pointer
auto writer = co_await rpc.stream_out(&YourStub::ClientStream, response);
co_await writer.write(request);
co_await writer.writes_done();
auto status = co_await writer.finish();
// Bidirectional
auto stream = co_await rpc.bidi(&YourStub::BiDi);
co_await stream.write(request);
auto response = co_await stream.read();Call options (e.g. timeout, metadata) are passed as variadic trailing arguments:
opts...[0]—duration_type(deadline)opts...[1]—std::vector<std::pair<std::string, std::string>>(gRPC metadata headers)
#include <kmx/grpc/server/manager.hpp>
kmx::grpc::server::manager srv;
auto result = srv.build(
"0.0.0.0:50051", service, /*cq_count=*/1,
kmx::grpc::server::options::max_receive_message_size{4 * 1024 * 1024},
kmx::grpc::server::options::default_compression_level{GRPC_COMPRESS_LEVEL_HIGH}
);
srv.wait();
srv.shutdown(std::chrono::milliseconds{500});build() returns result<void> (std::expected<void, grpc::Status>). The destructor calls shutdown() automatically.
#include <kmx/grpc/server/stream.hpp>
// In a server-streaming handler:
kmx::grpc::server::stream::writer<Res> out(writer_ptr);
out.send_all(source_generator); // returns result<void>
// In a client-streaming handler:
kmx::grpc::server::stream::reader<Req> in(reader_ptr);
for (Req req : in.receive()) { /* ... */ }
// In a bidirectional handler:
kmx::grpc::server::stream::bidirectional<Req, Res> stream(rw_ptr);
for (Req req : stream.receive())
stream.send(make_response(req));Implement this interface for custom async dispatch over server::manager's completion queues:
struct my_tag : kmx::grpc::server::completion_queue_tag {
void proceed(bool ok) noexcept(false) override { /* ... */ }
};All QBS products use C++26 (c++26) with -fno-rtti. The sample and integration-test products link the full static gRPC dependency set (absl, upb, protobuf, cares, re2, ssl, crypto) via driverLinkerFlags defined in source/sample/grpc_linker_flags.js.
- Clean Async API Design
- Eliminates completion-queue event loop complexity behind coroutine
co_awaitsyntax - Async-only surface (no blocking methods) removes ambiguity and simplifies API
- All RPC operations return
kmx::aio::task<result<T>>consistently
- Strong Type Safety
- Concepts (
grpc_stub,service,duration_type) enforce compile-time contracts - Template method pointer typedefs (
stream_in_method_t,stream_out_method_t,bidi_method_t) prevent misuse requiresconstraints on_auto()overloads route method pointers vs. string paths at compile-timestd::expected-based error handling avoids exceptions in most paths
- Symmetrical Client/Server Design
- Reader/writer/bidirectional stream wrappers mirror on both sides
- Consistent operation dispatch via
completion_queue_taginterface - RAII semantics for context/stream lifetime (shared_ptr, unique_ptr)
- Dual-Path RPC Routing (Elegant)
- Typed method pointer overloads for compile-time safety
- Generic string-path overloads for dynamic dispatch
- Both paths coexist without naming collisions
- Thread-Safe Operation Dispatch
operation_awaitablemixin withstd::scoped_lockensures safe coroutine resumption- Read/write/finish operations properly isolated
- Pack Indexing Usage (C++26 Feature)
- Modern C++26 idiom for variadic argument selection in
apply_options() - Requires GCC 15.2+; forward-looking design choice
- Enables compile-time call options routing
- Generic Path Serialization (ByteBuffer Handling)
- Generic RPC path manually serializes requests and handles
ByteBuffer - More verbose than typed path but necessary for dynamic method dispatch
- Uses
::grpc::SerializationTraitsfor extensibility without protobuf coupling
- Universal Error Handling
- All
await_resume()implementations returnresult<T>(std::expected) - Failures map to
UNAVAILABLEstatus code (conservative default) - Empty
optional<Response>when stream read fails (idiomatic EOF handling)
- Context Lifetime Management
context_theld asshared_ptrin stream wrappers- Prevents premature cleanup while user holds stream reference
- Small overhead but correct async safety semantics
- Mutex Overhead:
operation_awaitableusesstd::scoped_lockon everyawait_call. Single-threaded completion-queue dispatch makes this negligible; consider lock-free variants (atomics) if CQ processing becomes a bottleneck under extreme concurrency. - Memory: Stream wrappers allocate
shared_ptr<ClientContext>per RPC; typical 200-300 bytes per active call. - Latency: Coroutine suspension is zero-cost compared to raw callback dispatch; CQ event latency dominates end-to-end timing.
Copyright (C) 2026 - present KMX Systems. All rights reserved.