Reputation: 3197
I have a composed async operation which uses non-boost Asio 1.18.1 to resolve and connect to a host and service.
I want it to pass the actual endpoint it connects to, to the completion token. Right now it does not.
#include <iostream>
#include <string_view>
#include "asio.hpp"
template <typename Token, typename Executor>
auto async_connect_to(Executor&& executor, asio::ip::tcp::socket& socket,
asio::ip::tcp::resolver& resolver, std::string_view host,
std::string_view service, Token&& token) {
return asio::async_compose<Token, void()>(
[&](auto& self) {
co_spawn(
executor,
[&]() -> asio::awaitable<void> {
auto results = co_await resolver.async_resolve(host, service, asio::use_awaitable);
auto endpoint = co_await asio::async_connect(socket, results, asio::use_awaitable);
self.complete();
co_return;
},
asio::detached);
},
token, executor);
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: client <host> <port>\n";
return 1;
}
asio::io_context io_context;
asio::ip::tcp::socket socket(io_context);
asio::ip::tcp::resolver resolver(io_context);
async_connect_to(io_context, socket, resolver, argv[1], argv[2],
[](asio::ip::tcp::endpoint endpoint = {}) {
std::cout << "connected to: " << endpoint << "\n";
});
try {
io_context.run();
} catch (std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
return 1;
}
return 0;
}
Because I'm doing self.complete()
, the completion handler in main
never actually gets the endpoint we connected to so it's defaulted.
(error_code, tcp::endpoint)
instead of just (tcp::endpoint)
, i.e. so that it works the same way as built-in async operations? (I believe this is straightforward after the first question is answered)The docs do not provide an example with async_compose and coroutines, nor with returning a value from co_spawn
.
Upvotes: 3
Views: 1162
Reputation: 3197
I thought I had already tried this, but just changing the signature of the async_compose
template parameter and self.complete
works.
For errors it seems like you need to explicitly catch the system_error
, pass it as a error_code
to the completion handler, and it may be either passed to the completion handler of the user or converted back into a system_error
with something like a use_future
token.
As Andrew suggested in the comments, I also move-capture self
into the coroutine and make the lambda mutable.
template <typename Token, typename Executor>
auto async_connect_to(Executor&& executor, asio::ip::tcp::socket& socket,
asio::ip::tcp::resolver& resolver, std::string_view host,
std::string_view service, Token&& token) {
return asio::async_compose<Token,
void(std::error_code, asio::ip::tcp::endpoint)>(
[&](auto& self) {
co_spawn(
executor,
[&, self = std::move(self)]() mutable -> asio::awaitable<void> {
try {
auto results = co_await resolver.async_resolve(
host, service, asio::use_awaitable);
auto endpoint = co_await asio::async_connect(
socket, results, asio::use_awaitable);
self.complete({}, endpoint);
} catch (std::system_error& e) {
self.complete(e.code(), {});
}
co_return;
},
asio::detached);
},
token, executor);
}
This seems to work fine like this:
async_connect_to(
io_context, socket, resolver, argv[1], argv[2],
[](std::error_code ec, asio::ip::tcp::endpoint endpoint = {}) {
if (ec) {
std::cout << "error: " << ec.message() << "\n";
} else {
std::cout << "connected to: " << endpoint << "\n";
}
});
And with futures:
auto f = async_connect_to(io_context, socket, resolver, argv[1], argv[2], asio::use_future);
try {
auto endpoint = f.get();
std::cout << "connected to: " << endpoint << "\n";
} catch (std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
}
Edit: It appears that as of Asio 1.25.0 there is also asio::experimental::co_composed
which is meant for precisely this purpose. It doesn't look much simpler than my above solution IMO, but it may be lighter weight.
Upvotes: 2