the logical flow.**
# Promise and Future: Bridging Callbacks and Coroutines
This is exactly where [asyncio](https://github.com/Hackerl/asyncio) shines.
`asyncio` is an async framework built on C++20 coroutines and the `libuv` event loop. Its core design idea is simple: use `Promise` and `Future` as a bridge to connect the callback world and the coroutine world.
A `Promise` is an object that can be "resolved" or "rejected". `Future` is its other face — you can `co_await` a `Future`, and the coroutine will automatically resume when the `Promise` is resolved.
Take the `sleep` function as an example, to see how `asyncio` does it:
asyncio::task::Task<void> asyncio::sleep(std::chrono::milliseconds ms) {
Promise<void> promise;
uv_timer_start(timer, [](uv_timer_t *handle) {
// Timer fires, resolve the promise, coroutine resumes
static_cast<Promise<void> *>(handle->data)->resolve();
}, ms.count(), 0);
co_return co_await promise.getFuture();
}
Apply the same treatment to gRPC callbacks: `resolve` or `reject` a `Promise` inside the callback, and `co_await` the corresponding `Future` on the coroutine side. This lets you write what would otherwise require nested callbacks as straight-line code.
# Cancellation Support
Coroutine cancellation is also implemented through the `Promise` mechanism. `asyncio` provides a `Cancellable` wrapper that takes a `Future` and a cancellation function:
co_return co_await asyncio::task::Cancellable{
promise.getFuture(),
[&]() -> std::expected<void, std::error_code> {
// Perform the actual cancellation here
context->TryCancel();
return {};
}
};
When external code calls `task.cancel()`, `asyncio` walks the task chain to find the `Cancellable` currently being awaited and executes its cancellation function. For gRPC, the cancellation function simply calls `context->TryCancel()`. gRPC then handles the cleanup, triggers the `OnDone` callback, the `Promise` is eventually rejected, and the coroutine ends with a cancellation error.
With this mechanism, we can cleanly add cancellation support to any gRPC async operation without introducing any special state variables into business code.
# Defining the Sample Service
Before writing code, let's define a sample service covering all four RPC types:
syntax = "proto3";
package sample;
service SampleService {
// Unary RPC: simplest request-response pattern
rpc Echo(EchoRequest) returns (EchoResponse);
// Server streaming: server continuously pushes data to the client
rpc GetNumbers(GetNumbersRequest) returns (stream Number);
// Client streaming: client continuously sends data, server returns one result
rpc Sum(stream Number) returns (SumResponse);
// Bidirectional streaming: both sides can send and receive continuously
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message EchoRequest { string message = 1; }
message EchoResponse { string message = 1; int64 timestamp = 2; }
message GetNumbersRequest { int32 value = 1; int32 count = 2; }
message Number { int32 value = 1; }
message SumResponse { int32 total = 1; int32 count = 2; }
message ChatMessage { string user = 1; string content = 2; int64 timestamp = 3; }
Each RPC pattern has its use: `Echo` is the canonical RPC, `GetNumbers` lets the server stream a batch of data, `Sum` lets the client stream data and get an aggregated result, and `Chat` is the most complex — a bidirectional stream where either side can send at any time.
Let's implement them one by one, starting with the simplest: client Unary RPC.
# Client Unary RPC
Unary RPC is the most straightforward: send one request, receive one response. The corresponding gRPC Reactor method signature is:
void Stub::async::Echo(
grpc::ClientContext *,
const EchoRequest *,
EchoResponse *,
std::function<void(grpc::Status)>
);
gRPC calls the
Post #25032
12