ASIO/Networking TS: Boost >= 1.70

Thanks to Christos Stratopoulos for this Outcome recipe.


Compatibility note

This recipe targets Boost versions including and after 1.70, where coroutine support is based around the asio::use_awaitable completion token. For integration with Boost versions before 1.70, see this recipe.


Use case

Boost.ASIO and standalone ASIO provide the async_result customisation point for adapting arbitrary third party libraries, such as Outcome, into ASIO.

Historically in ASIO you need to pass completion handler instances to the ASIO asynchronous i/o initiation functions. These get executed when the i/o completes.

  // Dynamically allocate a buffer to read into. This must be move-only
  // so it can be attached to the completion handler, hence the unique_ptr.
  auto buffer = std::make_unique<std::vector<byte>>(1024);

  // Begin an asynchronous socket read, upon completion invoke
  // the lambda function specified
  skt.async_read_some(asio::buffer(buffer->data(), buffer->size()),

                      // Retain lifetime of the i/o buffer until completion
                      [buffer = std::move(buffer)](const error_code &ec, size_t bytes) {
                        // Handle the buffer read
                        if(ec)
                        {
                          std::cerr << "Buffer read failed with " << ec << std::endl;
                          return;
                        }
                        std::cout << "Read " << bytes << " bytes into buffer" << std::endl;

                        // buffer will be dynamically freed now
                      });
View this code on Github

One of the big value adds of the Coroutines TS is the ability to not have to write so much boilerplate if you have a Coroutines supporting compiler:

  // As coroutines suspend the calling thread whilst an asynchronous
  // operation executes, we can use stack allocation instead of dynamic
  // allocation
  char buffer[1024];

  // Asynchronously read data, suspending this coroutine until completion,
  // returning the bytes of the data read into the result.
  try
  {
    // The use_awaitable completion token represents the current coroutine
    // (requires Coroutines TS)
    size_t bytesread =  //
    co_await skt.async_read_some(asio::buffer(buffer), asio::use_awaitable);
    std::cout << "Read " << bytesread << " bytes into buffer" << std::endl;
  }
  catch(const std::system_error &e)
  {
    std::cerr << "Buffer read failed with " << e.what() << std::endl;
  }
View this code on Github

The default ASIO implementation always throws exceptions on failure through its coroutine token transformation. The redirect_error token transformation recovers the option to use the error_code interface, but it suffers from the same drawbacks that make pure error codes unappealing in the synchronous case.

This recipe fixes that by making it possible for coroutinised i/o in ASIO to return a result<T>:

  // Asynchronously read data, suspending this coroutine until completion,
  // returning the bytes of the data read into the result, or any failure.
  outcome::result<size_t, error_code> bytesread =  //
  co_await skt.async_read_some(asio::buffer(buffer), as_result(asio::use_awaitable));

  // Usage is exactly like ordinary Outcome. Note the lack of exception throw!
  if(bytesread.has_error())
  {
    std::cerr << "Buffer read failed with " << bytesread.error() << std::endl;
    return;
  }
  std::cout << "Read " << bytesread.value() << " bytes into buffer" << std::endl;
View this code on Github

Implementation

The below involves a lot of ASIO voodoo. NO SUPPORT WILL BE GIVEN HERE FOR THE ASIO CODE BELOW. Please raise any questions or problems that you have with how to implement this sort of stuff in ASIO on Stackoverflow #boost-asio.

The real world, production-level recipe can be found at the bottom of this page. You ought to use that in any real world use case.

It is however worth providing a walkthrough of a simplified edition of the real world recipe, as a lot of barely documented ASIO voodoo is involved. You should not use the code presented next in your own code, it is too simplified. But it should help you understand how the real implementation works.

Firstly we need to define some helper type sugar and a factory function for wrapping any arbitrary third party completion token with that type sugar:

namespace detail
{
  // Type sugar for wrapping an external completion token
  template <class CompletionToken> struct as_result_t
  {
    CompletionToken token;
  };
}  // namespace detail

// Factory function for wrapping a third party completion token with
// our type sugar
template <class CompletionToken>  //
inline auto as_result(CompletionToken &&token)
{
  return detail::as_result_t<std::decay_t<CompletionToken>>{std::forward<CompletionToken>(token)};
};
View this code on Github

