Using C++ Coroutines with Boost C++ Libraries

Eric Battalio

This article was written by Gor Nishanov.

Last month, Jim Springfield wrote a great article on using C++ Coroutines with LibuvĀ (a multi-platform C library for asynchronous I/O). This month we will look at how to use coroutines with components of Boost C++ libraries, namely boost::future and boost::asio.

Getting Boost

If you already have boost installed, skip this step. Otherwise, I recommend using vcpkg to quickly get boost installed on your machine. Follow the instructions to get vcpkg and then enter the following line to install 32bit and 64bit versions of boost:

.\vcpkg install boost boost:x64-windows

To make sure everything got installed correctly, open and create a C++ Win32 Console Application:


#define BOOST_THREAD_PROVIDES_FUTURE
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION // Enables future::then
#include <boost/thread.hpp>
#include <boost/asio.hpp>
#include <cstdio>

using namespace boost;
using namespace boost::asio;

int main() {
io_service io;
promise<int> p;
auto f = p.get_future();

io.post([&] { p.set_value(42); });
io.run();

printf("%d\n", f.get());
}

When you run it, it should print 42.

Boost::Future: Coroutine Part

When a compiler encounters co_await, co_yield or co_return in a function, it treats the function as a coroutine. By itself C++ does not define the semantics of the coroutine, a user or a library writer needs to provide a specialization of the std::experimental::coroutine_traits template that tells the compiler what to do. (Compiler instantiates coroutine_traits by passing the types of the return value and types of all of the parameters passed to a function).

We would like to be able to author coroutines that return a boost::future. To do that, we are going to specialize coroutine_traits as follows:


template <typename... Args>
struct std::experimental::coroutine_traits<boost::future<void>, Args...> {
struct promise_type {
boost::promise<void> p;
auto get_return_object() { return p.get_future(); }
std::experimental::suspend_never initial_suspend() { return {}; }
std::experimental::suspend_never final_suspend() { return {}; }
void set_exception(std::exception_ptr e) { p.set_exception(std::move(e)); }
void return_void() { p.set_value(); }
};
};

When a coroutine gets suspended, it needs to return a future that will be satisfied when the coroutine runs to completion or completes with an exception.

The member function promise_type::get_return_object defines how to obtain a future that will be connected to a particular instance of a coroutine.Ā The member function promise_type::set_exception defines what happens if an unhandled exception happens in a coroutine. In our case, we would like to store that exception into the promise connected to the future we returned from a coroutine.

The member function promise_type::return_void defines what happens when execution reaches co_return statement or control flows runs to the end of the coroutine.

Member functions initial_suspend and final_suspend, as we defined them, tell the compiler that we would like to start executing the coroutine immediately after it is called and to destroy the coroutine as soon as it runs to completion.

To handle non-void futures, define specialization for boost::future for arbitrary types:


template <typename R, typename... Args>
struct std::experimental::coroutine_traits<boost::future<R>, Args...> {
struct promise_type {
boost::promise<R> p;
auto get_return_object() { return p.get_future(); }
std::experimental::suspend_never initial_suspend() { return {}; }
std::experimental::suspend_never final_suspend() { return {}; }
void set_exception(std::exception_ptr e) { p.set_exception(std::move(e)); }
template <typename U> void return_value(U &&u) {
p.set_value(std::forward<U>(u));
}
};
};

Note that in this case we defined return_value, as opposed to return_void as it was in the previous example. This tells the compiler that we expect that a coroutine needs to eventually return some non-void value (via a co_return statement) and that value will be propagated to the future associated with this coroutine. (There is a lot of common code between these two specializations; it can be factored out if desired).

Now, we are ready to test it out. Add an ā€œ/awaitā€ command line option to enable coroutine support in the compiler (since coroutines are not yet part of the C++ standard, an explicit opt-in is required to turn them on).

Also, add an include for the coroutine support header that defines primary template for std::experimental::coroutine_traits that we want to specialize:


#include <experimental/coroutine>

//ā€¦ includes and specializations of coroutine_traits ā€¦

boost::future<void> f() {
puts("Hi!");
co_return;
}

boost::future<int> g() {
co_return 42;
}

int main() {
f().get();
printf("%d\n", g().get());
};

When it runs, it should print: ā€œHi!ā€ and 42.

Boost::Future: Await Part

The next step is to explain to the compiler what to do if you are trying to ā€˜awaitā€™ on the boost::future.

Given an expression to be awaited upon, the compiler needs to know three things:

  1. Is it ready?
  2. If it is ready, how to get the result.
  3. If it is not ready, how to subscribe to get notified when it becomes ready.

To get answers to those questions, the compiler looks for three member functions: await_ready() that should return ā€˜trueā€™ or ā€˜falseā€™, await_resume() that compiler will call when the expression is ready to get the result (the result of the call to await_resume() becomes the result of the entire await expression), and, finally, await_suspend() that compiler will call to subscribe to get notified when the result is ready and will pass a coroutine handle that can be used to resume or destroy the coroutine.

In case of the boost::future, it has facilities to give the answers, but, it does not have the required member functions as described in the previous paragraph. To deal with that, we can define an operator co_await that can translate what boost::future has into what the compiler wants.


template <typename R> auto operator co_await(boost::future<R> &&f) {
struct Awaiter {
boost::future<R> &&input;
boost::future<R> output;
bool await_ready() { return false; }
auto await_resume() { return output.get(); }
void await_suspend(std::experimental::coroutine_handle<> coro) {
input.then([this, coro](auto result_future) {
this->output = std::move(result_future);
coro.resume();
});
}
};
return Awaiter{static_cast<boost::future<R>&&>(f)};
}

