-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Asynchronous Rust and Qt signals and slots #102
Comments
Hi, Thanks for nicely structured write-up.
Maybe if we do that, we could figure out more about bridging C++ and Rust in the process. For #81 as well.
And don't forget, the big guy is writing the ultimate smol runtime to rule 'em all!
What I like so much about |
Thanks for working on this. I think the 1. definitively make sense to expose the primitives so one can run the application with any event loop. As for 3, there is already qmetaobject::future::wait_on_signal which i believe is what you meant. |
I had not seen that, very interesting! That seems to implement already half of what I mean, although I am also thinking about a more "safe" approach to it, and with a bit of syntactic aid; it would be great to read e.g. |
I recently read another story about integrating event loops, in case it's relevant: python-trio/trio#1551 |
That's looks to be basically the other way around, and seems to be more-or-less supported already. The most challenging part of this all is to cleanly integrate my previous work in |
I'm trying to adapt this clever architecture @rubdos came up with for driving the Qt event loop with Tokio in Whisperfish so it runs on a typical Linux stack without Sailfish OS (primarily so I can have a Signal client on my PinePhone, but replacing the Signal Electron application on my laptop would be nice too). I'm puzzled why it is deadlocking in the QGuiApplication constructor on Wayland. Sailfish OS uses Qt 5.6 and I'm using Qt 5.12.2 on my laptop running Fedora 34, however the QtWaylandClient::QWaylandDisplay::forceRoundTrip function where it deadlocks has not changed between Qt 5.6 and 5.12. My best guess is that the TokioQEventDisptacher class that @rubdos wrote to process Qt events within the Tokio event loop is violating some undocumented assumption of Qt. Interestingly this occurs before any event loop is started, and this would be executed before calling QGuiApplication::exec in a C++ Qt application or a qmetaobject-rs application with an architecture similar to the examples. If we can resolve this, it would be cool to merge the code upstream into this crate or separate it into its own crate. @ogoffart @ratijas, if you could take a look at my summary of the issue and share any insights, that would be much appreciated. |
More brain dump. The approach that I took in Whisperfish involved polling the Qt event loop from Tokio. Qt would register its timers and sockets through the QAbstractEventDispatch interface, and Tokio would do the necessary polling. One big issue here, is that the Wayland events (SailfishOS runs on a Wayland compositor) were to be polled through a private, undocumented Qt interface. This broke when Jolla discovered they had still to update their QWayland dependency from 5.4 to 5.6 (wow), because apparently I was assuming things I shouldn't have assumed. In short: polling Qt from Tokio is unmaintainable. A year ago, when I started the intertwining of the Qt and Tokio loops, I wrote about three approaches. Tokio-poll-Qt, Qt-polls-Tokio, and both running their own lives in their own threads and using synchronisation primitives. Since Literally Everything™ broke two weeks ago, I started reworking the event loop intertwining, and I think the new approach is a valid one for inclusion in either qmetaobject-rs, or possibly in a new subcrate At program start, I create (but don't start) a single threaded Tokio executor. This name is a bit ill-chosen: the single threaded Tokio executor is capable of running on multiple threads, but spawned futures stay on their own thread. I think this is crucial if you want friendly cooperation with Qt, but I'm not sure. It is important to me, because I'm running Actix. Notably, I manually create the qmetaobject::future::execute_async(futures::future::poll_fn(move |cx| {
RUNTIME.with(|rt| {
let _guard = rt.enter();
LOCALSET.with(|ls| {
let mut ls = ls.borrow_mut();
// If the localset is "ready", the queue is empty.
let _rdy = futures::ready!(futures::Future::poll(std::pin::Pin::new(&mut *ls), cx));
log::warn!("LocalSet is empty, application will either shut down or lock up.");
std::task::Poll::Ready(())
})
})
})); This ensures that #[derive(QObject, Default)]
pub struct Foo {
base: qt_base_class!(trait QObject),
count: qt_method!(fn(&self) -> usize),
}
impl Foo {
#[with_executor]
fn count(&self) -> usize { tokio::spawn(async {}) }
} Now, this is not enough to drive Tokio's I/O and networking. For that, the Runtime needs starting. // Spawn the TOkio I/O loop on a separate thread.
// The first thread to call `block_on` drives the I/O loop; the I/O that gets spawned by other
// threads gets polled on this "main" thread.
let (tx, rx) = futures::channel::oneshot::channel();
let driver = std::thread::Builder::new()
.name("Tokio I/O driver".to_string())
.spawn(move || {
runtime.block_on(async move {
rx.await.unwrap();
});
})?;
with_runtime(|rt| {
let _guard = rt.enter();
app.exec();
tx.send(()).unwrap();
});
driver.join().unwrap(); This starts the Tokio+mio I/O polling on a separate thread, until Qt decides the application has quit, which will also quit the Tokio I/O polling thread. I think this is a valid approach for a really clean interaction between Tokio and Qt (unlike the previous one). The diffstat for Whisperfish is around +60-600, and the application runs smoother than before. Moreover, the approach with the proc_macro attribute might be moot, if we could integrate this approach into qmetaobject: I'm a bit sad that the Tokio I/O needs its own thread, but given that Qt already starts a dozen threads for itself, having one for my own isn't that bad I guess. |
I have extracted the above idea into this crate: https://gitlab.com/rubdos/qmeta-async , and I'm converting Whisperfish such that it uses that. |
I have now also talked about this on the Belgian Rust Meetup. Video here: https://video.rubdos.be/w/bhhMcctgLXTX5hrVARw3eu?start=51m45s (starts at 51m45s, but that's in the link) |
I have already touched upon my experiments in a slightly related #98, and since @ratijas seemed to be interested, I decided to start a discussion here too.
Context
What
qmetaobject-rs
currently does, is starting Qt's event loop and handing off control to Qt. From there, Qt calls back into Rust's primitives. In my proof of concept (blog post), I've built an event loop in Rust that's capable of driving all events in Qt (read: all events that I needed on a SailfishOS application). I currently wrote this in the application I am writing, having to duplicate some C++ code fromqmetaobject
on the way.Dispatching the Qt events from a Rust based event loop has one big advantage: there is no need any more to synchronize data between a Rust event loop (you're possibly running sockets and I/O from Rust) and the Qt event loop, because they're one and the same: before this integration, I had to basically put everything behind
Arc<Mutex<T>>
. It might also reduce some resource usage, since it reduces the thread count by one (although Qt loves to spawn threads, apparently, so that's not a lot).Proposals
trait QAbstractEventDispatcher
, for further implementation by Rust applications. This should be fairly doable. We'd also require to correctly exportstruct QSockNotifier
, and probably some other types that I have currently not implemented (because they're unneeded in my own case).QAbstractEventDispatcher
. With thorough clean-up, this could land in this crate in two forms:QAbstractEventDispatcher
cfg(feature = "tokio")
, possibly following with anasync-std
implementation too.Possible results
The benefit of all of this combined, would be to write very idiomatic async Rust code, which would also be driving the GUI. For example, my main application logic currently reads (using Actix):
This
run()
method in turns spawn some asynchronous workers, on the same event loop that the GUI is running on.Alternative forms
Of course, this is all a lot of new (and extremely experimental) features. Whatever the verdict, I will move the bare Tokio logic out of the application I am writing into an external crate. If you think it is too far out of scope for
qmetaobject
, I'll start a separate crate for that.The discussion I would like to have here is which one of the 1, 2, 3 mentioned above would be welcome here. If part should be externalized, I'd like to discuss what interfaces are still necessary in
qmetaobject-rs
, as to keep the Tokio-related stuff clean.I believe that the design of #100 might keep (3) in mind, so I'm tagging that as slightly-related.
The text was updated successfully, but these errors were encountered: