Future cannot be sent between threads safely Gtk-rs

Hello

I am currently able to make a login-form in Gtk-rs, send the form-data in a POST-request to my server to authenticate, and retrieve the response and session.

But now I also have a label which I want to give the text "success" or "failure" depending on if the authentication was successful.

    // Make label
    let label = gtk::Label::new(Some(""));

    // On button click call async login-function to send a POST-request to server with username and password
    login_button.connect_clicked(move | _button| {
        let username = username_input.text().as_str().to_string();
        let password = password_input.text().as_str().to_string();
        tokio::spawn(async move {
            match server::login(username, password).await {
                true => label.set_text("success"),   // SUCCESS
                false => label.set_text("failure"),  // FAILURE
            };
        });
    });

Compile error:

error: future cannot be sent between threads safely
   --> src/main.rs:104:22
    |
104 |           tokio::spawn(async move {
    |  ______________________^
105 | |             match server::login(username, password).await {
106 | |                 true => label.set_text("SUCCESS"),
107 | |                 false => label.set_text("FAILURE"),
108 | |             };
109 | |         });
    | |_________^ future created by async block is not `Send`
    |

How would I achieve this?

tasks (i.e. futures spawned on a runtime) is scheduled by the runtime and can be running on any work thread, but gtk requires all UI operations must run on the same thread as the event loop (typically it's the "main" thread)

you should send a notification from the async task to the event loop thread, and update the UI accordingly. you can use the glib::MainContext type to register an callback on the event loop thread.

let (tx, rx) = MainContext::channel(Priority::default());
tokio::spawn(async move {
    let successful = server::login(username, password).await;
    tx.send(successful).unwrap();
});
// you'll need a weak reference to the widget
// and move the weak reference to the callback
let weak_label = label.downgrade();
rx.attach(None, move |successful| {
    let label = weak_label.upgrade().unwrap();
    if successful {
        label.set_text("success");
    } else {
        label.set_text("failure");
    }
    // if this is a one shot event callback, return `Break`
    // if this callback need to be called repeatedly, return `Continue`
    ControlFlow::Break
});

see explanation here:

https://gtk-rs.org/gtk4-rs/stable/latest/book/main_event_loop.html

I want to add, glib::MainContext is itself an async runtime, in certain cases, you might not need the tokio runtime, in which case, the code can be simplified dramatically:

// `MainContext::spawn_local()` doesn't require `Send` bound
// you still need a weak reference though
let weak_label = label.downgrade();
login_button.connect_clicked(move | button| {
    let username = username_input.text().as_str().to_string();
    let password = password_input.text().as_str().to_string();
    MainContext::default()::spawn_local(async move {
        match server::login(username, password).await {
            true => weak_label.upgrade().unwrap().set_text("success"),   // SUCCESS
            false => weak_label.upgrade().unwrap().set_text("failure"),  // FAILURE
        };
    });
});

but this won't work if the future depends on some tokio specific features. unfortunately, tokio conflates different concepts (like scheduler/executor, reactor, poller, etc), into its single Runtime type, and many async libraries are tightly coupled with tokio and can't be re-used with other async runtimes (such as glib).

2 Likes

You can use the async-compat crate to solve the tokio problem. If you don't want to add yet another crate I invite you to go look at its source code. It's really short and can be replicated in your own project with even less code if you only care about specific features.

2 Likes
    let label = gtk::Label::new(Some("Initial Text"));

    let gtk_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();
    gtk_box.append(&username_input);
    gtk_box.append(&password_input);
    gtk_box.append(&login_button);
    gtk_box.append(&label);



    login_button.connect_clicked(move | _button| {
    let weak_label = label.downgrade();
        let username = username_input.text().as_str().to_string();
        let password = password_input.text().as_str().to_string();
        glib::MainContext::default().spawn_local(async move {
            match server::login(username, password).await {
                true => weak_label.upgrade().unwrap().set_text("success"),
                false => println!("lol"),
            };
        });
    });

Had to move the weak_label a few lines lower but. works perfectly now. Thanks.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.