Trying to implement Listener pattern in GTK4-RS program

I have a GTK application with a number of different views, implemented as composite-templates containers. When my application loads it shows the main window with a number of widgets, but also needs to do some extended time consuming data initialisation, which is on a seperate thread,

When the initialisation completes I need signal to the UI part, which is a typical listener pattern in other languages.

I have created a EventListener trait, and have my GTK container implementation implement the trait. This works OK, as long as the callback takes no parameters, but what I want to do is execute

   impl EventListener for AirportView {
        fn event_occurred(&self, event: &Event) {
            self.my_search_button.set_sensitive(true);
        }
    }

This however won't work as the "EventManager" needs to hold a reference to AirportView, which is a GTK subclass and holds references to my GTKButton etc and so does not implement Sync or Send.

Changing the EventManager to just hold a reference to a plain function (not a method) works well, but then there is no way from the function to access self.my_search_button

I can't find anyway to implement this or even to hold a static global reference to the "window" so I can access it / send it messages from elsewhere in the application.

Unfortunately, the terminology is a bit misleading, and Rust references aren't a general-purpose way of referencing objects, nor for storing them by reference. They're temporary scope-limited loans, and are semantically more like compile-time read/write locks. They're rarely applicable for anything else than function arguments that the function doesn't need to keep.

In event listeners you will not be able to use the temporary references at all. You will have to hold objects via Arc or Rc (a shared reference, not limited by a scope/lifetime), and possibly use Arc<Mutex> combo to modify anything.

gtk extensively uses Rc and has a clone! macro for closures to help using Rc.

1 Like

Thanks for the reply. I have tried using Arc and other combinations, but with no success. I had thought a weak reference should be passable between threads Arc::downgrade(xx) , but apparently not.

After your suggestions I have looked into the clone! macro and tried to use it, but ran into trouble passing the closures as they capture a value and so cannot be passed as a function.

I think I will ave to give up Rust/Gtk at this time as too many problems seem unsolvable.

A std::sync::Weak is definitely passable between threads.

One common mistake is to call clone after you move it over the thread boundary. You have to clone first, then move the clone across the thread boundary.

1 Like

Where did you run into it? gtk-rs accounts for that and allows closures everywhere. Their callbacks take generic Fn that allows any data that is either immutable or behind a mutex. They don't use thin function pointers fn() in Rust APIs.

BTW, root of the problems you're running into are dealing with single-ownership vs borrowing, and immutable vs exclusive access. These are fundamentals of Rust semantics, and it's really necessary to understand them. This "friction" is typical for learning Rust, and isn't a fault of gkt-rs. You will run into these things everywhere, with any framework, in any non-trivial program.

1 Like

Take a look at glib::MainContext::channel here, which solves this exact problem. It works pretty much like the standard library channels: You move the Sender to your worker thread, and listen to changes in the main (UI) thread via Receiver::attach. The closure you bind there will execute on the main UI loop after any messages you send from the worker thread.

Thanks, but the channel pattern is Multiple sender -> single receiver, which is really the inverse of the observer pattern and requires some close coupling between the construction of the sender and receiver, which I would prefer to avoid.

Still really stuck here. I feel like I understand the fundamentals of the Rust semantics, but just like relativity and quantum mechanics, I probably don't :slight_smile:

So a fuller snippet of what I'm trying to do in its very simplest cutdown example:
I am using the comosite template to build this part of the UI

    #[derive(Default, CompositeTemplate)]
    #[template(resource = "/com/shartrec/kelpie_planner/airport_view.ui")]
    pub struct AirportView {
       .......
        #[template_child]
        pub airport_search: TemplateChild<Button>,
    }

Then I have an EventListener holding a weak reference to the button

    struct MyEventHandler {
        button: Weak<Mutex<Button>>,
    }

    impl MyEventHandler {
        pub fn new(x: Button) -> Self {
            MyEventHandler {
                button: {

                    let x = Arc::new(Mutex::new(x));
                    Arc::downgrade(&x)
                },
            }
        }
    }

    impl EventListener for MyEventHandler {
        fn event_occurred(&self, event: &Event) {
            match self.button.upgrade() {
                Some(b) => b.set_sensitive(true),
                None => (),
            }
        }
    }
}

