Skip to content

KMX-Systems/kmx-grpc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kmx-grpc: C++26 gRPC Wrapper Library

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.

Features

  • Header-only library — depends only on kmx-grpc-lib QBS product
  • std::generator-based streaming — server and client streaming via C++26 coroutines
  • Type-safe templatesgrpc_stub and service concepts with strong result types
  • Four RPC modes — unary, client-streaming, server-streaming, bidirectional
  • Protobuf-free — samples use custom grpc::SerializationTraits specializations
  • Options sub-namespaceserver::options::* structs for ServerBuilder configuration
  • Modern C++26 — pack indexing, std::expected, std::generator, RTTI disabled

Project Structure

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

Building

Prerequisites

  • 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

Default Build

Builds all targets: library, unit tests, integration tests, sample server, and sample client.

qbs build -f kmx-grpc.qbs -d build/debug config:debug

Standalone RPC Modes Sample

An 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

Running

Tests

# 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

Sample (two processes)

# 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-client

Both processes communicate at 127.0.0.1:50051 using the generic gRPC async API with a text-encoded int32 wire format (no protobuf).

Key APIs

client::manager<Stub>

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

server::manager

#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.

Server-side stream wrappers

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

server::completion_queue_tag

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 { /* ... */ }
};

Build Configuration

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.

Architecture & Design

Strengths

  1. Clean Async API Design
  • Eliminates completion-queue event loop complexity behind coroutine co_await syntax
  • Async-only surface (no blocking methods) removes ambiguity and simplifies API
  • All RPC operations return kmx::aio::task<result<T>> consistently
  1. 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
  • requires constraints on _auto() overloads route method pointers vs. string paths at compile-time
  • std::expected-based error handling avoids exceptions in most paths
  1. Symmetrical Client/Server Design
  • Reader/writer/bidirectional stream wrappers mirror on both sides
  • Consistent operation dispatch via completion_queue_tag interface
  • RAII semantics for context/stream lifetime (shared_ptr, unique_ptr)
  1. 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
  1. Thread-Safe Operation Dispatch
  • operation_awaitable mixin with std::scoped_lock ensures safe coroutine resumption
  • Read/write/finish operations properly isolated

Design Decisions

  1. 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
  1. 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::SerializationTraits for extensibility without protobuf coupling
  1. Universal Error Handling
  • All await_resume() implementations return result<T> (std::expected)
  • Failures map to UNAVAILABLE status code (conservative default)
  • Empty optional<Response> when stream read fails (idiomatic EOF handling)
  1. Context Lifetime Management
  • context_t held as shared_ptr in stream wrappers
  • Prevents premature cleanup while user holds stream reference
  • Small overhead but correct async safety semantics

Performance Considerations

  • Mutex Overhead: operation_awaitable uses std::scoped_lock on every await_ 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.

License

Copyright (C) 2026 - present KMX Systems. All rights reserved.

About

Modern C++26 header-only gRPC wrapper library. It depends on KMX AIO library.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors