I'm currently working on a project that uses boost asio for networking. I want to implement a function that look like this:
template<command T, response R = typename T::response_type>
auto send_command(T c) -> boost::asio::awaitable<R> {
// ...
}
In my code, when I'm writing the command to the socket, I will eventually receive a response with the result. I may receive any number of other responses in the socket before getting the response to a specific command.
I figured I could use async_initiate to make sending a command an asynchronous operation composed of a write and a read.
I've already used async_initiate to start asynchronous operations, but it was of the type boost::asio::awaitable<void>. And the code looked like this:
// returns the awaitable for the coroutine to suspend
return asio::async_initiate<CompletionToken, void(void)>(
[self = shared_from_this()](auto&& handler) {
self->callback_queue.push(std::forward<decltype(handler)>(handler));
}
);
// later
auto& resume = callback_queue.front();
resume(); // resume the suspended coroutine
But I can't figure where to put the return value here when It's not void!
auto const command_id = std::int32_t{...};
auto& resume_command = commands_continuations[command_id];
resume_command(response_result); // Send the `co_await` result in parameters??
I'm simply confused how to send the return of the awaitable and how to initiate the asynchronous operation with a result.
It turns out that there are a number of fixed signature a async operation can take.
The void(void) is one, but to have a result, we can simply use void(T):
return asio::async_initiate<CompletionToken, void(R)>(
[self = shared_from_this()](auto&& handler) {
self->callback_queue.push(std::forward<decltype(handler)>(handler));
}
);
The handler will be a callable that takes a R as parameter and will become the result of the co_await operation.
To have exception handling, cancellation, I tried two different ways to propagate exceptions. One is to send an std::exception_ptr and the other was to use boost::system::error_code. In my case, I choose error code as I'm more comfortable using them.
return asio::async_initiate<CompletionToken, void(boost::system::error_code, R)>(
[self = shared_from_this()](auto&& handler) {
self->callback_queue.push(std::forward<decltype(handler)>(handler));
}
);
The callback take form of a move only function that matches the signature void(boost::system::error_code, R). I call it like this:
auto callback = std::exchange(callback_queue[command_id], {});
if (result) {
// resumes the coroutine normally with the result
callback(boost::system::error_code{}, *result);
} else {
// Throws exception in the coroutine
callback(make_error_code(boost::system::errc::operation_canceled), R{});
}
There is more information about it in the documentation about ASIO/Networking TS
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With