How do I write a custom Boost ASIO completion token, as_expected, that yields a std::expected (tl::expected)?

Viewed 115

I want to write a custom completion token for asio, like asio::experimental::use_tuple, but which actually returns a tl::expected.

The usage would be like:

asio::awaitable<void> echo(tcp::socket socket) {
  char data[1024];
  for (;;) {
    tl::expected<std::size_t> result = co_await socket.async_read_some(
        asio::buffer(data), as_expected(asio::use_awaitable));
    if (result.has_value()) {
      // success
    }

    // ...
  }
}

I'm following Richard Hodges's article about creating a custom timeout completion token to get some of the boilerplate right but am running into trouble.

I'm stuck on writing the initiation function object. How do I call the underlying Initiation correctly, transform the result of the completion handler, and give it my wrapped CompletionHandler?

Here's what I have so far (expected_initiation is where I am stuck):

// token type
template <typename CompletionToken>
struct expected_token {
  CompletionToken& token;
};

// initiation function
template <typename... Signatures>
struct expected_initiation {
  template <typename CompletionHandler, typename Initiation,
            typename... InitArgs>
  void operator()(CompletionHandler handler, std::chrono::milliseconds timeout,
                  Initiation&& initiation, InitArgs&&... init_args) {
    auto ex = asio::get_associated_executor(
        handler, asio::get_associated_executor(initiation));

    // is this bind_executor right? how do I transform the output and call
    // handler()?
    asio::bind_executor(ex, [&](auto&& token) {
      return asio::async_initiate<decltype(token), Signatures...>(
          std::forward<Initiation>(initiation), token,
          std::forward<InitArgs>(init_args)...);
    })
  }
};

// specialization
template <typename InnerCompletionToken, typename... Signatures>
struct asio::async_result<expected_token<InnerCompletionToken>, Signatures...> {
  template <typename Initiation, typename... InitArgs>
  static auto initiate(Initiation&& init,
                       expected_token<InnerCompletionToken> t,
                       InitArgs&&... init_args) {
    return asio::async_initiate<InnerCompletionToken, Signatures...>(
        expected_initiation<Signatures...>{}, t.token,
        std::forward<Initiation>(init), std::forward<InitArgs>(init_args)...)
  }
};

template <typename CompletionToken>
auto as_expected(CompletionToken&& token) {
  return expected_token<CompletionToken>{token};
}

I'm worried I'm on the wrong path. I tried to look at the source of experimental::as_tuple, but it was beyond me and looked much more complicated than my attempt, so I wonder if Richard's article is less applicable than I thought, since I'm modifying the completion handler's signature with my token.

Edit: There's a lot more to this than I thought. To transform the completion signature like this, I think I need to do something like use_future does and specialize for a bunch of signatures:

// Helper template to choose the appropriate concrete promise handler
// implementation based on the supplied completion signature.
template <typename> class promise_handler_selector;

template <>
class promise_handler_selector<void()>
  : public promise_handler_0 {};

template <>
class promise_handler_selector<void(asio::error_code)>
  : public promise_handler_ec_0 {};

template <>
class promise_handler_selector<void(std::exception_ptr)>
  : public promise_handler_ex_0 {};

template <typename Arg>
class promise_handler_selector<void(Arg)>
  : public promise_handler_1<Arg> {};

template <typename Arg>
class promise_handler_selector<void(asio::error_code, Arg)>
  : public promise_handler_ec_1<Arg> {};

template <typename Arg>
class promise_handler_selector<void(std::exception_ptr, Arg)>
  : public promise_handler_ex_1<Arg> {};

#if defined(ASIO_HAS_VARIADIC_TEMPLATES)

template <typename... Arg>
class promise_handler_selector<void(Arg...)>
  : public promise_handler_n<std::tuple<Arg...> > {};

template <typename... Arg>
class promise_handler_selector<void(asio::error_code, Arg...)>
  : public promise_handler_ec_n<std::tuple<Arg...> > {};

template <typename... Arg>
class promise_handler_selector<void(std::exception_ptr, Arg...)>
  : public promise_handler_ex_n<std::tuple<Arg...> > {};

This may be well over my head.

1 Answers

Aha! Googled the right thing eventually.

https://www.boost.org/doc/libs/1_80_0/libs/outcome/doc/html/recipes/asio-integration-1-70.html answers my question in terms of boost::outcome, which has an extremely similar API so porting should be simple.

https://gist.github.com/cstratopoulos/901b5cdd41d07c6ce6d83798b09ecf9b/863c1dbf3b063a5ff9ff2bdd834242ead556e74e has the source code.

Edit: here's my ported code (non-boost asio), seems to work well:

#pragma once

#ifdef WIN32
#include <sdkddkver.h>
#endif

#include <system_error>
#include <type_traits>
#include <utility>

#include "asio/associated_allocator.hpp"
#include "asio/associated_executor.hpp"
#include "asio/async_result.hpp"
#include "asio/detail/handler_alloc_helpers.hpp"
#include "asio/detail/handler_cont_helpers.hpp"
#include "asio/detail/handler_invoke_helpers.hpp"
#include "tl/expected.hpp"

template <typename CompletionToken>
struct as_expected_t {
  CompletionToken token_;
};