Building this gives the following error:

110 |     impl EventListener for MyEventHandler {
    |                            ^^^^^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely
    |
    = help: the trait `Send` is not implemented for `*mut c_void`
    = note: required for `TypedObjectRef<*mut c_void, ()>` to implement `Send`
    = note: required because it appears within the type `Button`
    = note: required for `Mutex<gtk4::Button>` to implement `Sync`
    = note: 1 redundant requirement hidden
    = note: required for `std::sync::Weak<Mutex<gtk4::Button>>` to implement `Sync`
note: required because it appears within the type `MyEventHandler`

the channel for you to register a piece of code to the thread that holds the observable object (the event loop thread in your case), you receive notification through the registered callback. it's not that you are sending notifications directly through a channel and polling the channel on the other end.

you are moving the button into the handler constructor and then create a reference counted pointer internally (and then the Arc is dropped immediately). that's not how you share objects.

instead, your function should take a weak pointer directly, and at the call site you downgrade the an Arc to Weak then move the Weak into the listener.

Send is a trait that marks types that are thread-safe and can be moved to another thread. Unfortunately, it looks like Button is not marked as safe to use from any other thread! Sorry about that, I didn't realize this when I suggested Arc previously.

In this case you do have a limitation of gtk-rs requiring you to handle widgets on the same thread. Instead of the thread-safe Arc<Mutex<Button>> you may be able to use its single-threaded equivalent of Rc<RefCell<Button>>.

Yes, I understand that. That was going to be my next question; how can I actually add something into the struct that is built by the gtk builder opaque magic to hold a reference, Arc or Rc or whatever, but that is just another step too far it seems. i also realise that I need to do any UI related stuff on the UI thread with add_idle or similar, but omitted that for the sake of brevity here.

your design of EventHandler seems like you want the worker thread to own the registered handlers and be able to call them from the worker thread directly once the work is done, which is, as indicated by the trouble you had, is not safe.

it's unsafe in more than one aspect:

on the one hand, since the worker thread owns the registry of handlers, every time you want to register a new handler, the handler must be sent across threads, which means it cannot capture types that is not Send.

on the other hand, even if you cheat the type system to use unsafe wrappers to by pass the Send requirement, when you invoke the handler from the worker thread, I'd imagine most of the time the handler would update the UI, but calling UI stuff is not supported by gtk (and most GUI systems).

the recommended way is to make the work thread send (e.g. using channels) Events to the event loop, which in turn manages the handlers. this way, your handlers don't need to be Send, and it's totally safe to update the UI in the event handler.

gtk (or rather, glib under the hood) uses internal reference counting, and it has it's own type for weak references, which the rust binding exposes as the WeakRef wrapper. you can use the downgrade() method to obtain a weak reference of a GObject. note the WeakRef is deliberately marked as Send only when the inner object type thread safe (namely Send + Sync), which Button is not.

The fact that you have a single point of entry to listen to changes from the worker thread just means you have to create some kind of abstraction to make the observer pattern work. Here is how I would do it:

#[derive(Debug, Clone, Copy)]
pub struct Event(i64);

pub trait EventListener {
    fn event_occurred(&self, event: &Event);
}

#[derive(Clone)]
pub struct LabelOne(gtk4::Label);

#[derive(Clone)]
pub struct LabelTwo(gtk4::Label);

impl EventListener for LabelOne {
    fn event_occurred(&self, event : &Event) {
        self.0.set_text(&format!("LabelOne: {}",event.0))
    }
}

impl EventListener for LabelTwo {
    fn event_occurred(&self, event : &Event) {
        self.0.set_text(&format!("LabelTwo: {}",event.0));
    }
}

fn attach_observers(rx : glib::Receiver<Event>, observers : Vec<std::boxed::Box<dyn EventListener>>) {
    rx.attach(None, move |ev : Event| {
        observers.iter().for_each(|obs| obs.event_occurred(&ev) );
        glib::source::Continue(true)
    });
}

// Do this on the UI thread. You can attach as many UI listeners as you need.
// You might want to make the Event message an enum, if different UI elements
// need to ignore or pay attention to different messages.
let l1 = LabelOne(gtk4::Label::new(None));
let l2 = LabelTwo(gtk4::Label::new(None));
let (tx, rx) = glib::MainContext::channel::<Event>(glib::Priority::default());

// Worker thread
std::thread::spawn(move|| {
    let mut ev = Event(0);
    loop {
        std::thread::sleep_ms(500);
        tx.send(ev);
        ev.0 += 1;
    }
});
attach_observers(rx, vec![std::boxed::Box::new(l1.clone()), std::boxed::Box::new(l2.clone())]);

Thanks everyone for your help.
I have decide the channel is the best option for me here. So for anyone else who may need the same, what I ended up with is:

        let (tx, rx) = MainContext::channel(PRIORITY_DEFAULT);
        let transmitter = tx.clone();

        thread::spawn(move || {
            crate::earth::initialise(transmitter);
        });

        let view = Box::new(self.airport_view.clone());
        rx.attach(None, move |ev: Event| {
            match ev {
                Event::AirportsLoaded => view.imp().airports_loaded(),
                _ => (),
            }
            glib::source::Continue(true)
        });

It was the view.imp()... that had me baffled for a long while, as I couldn't easily see how the glib::wrapper object and my struct were connected.
Once again thanks everyone.

1 Like

What does the view.imp() do?

If you look at the example code in the CompositeTemplates chapter in the Rust GTK book, Composite Templates - GUI development with Rust and GTK 4 , It constructs a

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

and an actual implementation

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
    #[template_child]
    pub button: TemplateChild<Button>,
}

