ASIO/Networking TS : Boost < 1.70

Thanks to Christos Stratopoulos for this Outcome recipe.


Compatibility note

This recipe targets Boost versions before 1.70, where coroutine support is based around the asio::experimental::this_coro::token completion token. For integration with Boost versions 1.70 and onward, 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];

  // Get an ASIO completion token for the current coroutine (requires
  // Coroutines TS)
  asio::experimental::await_token token =  //
  co_await asio::experimental::this_coro::token();

  // Asynchronously read data, suspending this coroutine until completion,
  // returning the bytes of the data read into the result.
  try
  {
    size_t bytesread =  //
    co_await skt.async_read_some(asio::buffer(buffer), token);
    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(token));

  // 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)>                   //

    // NOTE we subclass for an async result taking an outcome::result
    // as its completion handler. We will mangle the void(error_code, T)
    // completion handler into this completion handler below.
    : public asio::async_result<CompletionToken, void(outcome::result<T, error_code>)>
{
  // The result type we shall return
  using result_type = outcome::result<T, error_code>;
  using _base = asio::async_result<CompletionToken, void(result_type)>;
  // The awaitable type to be returned by the initiating function,
  // the co_await of which will yield a result_type
  using return_type = typename _base::return_type;

  // Get what would be the completion handler for the async_result
  // whose completion handler is void(result_type)
  using result_type_completion_handler_type =  //
  typename _base::completion_handler_type;
View this code on Github

The tricky part to understand is that our async_result specialisation inherits from an async_result for the supplied completion token type with a completion handler which consumes a result<T>. Our async_result is actually therefore the base async_result, but we layer on top a completion_handler_type with the void(error_code, size_t) signature which constructs from that a result:

  // Wrap that completion handler with void(error_code, T) converting
  // handler
  struct completion_handler_type
  {
    // Pass through unwrapped completion token
    template <class U>
    completion_handler_type(::detail::as_result_t<U> &&ch)
        : _handler(std::forward<U>(ch.token))
    {
    }

    // 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)));
    }

    result_type_completion_handler_type _handler;
  };

  // Initialise base with the underlying completion handler
  async_result(completion_handler_type &h)
      : _base(h._handler)
  {
  }

  using _base::get;
};
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];
asio::experimental::await_token token =
  co_await asio::experimental::this_coro::token();

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

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/da584844f58353915dc2600fba959813f793b456.