Qt and the Coroutines TS
Signals and Slots in Qt are a mechanism for connecting high level events or state changes in objects to actions in other objects. For the programmer they are an advanced form of callbacks, and can have similar drawbacks. In particular, handling an expected sequence of events requires keeping a bunch of state around to remember where you are in the process.
For example, code to draw a line between two user-selected points might involve connecting a Slot to handle mouse clicks:
bool got_first_point{false};
QPointF first_point;
QObject::connect(&w, &MyWidget::click, [&](QPointF p) {
if (got_first_point) {
// draw
w.setLine(first_point, p);
got_first_point = false;
} else {
first_point = p;
got_first_point = true;
}
});
We are expecting two click
signals to arrive asynchronously, indicating the start and end points of the line. The first_point
and got_first_point
variables remember where we are in the process. Inverting this control flow, eliminating callbacks, and removing state is one of the main motivations for the Coroutines TS.
Clarifying Control Flow with Coroutines
With support for Coroutines we could change the above code to something like this:
QPointF first_point = co_await make_awaitable_signal(&w, &MyWidget::click);
QPointF second_point = co_await make_awaitable_signal(&w, &MyWidget::click);
w.setLine(first_point, second_point);
Instead of keeping around a bool
to tell us whether we’ve received the first point or not, our progress through the code itself tracks where we are. The code is still asynchronous, because the co_await
returns control to the Qt event loop until the signal arrives.
My repo contains code to do allow exactly this; if you’re interested in the implementation details read on, and otherwise check out the qt_coro.cpp example.
Coroutine Implementation at a High Level
There are people out there making libraries that wrap the TS to make it easy to use with all kinds of existing code (see the References section). But if you want to build one yourself, or to understand my code, this section is for you.
The TS requires the construction of some helper classes with a predefined API. This is like how range-based for loops expect containers to have begin()
and end()
methods, though quite a bit more complicated. There are basically two special types you need to implement:
- The promise type is a sort of handle to the coroutine itself, for its creator. It can be the type listed as the return type (the return object) of the coroutine or a type member of it named
promise_type
. Note that the “return object” is not returned from the coroutine, but constructed when it is first entered! There is a different mechanism for returning values out of the coroutine:promise_type::return_value()
. - The awaitable type represents the interface between the coroutine and what it
co_await
s on. It is a way to communicate how the value is supplied and whether suspending is necessary.
I’ll leave the detailed explanation for the experts but touch on a few important bits about my implementation:
- The promise type indicates that execution should not initially suspend:
auto initial_suspend() const noexcept {
return std::experimental::suspend_never();
}
This means that when we create the coroutine it will start executing, and only suspend when it has a reason to (i.e., when it encounters a co_await
). At that point it will return to where it was created and we will continue executing, creating other coroutines or setting up widgets.
- The awaitable type creates and connects to a temporary Slot on construction, i.e., when
co_await
is executed. That slot’s code, when called, will disconnect the signal, store any signal arguments, and resume the coroutine. - The awaitable is initially suspended:
bool await_ready() const noexcept {
return false; // waiting for the signal to arrive
}
- Finally, I go to some effort to massage the signal arguments into the friendliest form for a return value: multiple arguments become a
std::tuple
, single arguments stay as themselves, and nullary signals returnvoid
.
Execution Sequence in Qt
Consider this coroutine use:
QTimer * changeTimer = new QTimer(&app);
auto ro = [&]() -> return_object<> {
co_await make_awaitable_signal(changeTimer, &QTimer::timeout);
// do something
}();
The calling code creates a timer, followed by a coroutine. The coroutine runs to the first co_await
and suspends back to its creator, supplying a return_object
. This is not a return value, but a handle to the coroutine, which is now waiting for a timer event.
Meanwhile, the co_await
itself has caused the creation of an awaitable
that will manage its resumption when the slot is activated. In a sense this too is a handle to the coroutine - but one that the subject of the co_await
will use.
After the coroutine above is created (e.g. in the main program) we can continue initial construction of other widgets, or even more coroutines, until we enter the main Qt event loop:
return app.exec();
Now the direction of control flow reverses itself. Events occuring in the OS or the window system trigger signals to be emitted: mouse clicks, timer expirations, network activity, etc. The event loop, now running from that exec()
method, starts activating slots and thereby resuming our coroutines. In the case of the timer above, the slot looks like:
auto operator()(QMetaObject::Connection& signal_conn,
std::experimental::coroutine_handle<>& coro_handle) {
return [&signal_conn, &coro_handle]() {
QObject::disconnect(signal_conn);
coro_handle.resume();
};
}
That final statement causes us to resume execution where the coroutine was suspended, i.e., at the co_await
statement. The last time it was running it had arrived from the main program, but now it’s being run by the Qt event loop! You can see the difference in the backtrace with a debugger, which is kind of neat.
Conclusion
The Coroutines TS enables some interesting new directions for Qt-based code. Official support is probably the best path for this to take but in the interim I hope this prototype will be useful.
References
There are some very smart people working on Coroutines. I benefited from reading a number of good blog posts. My favorite tutorial is by Kirit Sælensminde and can be found here. Lewis Baker has an amazing Coroutines utility library, and also explains the mechanisms behind co_await
in extensive detail here.
A number of other people are also adding coroutine support to popular libraries. Jim Springfield has a great post about using them with the asynchronous I/O library libuv. Casey Carter adapted the range-v3 library so ranges can be generators. Finally Boost.Asio offers coroutine support as well now; my repo contains an example of its use.
Last I would point out that I’m not the first to try using coroutines with Qt; for example, a recent effort by Jesús Fernández using stackful coroutines is here.