Using Gtk-rs and Tokio

Hello

I currently have a console application using Tokio. Now I want to make a desktop app out of it using Gtk-rs.
The problem is that Tokio runs asynchronous and Gtk-rs needs to run synchronous.

I was thinking of making two threads. One sync thread for Gtk-rs and one async thread for Tokio. But I don't really know on how to achieve this.

This is what I tried:

use gtk4 as gtk;
use gtk::prelude::*;
use gtk::{glib, Application};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tokio::sync::mpsc;


fn main() {
    // Create a Tokio runtime
    let tokio_rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2) // Set the number of Tokio worker threads
        .enable_all()
        .build()
        .unwrap();

    // Spawn a Tokio thread
    tokio_rt.spawn(async {
        tokio_tasks().await;
    });

    // Spawn a Gtk-rs thread
    std::thread::spawn(move || {
        gtk_rs();
    });

    // Wait for threads to finish (optional)
    tokio_rt.block_on(async {
        // Your main Tokio async code here
    });

}

// Run async Tokio requests
async fn tokio_tasks() {

}

// Intialize GTK-RS
fn gtk_rs() -> gtk::glib::ExitCode {
     let application = gtk::Application::builder()
        .application_id("com.github.gtk-rs.examples.basic")
        .build();
    application.connect_activate(build_ui);
    application.run()
}

// Build GTK-RS UI
fn build_ui(application: &gtk::Application) {
    let window = gtk::ApplicationWindow::new(application);

    window.set_title(Some("First GTK Program"));
    window.set_default_size(350, 70);

    let button = gtk::Button::with_label("Click me!");

    window.set_child(Some(&button));

    window.present();
}

This compiles but immediately exists and does nothing, not even show the Gtk-window. How can I achieve this? Tokio should also keep running, when it was a console application I could achieve this using #[tokio::main], how would I do this now? And how would I put the results of Tokio requests into the UI?

I have some interest in this myself, but I never got around to actually writing any code for it. I found that someone had made a crate to facilitate communication between async/threads and the gtk worlds, but in a later blog post they stating that they had abandoned the project. But I believe the basic premise was to use channels to bridge the worlds.

When the main thread terminates, the process terminates. You need to keep the main thread running.

With that said: What platform is this?

IIRC Some Platforms™️ require the "first" thread of the process to be the gui thread. Which also solved the issue with that your process must not terminate. Spawn tokio on a different thread and let the main thread call all the Gtk stuff (most importantly application.run()).

Maybe you removed some important context, but it seems that the block_on at the end of your main does nothing and returns immediately. This means that you reach the end of the scope of the main function, dropping all local variables including the tokio runtime.
When the runtime is dropped, it shuts down and all remaining tasks are dropped when they yield back.

Shutting down the runtime is done by dropping the value, or calling shutdown_background or shutdown_timeout.

Tasks spawned through Runtime::spawn keep running until they yield. Then they are dropped. They are not guaranteed to run to completion, but might do so if they do not yield until completion.

IIRC, it is not absolutely required to run the GUI main loop in main directly (the thread in which init was called is considered the "main thread"), but I would recommend the following nonetheless:

fn main() {
    let rt = /* create the runtime */;

    // Ignite the tokio runtime in its own separate thread
    std::thread::spawn(move || {
        // Use `block_on` so that the runtime is not dropped immediately at the end of the scope.
        // Make sure `tokio_tasks` is not a Future that is resolved immediately, otherwise `block_on`
        // will return and the runtime will be dropped.
        rt.block_on(tokio_tasks());
    });

    gtk_rs();
}

Or better, using std::thread::scope:

fn main() {
    let rt = /* create the runtime */;

    std::thread::scope(|s| {
        s.spawn(|| {
            rt.block_on(tokio_tasks());
        });

        gtk_rs();
    })
}

About your last question:

You’ll need some form of message passing using MPSC channels. In this case, use glib’s MPSC channels. You can attach the receiver to the MainContext. That’s how messages will actually end up in GTK’s main thread.

EDIT: check out this chapter of the Gtk-rs book to learn more about MainContext::channel

There are some more resources here from Tokio's tutorial: Bridging with sync code | Tokio - An asynchronous Rust runtime

BTW, GTK has its own async executor:

which you can use for handling some gtk operations asynchronously. You need to be careful to spawn Tokio-specific futures on tokio's runtime, and gtk-specific futures on glib's runtime. However, once spawned, you can use their join handles to mix futures between the runtimes and have UI future wait for network and vice versa.