Next we tell ASIO about a new completion token it ought to recognise by specialising async_result:

// Tell ASIO about a new kind of completion token, the kind returned
// from our as_result() factory function. This implementation is
// for functions with handlers void(error_code, T) only.
template <class CompletionToken, class T>                        //
struct asio::async_result<detail::as_result_t<CompletionToken>,  //
                          void(error_code, T)>                   //

{
  // The result type we shall return
  using result_type = outcome::result<T, error_code>;
  // The awaitable type to be returned by the initiating function,
  // the co_await of which will yield a result_type
  using return_type = //
  typename asio::async_result<CompletionToken, void(result_type)> //
  ::return_type;
View this code on Github

There are a couple tricky parts to understand. First of all, we want our async_result specialization to work, in particular, with the async_result for ASIO’s use_awaitable_t completion token. With this token, the async_result specialization takes the form with a static initiate method which defers initiation of the asynchronous operation until, for example, co_await is called on the returned awaitable. Thus, our async_result specialization will take the same form. With this in mind, we need only understand how our specialization will implement its initiate method. The trick is that it will pass the initiation work off to an async_result for the supplied completion token type with a completion handler which consumes result<T>. Our async_result is thus just a simple wrapper over this underlying async_result, but we inject a completion handler with the void(error_code, size_t) signature which constructs from that a result:

  // Wrap a completion handler with void(error_code, T) converting
  // handler
  template <class Handler>
  struct completion_handler {
    // Our completion handler spec
    void operator()(error_code ec, T v)
    {
      // Call the underlying completion handler, whose
      // completion function is void(result_type)
      if(ec)
      {
        // Complete with a failed result
        _handler(result_type(outcome::failure(ec)));
        return;
      }
      // Complete with a successful result
      _handler(result_type(outcome::success(v)));
    }

    Handler _handler;
  };

  // NOTE the initiate member function initiates the async operation,
  // and we want to defer to what would be the initiation of the
  // async_result whose handler signature is void(result_type).
  template <class Initiation, class... Args>
  static return_type
  initiate(
    Initiation&& init,
    detail::as_result_t<CompletionToken>&& token,
    Args&&... args)
  {
    // The async_initiate<CompletionToken, void(result_type)> helper
    // function will invoke the async initiation method of the
    // async_result<CompletionToken, void(result_type)>, as desired.
    // Instead of CompletionToken and void(result_type)	we start with
    // detail::as_result_t<CompletionToken> and void(ec, T), so
    // the inputs need to be massaged then passed along.
    return asio::async_initiate<CompletionToken, void(result_type)>(
      // create a new initiation which wraps the provided init
      [init = std::forward<Initiation>(init)](
        auto&& handler, auto&&... initArgs) mutable {
        std::move(init)(
          // we wrap the handler in the converting completion_handler from
          // above, and pass along the args
          completion_handler<std::decay_t<decltype(handler)>>{
            std::forward<decltype(handler)>(handler)},
          std::forward<decltype(initArgs)>(initArgs)...);
      },
      // the new initiation is called with the handler unwrapped from
      // the token, and the original initiation arguments.
      token.token,
      std::forward<Args>(args)...);
  }
};
View this code on Github

To use, simply wrap the third party completion token with as_result to cause ASIO to return from co_await a result instead of throwing exceptions on failure:

char buffer[1024];

outcome::result<size_t, error_code> bytesread =
  co_await skt.async_read_some(asio::buffer(buffer), as_result(asio::use_awaitable));

The real world production-level implementation below is a lot more complex than the above which has been deliberately simplified to aid exposition. The above should help you get up and running with the below, eventually.

One again I would like to remind you that Outcome is not the appropriate place to seek help with ASIO voodoo. Please ask on Stackoverflow #boost-asio.


Here follows the real world, production-level adapation of Outcome into ASIO, written and maintained by Christos Stratopoulos. If the following does not load due to Javascript being disabled, you can visit the gist at https://gist.github.com/cstratopoulos/901b5cdd41d07c6ce6d83798b09ecf9b/863c1dbf3b063a5ff9ff2bdd834242ead556e74e.