Integrating Qt events into Actix and Rust
Ever since it’s illegal for me to leave my house,
my weekends have been filled with rewriting Whisperfish.
Whisperfish is an app, originally by Andrew E. Bruno,
that natively implements Signal for SailfishOS.
My goal with the rewrite is to modernize the non-GUI code such that it uses the official
libsignal-protocol-c
instead of the Go-reimplementation.
For this, I would either use C++ or Rust; the title of the post probably spoiled which one I prefer.
I’m imagining two target audiences for this blog post: either you’re a Rustacean, and you’re here for the Tokio and Actix magic, or (and that’s not xor) you’re from the SailfishOS community and you’re wondering what all the Tokio and Actix buzzwords are even about. With that in mind, I’ll make an introduction on both topics, and depending on your background, you can skip either.
SailfishOS for Rustaceans
SailfishOS is heavily based on Qt 5; you might say it’s kind of the KDE of the mobile operating systems. Being based on Qt, most of the system and apps are written using C++ and QML, the latter being a declarative language for the graphical interface.
I want to retain the UI and QML code for the 0.6 release, but to fully rewrite the driving code, either in C++ or Rust. Since I’m quite into Rust my preference went there, although it’s not an officially supported language for Jolla’s official application store. That’s not a major issue though: the original Go implementation would not qualify either, so it’s at least not a step back.
Rust, Tokio and Actix for Sailors
I don’t think Rust needs a lot of introduction, given its recent enormous rise in popularity. Either way: Rust is a language that enables the same low-level operations as C or C++ would, while offering a lot of zero-cost abstraction. In a sense, it’s a lot like C++; but like I like to say, it is what C++ should have looked like.
Its main feature is the safety guarantees:
as long as you don’t write the feared unsafe
keyword, there should not be any race-conditions data races, undefined behaviour or memory errors.
Spoiler alert: integrating as deep with Qt as I will describe here, involves quite some unsafe
and even compiler bugs.
The upside is that unsafe
allows to make safe abstractions over what Rust considers unsafe
.
Something else that gets a lot of praise is Rust’s ecosystem: there are many high-quality, community-provided “crates” (libraries) to use as a dependency in your application. Tokio is one such well-known crate. Tokio provides a runtime for asynchronous programming: just like in Javascript and Python, Rust has the concept of futures and asynchronous functions. Unlike Javascript and Python, however, these concepts compile to very efficient state machines. Tokio provides the cross-platform glue for sockets and I/O, and a runtime for actually running the asynchronous code.
Actix builds on top of Tokio, firstly providing actors for Rust, but it’s most well-known for their record-breaking HTTP implementation.
Since I’d love to use the asynchronous infrastructure in Rust in this app, I had to take a look how to use Tokio in this context. If it doesn’t work out… Well, everything is recorded in Git.
Marrying opinionated frameworks
Both Qt and Tokio are quite opinionated: they are both complete frameworks, and while their goals are different (graphical application framework, asynchronous runtime), they cover much of the same ground, in quite different ways.
Specifically, both Qt and Tokio both implement an event loop. Event loops are a program’s source of blocking. In essence, it is a big, infinite loop like this:
loop { // n.b., this is pseudo-Rust.
while let Some(event) = fetch_an_event() {
event.handle();
}
sleep_until_more_events();
}
Qt implements this in the QEventLoop
class, in the ::exec()
method.
This method is called, when for example QCoreApplication::exec();
is called: QCoreApplication::exec()
creates a QEventLoop
and calls exec
on it.
Tokio calls this concept the Runtime
.
The block_on
method starts the actual loop.
Running both event loops on the same thread is not an option: either event loop will just go to sleep, while the other may have events to be processed. All this information leaves us with three options:
- Run Qt and Tokio on different threads.
- Integrate Tokio and Actix into Qt;
- Integrate the Qt events into Tokio.
Let’s discuss our three options.
Two event loops in two threads
This is how I started off, since that’s the most straightforward way: spawn two loops, run them on separate threads and use standard synchronisation primitives in order to facilitate communication between Actix and Qt.
I had my hopes on this approach, until the complexity of the synchronisation came apparent:
Qt likes to call “slots” (Signals & Slots),
which are implemented as function references in Rust.
The hoops I have to jump through to make the borrow checker happy,
would be pretty painful: there would be quite a lot of shared mutability, which should be checked at runtime: that means using RefCell
everywhere.
It’s not impossible, it’s that Rust would not be used to its full extent.
The only way I currently see to mitigate this, is to move to a more tight integration of the event loops.
Integrate a Rust event dispatch into Qt
This is something I’ve only thought about after starting the opposite, but it sounds like an interesting case to me too. The main reason I wouldn’t use it in this project, is because Actix is so tightly integrated in Tokio: one would need to reimplement most of the Tokio functionality into Qt.
What we will discover in what’s coming, is that Qt has some elements in the GUI system that are tightly coupled to the QEventLoop
,
which would be avoided here: Rust futures would be polled from within the QEventLoop
, and Qt would just run as it’s used to.
What I’m afraid of, are libraries that are closely coupled with the Tokio runtime, but as I understand it that’s already a concern within the Rust community for different but alike reasons. Again, probably not impossible, and I’m very much open to discussing this with someone that knows more about it than me.
Integrate Qt events into a Tokio loop
This is what I ended up with. Note that I only end up here because I thought it would be fun and challenging, not because it’s necessary. In the end, it was fun, challenging and frustrating…
In this situation, the Tokio event loop is “the boss”, and Qt is subservient.
The Intertubes suggest this is possible,
by calling processEvents
in your own event loop.
This alone doesn’t suffice:
- Tokio should know when to wake up; in the StackExchange example, they just sleep for 10ms;
- Qt documents quite often that
processEvents
does not update the windowing system. - Tokio should know when to quit: a typical Qt program runs until the last window is closed;
Handling timer and socket registration
Luckily, Qt offers a system to integrate your own event dispatcher,
by inheriting from QAbstractEventDispatcher
.
The methods in there get called from within Qt to register and deregister sockets and timers.
Sockets are passed through as file descriptors (among which is the Wayland Unix-socket for the windowing system),
and timers as an identifier together with an amount of milliseconds as interval.
These two map fairly well to Tokio; sockets can be handled using a Registration
,
while timers can go in a Interval
.
While this mapping is pretty straightforward, care has to be taken when dispatching the actual events:
Qt may re-enter your QAbstractEventDispatcher
and demand a socket or timer to be deregistered while you are processing the list.
It took a very long time before I understood this, but luckily valgrind
had me see the light when jumps were made into nothingness.
I realized that a Vec<_>
was being mutated while borrowed (unsafe
C++-code was called, don’t blame Rust),
and dismissing event dispatch until after collecting the events solved a bunch of very strange issues.
By then, I also fell over an LLVM bug,
because the arm-unknown-linux-gnueabihf
target in Rust apparently generates code that’s not legal on ARMv7.
Moving to armv7-unknown-linux-gnueabihf
resolved that issue.
This issue was only apparent on the most modern Aarch64 CPUs that run an ARMv7 userspace, which in Jolla-land means the Sony Xperia 10.
My friend with an Xperia X could not reproduce that issue.
Dispatching input events
The QAbstractEventDispatcher
implementation should call QCoreApplication::sendPostedEvents
in order for the Qt subsystem to dispatch events to their endpoints.
However, the documentation of that method denotes:
Events from the window system are not dispatched by this function, but by
processEvents()
.
processEvents
calls the QAbstractEventDispatcher::processEvents
method which… we just had to implement ourselves.
It didn’t help that I was taking inspiration from the QUnixEventDispatcher
private implementation in Qt, because I was looking at 10 year old code which did not do window events.
The dispatching of window events happens in the QWindowSystemInterface::sendWindowSystemEvents
, and inspiration should be taken from QUnixEventDispatcherQPA
, which is the Qt Platform Abstraction implementation.
This system is not yet documented very well, as indicated by its own documentation,
and I’m quite sure that I’m officially calling into an implementation detail here.
Quitting the application
Quitting the application is tied pretty tightly between QEventLoop
, QWindow
and QCoreApplication
.
Closing the last window triggers a call into QCoreApplication
.
If QCoreApplication
does not detect a running QEventLoop
, it will do nothing at all.
The solution here, for me, was to intercept the QEvent::Close
event using an event filter, and setting a flag for the SfosApplication
to finally return.
I’m unaware of any cleaner solution for now.
Final notes
I’ve spent about three weekends at this, and it’s been quite a journey.
For anyone who wants to undertake calling Qt from their own event loop, I’d suggest taking a look at the currently very ugly Rust code.
Also, you might be interested in this Qt event loop,
which binds Qt to libuv
.
I have promised myself a cleanup for this set of experiments,
by refactoring the code into a separate Qt support library.
It would be great to move optional Tokio/Actix support into qmetaobject-rs
,
a QML support system I also use for Whisperfish – currently in forked form, a story for another time.
Most of the Qt-Actix-Tokio code should move into a generic Qt library such as qmetaobject-rs
,
while the Sailfish specific code should become a SailfishOS Rust support crate.
That way, more Sailfish apps can be easily and cleanly developed using Rust in the future.