template <typename CompletionToken>
inline constexpr auto as_expected(CompletionToken&& completion_token) {
  return as_expected_t<std::decay_t<CompletionToken>>{
      std::forward<CompletionToken>(completion_token)};
}

namespace detail {

// Class to adapt as_expected_t as a completion handler
template <typename Handler>
struct expected_handler {
  void operator()(const std::error_code& ec) {
    using Result = tl::expected<void, std::error_code>;

    if (ec)
      handler_(Result{tl::unexpected(ec)});
    else
      handler_(Result{});
  }

  void operator()(std::exception_ptr ex) {
    using Result = tl::expected<void, std::exception_ptr>;

    if (ex)
      handler_(Result{tl::unexpected(ex)});
    else
      handler_(Result{});
  }

  template <typename T>
  void operator()(const std::error_code& ec, T t) {
    using Result = tl::expected<T, std::error_code>;

    if (ec)
      handler_(Result{tl::unexpected(ec)});
    else
      handler_(Result{std::move(t)});
  }

  template <typename T>
  void operator()(std::exception_ptr ex, T t) {
    using Result = tl::expected<T, std::exception_ptr>;
    if (ex)
      handler_(Result{tl::unexpected(ex)});
    else
      handler_(Result{std::move(t)});
  }

  Handler handler_;
};

template <typename Handler>
inline void* asio_handler_allocate(std::size_t size,
                                   expected_handler<Handler>* this_handler) {
  return asio_handler_alloc_helpers::allocate(size, this_handler->handler_);
}

template <typename Handler>
inline void asio_handler_deallocate(void* pointer, std::size_t size,
                                    expected_handler<Handler>* this_handler) {
  asio_handler_alloc_helpers::deallocate(pointer, size, this_handler->handler_);
}

template <typename Handler>
inline bool asio_handler_is_continuation(
    expected_handler<Handler>* this_handler) {
  return asio_handler_cont_helpers::is_continuation(this_handler->handler_);
}

template <typename Function, typename Handler>
inline void asio_handler_invoke(Function& function,
                                expected_handler<Handler>* this_handler) {
  asio_handler_invoke_helpers::invoke(function, this_handler->handler_);
}

template <typename Function, typename Handler>
inline void asio_handler_invoke(const Function& function,
                                expected_handler<Handler>* this_handler) {
  asio_handler_invoke_helpers::invoke(function, this_handler->handler_);
}

template <typename Signature>
struct expected_signature;

template <>
struct expected_signature<void(std::error_code)> {
  using type = void(tl::expected<void, std::error_code>);
};

template <>
struct expected_signature<void(const std::error_code&)>
    : expected_signature<void(std::error_code)> {};

template <>
struct expected_signature<void(std::exception_ptr)> {
  using type = void(tl::expected<void, std::exception_ptr>);
};

template <typename T>
struct expected_signature<void(std::error_code, T)> {
  using type = void(tl::expected<T, std::error_code>);
};

template <typename T>
struct expected_signature<void(const std::error_code&, T)>
    : expected_signature<void(std::error_code, T)> {};

template <typename T>
struct expected_signature<void(std::exception_ptr, T)> {
  using type = void(tl::expected<T, std::exception_ptr>);
};

template <typename Signature>
using expected_signature_t = typename expected_signature<Signature>::type;

}  // namespace detail

namespace asio {

template <typename CompletionToken, typename Signature>
class async_result<as_expected_t<CompletionToken>, Signature> {
 public:
  using expected_signature = ::detail::expected_signature_t<Signature>;

  using return_type =
      typename async_result<CompletionToken, expected_signature>::return_type;

  template <typename Initiation, typename... Args>
  static return_type initiate(Initiation&& initiation,
                              as_expected_t<CompletionToken>&& token,
                              Args&&... args) {
    return async_initiate<CompletionToken, expected_signature>(
        [init = std::forward<Initiation>(initiation)](
            auto&& handler, auto&&... callArgs) mutable {
          std::move(init)(
              ::detail::expected_handler<std::decay_t<decltype(handler)>>{
                  std::forward<decltype(handler)>(handler)},
              std::forward<decltype(callArgs)>(callArgs)...);
        },
        token.token_, std::forward<Args>(args)...);
  }
};

template <typename Handler, typename Executor>
struct associated_executor<::detail::expected_handler<Handler>, Executor> {
  typedef typename associated_executor<Handler, Executor>::type type;

  static type get(const ::detail::expected_handler<Handler>& h,
                  const Executor& ex = Executor()) noexcept {
    return associated_executor<Handler, Executor>::get(h.handler_, ex);
  }
};

template <typename Handler, typename Allocator>
struct associated_allocator<::detail::expected_handler<Handler>, Allocator> {
  typedef typename associated_allocator<Handler, Allocator>::type type;

  static type get(const ::detail::expected_handler<Handler>& h,
                  const Allocator& a = Allocator()) noexcept {
    return associated_allocator<Handler, Allocator>::get(h.handler_, a);
  }
};

}  // namespace asio

Small problem I've found: for some reason I can't define a combined token with this:

constexpr as_expected_t<asio::use_awaitable_t<>> Token;
// ...

// fails, no overload found?
auto res = co_await resolver.async_resolve(argv[1], argv[2], Token);

So if anyone can tell me why that is, bonus points. That example works with e.g. as_tuple_t or inlining as_expected_t<asio::use_awaitable_t<>>{}.

Related