The imp() allows you to get a handle to the implementation struct instance from the wrapper object, which is what the GUI parent holds an actual handle to.
For a better explanation, you would need someone with more experience in Rust/Gtk than me. I'm just a beginer (this is my first project).

1 Like

After much thinking, a much more general pattern that fits in well with GTK and Rust is to set up a gtk channel for each observer and register the Sender with the "EventManager" while ataching a handler to the Receiver locally. Here is a basic event manager:

lazy_static! {
    static ref MANAGER: EventManager = EventManager {
        listeners: RwLock::new(HashMap::new()),
        index: AtomicUsize::new(0),
    };
}
pub fn manager() -> &'static EventManager {
    &MANAGER
}

pub struct EventManager {
    listeners: RwLock<HashMap<usize, Sender<Event>>>,
    index: AtomicUsize,
}

impl EventManager {
    pub fn register_listener(&self, listener: Sender<Event>) -> usize {
        if let Ok(mut listeners) = self.listeners.write() {
            let i = self.index.fetch_add(1, Ordering::Relaxed);
            listeners.insert(i, listener);
            i
        } else {
            0
        }
    }

    pub fn unregister_listener(&self, index: &usize) {
        if let Ok(mut listeners) = self.listeners.write() {
            listeners.remove(index);
        }
    }

    pub fn notify_listeners(&self, ev: Event) {
        if let Ok(mut listeners) = self.listeners.read() {
            for listener in listeners.iter() {
                listener.1.send(ev.clone());
            }
        }
    }
}

so a listener needs to

let (tx, rx) = MainContext::channel(PRIORITY_DEFAULT);
            let index = event::manager().register_listener(tx);
            rx.attach(None,clone!(@weak self as view => @default-return glib::source::Continue(true), move |ev: Event| {
                match ev {
                    Event::PreferencesChanged => {
                        println!("Preference updated {:?}", view);
                    },
                    _ => (),
                }
                glib::source::Continue(true)
            }));
            // hold a reference to the listener so you can remove it. I use a RefCell for this
            self.my_listener_id.replace(index);

and the code that raises the event simply

event::manager().notify_listeners(Event::PreferencesChanged);

Hope this helps someone down the track.

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.