c++c++20coroutineasio

Coroutines: How to call async_* functions from non-coroutine aware library code


I'm trying to use asio coroutines in a project which uses an internal library implementing the core functionality.

Low-level routines are implemented in concrete implementations of an abstract interface which is used by the library. The data held by the concrete implementation is normally sufficient to call the low-level functions.

#include <boost/asio/io_context.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/deadline_timer.hpp>

struct RequestContext
{
    virtual void doSomething() = 0;
};

struct AsyncRequestContext : public RequestContext
{
    boost::asio::io_context ioc;

    void doSomething() override
    {
        // Do something asynchronous with coroutines ...
        boost::asio::deadline_timer timer(ioc);
        timer.expires_from_now(boost::posix_time::seconds(5));

        // Howto do an async call with cocoutines here?
        //co_await timer.async_wait(boost::asio::use_awaitable);

        timer.wait();
    }
};

// This is in a library which does not know about coroutines
void middle(RequestContext& rctx)
{
    rctx.doSomething();
}

int main()
{
    AsyncRequestContext rctx;

    // Spawn a listening port
    boost::asio::co_spawn(
        rctx.ioc,
        [&rctx]() -> boost::asio::awaitable<void> {

            // Call library code 
            middle(rctx);

            co_return;
        },
        boost::asio::detached);

    rctx.ioc.run();

    return 0;
}

My problem is, that if the coroutine spawn is at the top of the call hierarchy, then the library code is called which calls the low-level routines down in the call tree.

I have no clue how to use co_await and the async_* functions in these lower functions without changing the interface.


Solution

  • Coroutines are a flexible tool, but the way they work, are scheduled and resumed, is ultimately up to the particular interfaces and systems you're working with.

    The whole point with coroutines is that they're an implementation detail of a function. This means you don't know that some function is a coroutine; all you know is that it's a function that takes some parameters and returns a value. An asynchronous function would return some kind of "future-value" type that represents a value that will be generated but is not necessarily yet available.

    A coroutine is just a way to write such a function.

    As such, the way to interact with an asynchronous functions is conceptually orthogonal to whether the functions is implemented as a coroutine or something else. That is, you talk to the "future-value".

    So if you have code in one place that asynchronously generates a value, and code in another place that needs to synchronize with it and receive that value... you need to get the return value from the first place to the second place.

    This is generally done by having the functions between them return that value. But how you do it is up to you. You could use some kind of member variable, global variable, or something else. Your AsyncRequestContext could store the result of timer.await, for example. Of course, that means it's not a coroutine which... yes, it's not an asynchronous function. Because it's not written to be.

    It needs to start an asynchronous process, and store the future-value, but doRequest is itself not a coroutine or an asynchronous function. If it needs to do coroutine stuff within it, use a lambda that itself is a coroutine.