10

I am currently trying to use the new C++20 coroutines with boost::asio. However I am struggling to find out how to implement custom awaitable functions (like eg boost::asio::read_async). The problem I am trying to solve is the following:

I have a connection object where I can make multiple requests and register a callback for the response. The responses are not guaranteed to arrive in the order they have been requested. I tried wrapping the callback with a custom awaitable however I am unable to co_await this in the coroutine since there is no await_transform for my awaitable type in boost::asio::awaitable.

The code I tried to wrap the callback into an awaitable is adapted from here: https://books.google.de/books?id=tJIREAAAQBAJ&pg=PA457

auto async_request(const request& r)
{
    struct awaitable
    {
        client* cli;
        request req;
        response resp{};

        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h)
        {
            cli->send(req, [this, h](const response& r)
            {
                resp = r;
                h.resume();
            });
        }
        auto await_resume()
        {
            return resp;
        }
    };
    return awaitable{this, r};
}

which I tried calling in a boost coroutine like this:

boost::asio::awaitable<void> network::sts::client::connect()
{
    //...
    auto res = co_await async_request(make_sts_connect());
    //...
}

giving me the following error:

error C2664: 'boost::asio::detail::awaitable_frame_base<Executor>::await_transform::result boost::asio::detail::awaitable_frame_base<Executor>::await_transform(boost::asio::this_coro::executor_t) noexcept': cannot convert argument 1 from 'network::sts::client::async_request::awaitable' to 'boost::asio::this_coro::executor_t'

Is there any way to achieve this functionality ?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
ACB
  • 1,607
  • 11
  • 31

1 Answers1

8

I actually managed to find a solution to this. The way to do this is by using boost::asio::async_initiate to construct a continuation handler and just defaulting the handler to boost::use_awaitable. As an added bonus this way it is trivial to match it to the other async function by simply using a template argument for the handler.

template<typename ResponseHandler = boost::asio::use_awaitable_t<>>
auto post(const request& req, ResponseHandler&& handler = {})
{
    auto initiate = [this]<typename Handler>(Handler&& self, request req) mutable
    {
        send(req, [self = std::make_shared<Handler>(std::forward<Handler>(self))](const response& r)
        {
            (*self)(r);
        });
    };
    return boost::asio::async_initiate<
        ResponseHandler, void(const response&)>(
            initiate, handler, req
        );
}

The only problem here is that std::function does not move construct apparently so I had to wrap the handler in a std::shared_ptr.

ACB
  • 1,607
  • 11
  • 31
  • Can you elaborate a little bit on how exactly this solves your problem and how pieces come together??? thanks a lot! – Viktor Khristenko Jan 04 '22 at 10:18
  • 1
    Sure. Basically you can do `co_await post(req);` or use it any other "asio" way, like `auto fut = post(req, boost::asio::use_future)`. The `async_initiate` will set up all asio specific stuff and then call the `initiate` function with your parameters and the result handler. The `send` function is a simple callback based async function that calls the result handler as soon the result is available and boost then handles the coroutine stuff. – ACB Jan 05 '22 at 11:52
  • How about returning something more that awaitable from post? Actually I managed to do something like this in my code (using code that you provided), but the signature in async_initiate has to be like this void(std::exception_ptr, const response&> and I'm wondering why. Also I haven't found any explanation in boost doc why this extra param exception_ptr is needed.Perhaps you have idea why is that? – bielu000 Aug 11 '22 at 16:27
  • I am not sure I understand what you want. the post function I posted actually returnes a `response` object if used with `boost::use_awaitable`. Eg `auto res = co_await m_client->post(make_auth_list_gameaccounts());` – ACB Aug 13 '22 at 07:50
  • 1
    With your code I'm getting "error: ‘const void ec’ has incomplete type" event when the completition handler has signature with one parameters which is boost::system:error_code – bielu000 Aug 30 '22 at 14:37
  • well `const void` is not a valid parameter type. So there would be something wrong in your declaration probably or maybe you are missing a typedef for `boost::system::error_code` – ACB Aug 30 '22 at 15:28
  • The reason the make_shared here is needed is because the Callable passed to std::function must be copyable, replace it with std::move_only_function (available only in C++23) or fu2::unique_function (small header only lib): `template > auto async_execute(request_type request, CompletionToken&& token = {}) { return boost::asio::async_initiate( [this](auto&& tok, request_type req){ return execute_handler(std::move(req),std::forward(tok)); },token,std::move(request)); }` – rajkosto Apr 21 '23 at 11:34
  • The actually correct way of doing this, respecting executors and allocators the entire way through, is here: https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/example/cpp20/operations/callback_wrapper.cpp – rajkosto Apr 23 '23 at 11:42