Using C++ Coroutines with Boost C++ Libraries

May 19th, 2017

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:

1

.\vcpkg install boost boost:x64-windows

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

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:

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.

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:

[code lang=”cpp”] #include <experimental/coroutine> [/code]

[code lang=”cpp”] //… 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()); }; [/code]

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:

Is it ready?

If it is ready, how to get the result.

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.

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:

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:

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:

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:

[code lang=”cpp”] co_await async_await(timer, 100ms); [/code]

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.

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.

[code lang=”cpp”] boost::system::error_code ec; [/code]

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.

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.