Note that in the adapter above, we always return false from await_ready(), even when it *is* ready, forcing the compiler always to call await_suspend to subscribe to get a continuation via future::then. Another approach is to write await_ready as follows:


bool await_ready() {
if (input.is_ready()) {
output = std::move(input);
return true;
}
return false;
}

In this case, if the future is ready, the coroutine bypasses suspension via await_suspend and immediately proceeds to getting the result via await_resume.

Depending on the application, one approach may be more beneficial than the other. For example, if you are writing a client application, naturally your application will run a little bit faster if during those times when the future is already ready, you donā€™t have to go through suspension followed by subsequent resuming of a coroutine by the boost::future. In server applications, with your server handling hundreds of simultaneous requests, always going via .then could be beneficial as it may produce more predictable response times if continuations are always scheduled in the fair manner. It is easy to imagine a streak where a particular coroutine is always lucky and has its futures completed by the time it asks whether they are ready. Such a coroutine will hog the thread and might starve other clients.

Pick any approach you like and try our our brand new operator co_await:


//ā€¦ includes, specializations of coroutine_traits, operator co_await.

boost::future<int> g() {
co_return 42;
}

boost::future<void> f() {
printf("%d\n", co_await g());
}

int main() {
f().get();
};

As usual, when you run this fragment, it will print 42. Note, that we no longer need a co_return in function f. The compiler knows it is a coroutine due the presence of an await expression.

Boost::asio

With the adapters that we have developed so far, you now are free to use coroutines that returnboost::future and to deal with any APIs and libraries that return boost::futures. But what if you have some library that does not return boost::future and uses callbacks as a continuation mechanism?

As the model, we will use the async_wait member function of the boost::asio::system_timer. Without coroutines, you might use system_timer as follows:


#include <boost/asio/system_timer.hpp>
#include <boost/thread.hpp>

using namespace boost::asio;
using namespace std::chrono;

int main() {
io_service io;
system_timer timer(io);

timer.expires_from_now(100ms);
timer.async_wait([](boost::system::error_code ec) {
if (ec) printf("timer failed: %d\n", ec.value());
else puts("tick");
});

puts("waiting for a tick");
io.run();
};

When you run this program, it will print ā€œwaiting for a tickā€, followed by a ā€œtickā€ 100ms later. Letā€™s create a wrapper around timerā€™s async_await to make it usable with coroutines. We would like to be able to use this construct:


co_await async_await(timer, 100ms);

to suspend its execution for the required duration using the specified timer. The overall structure will look similar to how we defined operator co_await for boost::future. We need to return from async_wait an object that can tell the compiler when to suspend, when to wake up and what is the result of the operation.


template <typename R, typename P>
auto async_await(boost::asio::system_timer &t, std::chrono::duration<R, P> d) {
struct Awaiter { <stuff> };
return Awaiter{ t, d };
}

Note that we pass parameters t and d when constructing Awaiter. We will need to store them in the awaiter so that we can get access to them in the await_ready and await_suspend member functions.


boost::asio::system_timer &t;
std::chrono::duration<R, P> d;

Also, you probably noticed in the system_timer example that a completion callback for async_wait has a parameter that receives an error code that indicates whether the wait completed successfully or with an error (timer was cancelled, for example). We would need to add a member variable to the awaiter to store the error code until it is consumed by await_resume.


boost::system::error_code ec;

Member function await_ready will tells us whether we need to suspend at all. If we implement it as follows, we will tell the compiler not to suspend a coroutine if the wait duration is zero.


bool await_ready() { return d.count() == 0; }

In await_suspend, we will call timer.async_await to subscribe a continuation. When boost::asio will call us back we will remember the error code and resume the coroutine.


void await_suspend(std::experimental::coroutine_handle<> coro) {
t.expires_from_now(d);
t.async_wait([this, coro](auto ec) {
this->ec = ec;
coro.resume();
});
}

Finally, when a coroutine is resumed we will check the error code and propagate it as an exception if the wait is not successful.


void await_resume() {
if (ec)
throw boost::system::system_error(ec);
}

And for your convenience, the entire adapter in one piece:


template <typename R, typename P>
auto async_await(boost::asio::system_timer &t, std::chrono::duration<R, P> d) {
struct Awaiter {
boost::asio::system_timer &t;
std::chrono::duration<R, P> d;
boost::system::error_code ec;

bool await_ready() { return d.count() == 0; }
void await_resume() {
if (ec)
throw boost::system::system_error(ec);
}
void await_suspend(std::experimental::coroutine_handle<> coro) {
t.expires_from_now(d);
t.async_wait([this, coro](auto ec) {
this->ec = ec;
coro.resume();
});
}
};
return Awaiter{ t, d };
}

And a small example using it:


//ā€¦ includes, specializations of coroutine_traits, etc.

using namespace boost::asio;
using namespace std::chrono;

boost::future<void> sleepy(io_service &io) {
system_timer timer(io);
co_await async_await(timer, 100ms);
puts("tick1");
co_await async_await(timer, 100ms);
puts("tick2");
co_await async_await(timer, 100ms);
puts("tick3");
}

int main() {
io_service io;
sleepy(io);
io.run();
};

When you run it, it should print tick1, tick2 and tick3 100 milliseconds apart.

Conclusion

We took a quick tour on how to develop adapters that enable the use of coroutines with existing C++ libraries. Please try it out, and experiment with adding more adapters. Also tune in for the upcoming blog post on how to use CompletionToken traits of boost::asio to create coroutine adapters without having to write them by hand.

Posted in C++

Feedback usabilla icon