asio
asio copied to clipboard
Absence of the pass through await_transform() method for promise type
@vignatyuk commented on Mar 18, 2020, 8:25 PM UTC:
Absence of the pass through await_transform() method for an unknown awaitable types blocks usage of the user defined coroutine friendly completion handlers.
I propose to change the only available pass through await_transform() method from:
template template auto boost::asio::awaitable_frame_base::await_transform(awaitable<T, Executor> a) const { return a; }
to the
template template <typename T, typename = std::enable_if_t<is_awaitable_v>> decltype(auto) boost::asio::awaitable_frame_base::await_transform(T&& a) const noexcept { return std::forward(a); }
Second default argument through std::enable_if_t<> may be removed if new await_transform() method does not break any other overloads. There is no standard is_awaitable_v check, so it should be defined somewhere, though it's easy.to implement.
This issue was moved by chriskohlhoff from boostorg/asio#331.
I second this, I would like to retrieve an boost::system::error_code instead of catching an exception when reading or writing.
I see a simple solution which involves defining a custom awaitable:
auto asyncRead(tcp::socket &socket, boost::asio::mutable_buffer buffer) {
struct Awaitable
{
struct Result
{
boost::system::error_code errorCode;
std::size_t bytesRead;
};
tcp::socket &socket;
boost::asio::mutable_buffer buffer;
Result result;
bool await_ready()
{
return false;
}
void await_suspend(std::coroutine_handle<> awaitingCoroutine)
{
this->socket.async_read_some(
this->buffer,
[this, awaitingCoroutine](boost::system::error_code errorCode, std::size_t bytesRead) mutable {
this->result.errorCode = errorCode;
this->result.bytesRead = bytesRead;
awaitingCoroutine.resume();
});
}
Result await_resume()
{
return this->result;
}
};
return Awaitable{.socket = socket, .buffer = buffer};
};
...
boost::asio::awaitable<void> echo(tcp::socket socket)
{
try {
char buffer[1024];
while (true) {
auto result = co_await asyncRead(socket, boost::asio::buffer(buffer));
if (result.errorCode) {
std::cout << "async_read: " << result.ec.message() << std::endl;
} else {
co_await boost::asio::async_write(socket, boost::asio::buffer(buffer, result.bytesRead), boost::asio::use_awaitable);
}
}
} catch (std::exception &ex) {
std::cout << "echo exception: " << ex.what() << std::endl;
}
}
The problem here is the missing pass-through await_transform(...) of boost::asio::awaitable<> I guess.
The OP's proposed added overload above doesn't work with how ASIO dispatches coroutines, which involves pumping a stack and with which each awaitable needs to register for things to work.
What would work however is replacing await_transform(awaitable<T, Executor>) with reference passthroughs:
template <typename T>
awaitable<T, Executor>& await_transform(awaitable<T, Executor>& a) const {
if(attached_thread_->entry_point()->throw_if_cancelled_)
if(!!attached_thread_->get_cancellation_state().cancelled())
do_throw_error(boost::asio::error::operation_aborted, "co_await");
return a;
}
template <typename T>
awaitable<T, Executor>&& await_transform(awaitable<T, Executor>&& a) const {
if(attached_thread_->entry_point()->throw_if_cancelled_)
if(!!attached_thread_->get_cancellation_state().cancelled())
do_throw_error(boost::asio::error::operation_aborted, "co_await");
return std::move(a);
}
Now people can create their own awaitable types inheriting from awaitable<T, Executor>, and write their own custom logic into await_ready(), await_suspend() and await_resume(). This is very useful for writing efficient state machines whereby you specifically know which ASIO coroutine in the stack needs resuming, which then saves ASIO doing a linear pass over all suspended coroutines in the awaitable thread's stack polling each to decide which ought to be resumed.
The OP's proposed added overload above doesn't work with how ASIO dispatches coroutines, which involves pumping a stack and with which each awaitable needs to register for things to work.
What would work however is replacing
await_transform(awaitable<T, Executor>)with reference passthroughs:template <typename T> awaitable<T, Executor>& await_transform(awaitable<T, Executor>& a) const { if(attached_thread_->entry_point()->throw_if_cancelled_) if(!!attached_thread_->get_cancellation_state().cancelled()) do_throw_error(boost::asio::error::operation_aborted, "co_await"); return a; } template <typename T> awaitable<T, Executor>&& await_transform(awaitable<T, Executor>&& a) const { if(attached_thread_->entry_point()->throw_if_cancelled_) if(!!attached_thread_->get_cancellation_state().cancelled()) do_throw_error(boost::asio::error::operation_aborted, "co_await"); return std::move(a); }Now people can create their own awaitable types inheriting from
awaitable<T, Executor>, and write their own custom logic intoawait_ready(),await_suspend()andawait_resume(). This is very useful for writing efficient state machines whereby you specifically know which ASIO coroutine in the stack needs resuming, which then saves ASIO doing a linear pass over all suspended coroutines in the awaitable thread's stack polling each to decide which ought to be resumed.
Could you please give me an example? What's the correct way of inheriting awaitable and customizing await_suspend logic?