MHebes
MHebes

Reputation: 3197

How do I use a composed Asio operation with C++20 coroutines to return a value?

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.

  1. How do I actually return a value from this composed async operation?
  2. How do I improve the error handling so that the completion handler can take the signature (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

Answers (1)

MHebes
MHebes

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

Related Questions