FFI bindings design, refcounted objects and thread safety (libgtk3)


#1

What are the best practices when interfacing with a C library that passes around pointers to refcounted objects (I) and is not entirely thread safe (II).

I. Wrapper and aliasing

https://developer.gnome.org/gobject/stable/gobject-memory.html#gobject-memory-refcount

The functions g_object_ref/g_object_unref respectively increase and decrease the reference count. The reference count is, unsurprisingly, initialized to one by g_object_new which means that the caller is currently the sole owner of the newly-created reference. When the reference count reaches zero, that is, when g_object_unref is called by the last client holding a reference to the object, the dispose and the finalize class methods are invoked.

So I probably want to create a wrapper

struct Widget {
    ptr: *mut GtkWidget,
}

impl Widget {
    fn ref_(&mut self) { ffi::g_object_ref(self.ptr) }
    fn unref(&mut self) { ffi::g_object_unref(self.ptr) }

    pub fn new() -> Widget { Widget { ptr: ffi::gtk_widget_new() } }
    pub fn get_foo(&self) -> Foo { ffi::gtk_widget_get_foo(self.ptr) }
    pub fn set_foo(&mut self, foo: Foo) { ffi::gtk_widget_set_foo(self.ptr, foo) }
}

impl Clone for Widget {
    fn clone(&self) { let w = Widget { self.ptr }; w.ref_(); w }
}

impl Drop for Widget {
    fn drop(&mut self) { self.unref() }
}

impl !Sync for Widget { } // See (II)
impl !Send for Widget { } // See (II)

What I’m not sure of is how to deal with borrowing here.
GTK gives us some *mut pointers we can pass around that explicitly are aliased. It seemingly makes any kind of mutable (non-aliased) borrowing impossible or meaningless. For one the existence of clone negates the point of having &mut fns. Forbidding cloning seems unreasonable. Even if we managed to produce a unique pointer, so what, GTK wouldn’t know or care.
So my instinct here is to make all fns take &self. Is it correct? At least if we forbid Sync and Send?

II. Thread safety

https://developer.gnome.org/gdk3/stable/gdk3-Threads.html#gdk3-Threads.description

GLib is completely thread safe (all global data is automatically locked), but individual data structure instances are not automatically locked for performance reasons. So e.g. you must coordinate accesses to the same GHashTable from multiple threads.

GTK+, however, is not thread safe. You should only use GTK+ and GDK from the thread gtk_init() and gtk_main() were called on. This is usually referred to as the “main thread”.

So the Widget case is relatively easy: we forbid Sync and Send and probably add a (!Send) struct Gtk to the wrapper like so:

struct Gtk {
    _dummy: ()
}

impl Gtk {
    pub fn new() -> Gtk { ffi::gtk_init(); Gtk { _dummy: () } }
    pub fn main(&self) { ffi::gtk_main() }
}

impl Clone for Gtk {
    fn clone(&self) -> Gtk { Gtk { _dummy: () } }
}

impl !Sync for Gtk { }
impl !Send for Gtk { }

struct Widget {
    ptr: *mut GtkWidget,
    gtk: Gtk,
}

impl Widget {
    pub fn new(gtk: &Gtk) -> Widget { Widget { ptr: ffi::gtk_widget_new(), gtk: gtk.clone() } }
}

Does this make sense so far?

But what about GHashTable? It could be made usable from different threads with some effort. Which std types would help me wrap it correctly?


#2

[quote=“gkoz, post:1, topic:412”]
But what about GHashTable?
[/quote]It seems I’d just need to

unsafe impl Send for Mutex<GHashTable> { }

#3

Making all functions take &self for the reasons you describe seems sensible to me.


#4

May I welcome you to Grust and the issue tracker there :slight_smile:


#5

I did not create bindings for GHashTable yet, but the wrapper type will probably implement grust::refcount::Refcount, while not being an ObjectType (IIRC it is “boxed”, so it will have BoxedType instead).


#6

[quote=“mzabaluev, post:4, topic:412, full:true”]
May I welcome you to Grust and the issue tracker there
[/quote]Thanks for the link.
Let me just ask this here though: suppose I go with the Wrapper trait you have there (not sure if object hierarchy is supported). How would I enforce the following GTK invariant?

Because right now I’m leaning to adding an optional (e.g. a T that can be (); maybe only GDK/GTK need it) “anchor” into the wrapper that wouldn’t let other threads get hold of widgets (Send isn’t the only way; any thread might get a *mut GtkWidget from somewhere, they just shouldn’t be able to stuff it in a wrapper).


#7

Excellent! I’ve been playing with the same idea (GIR -> Rust), though I wasn’t really targetting a fully safe outcome. Got stuck trying to do checked casting without the Gtk C macros.


#8

By default objects are not Send; I think this is good enough to enforce thread affinity.

Global init functions are problematic by themselves, least because they don’t have any special annotation in GIR. Back in the days when any code using GType had to call g_type_init() first thing, I just exposed the init function; there may be a safer solution now that GType in Grust is wrapped into a newtype.


#9

[quote=“mzabaluev, post:8, topic:412”]
By default objects are not Send; I think this is good enough to enforce thread affinity.
[/quote]If a widget has a constructor in our bindings it could be created on any thread regardless of Send. This doesn’t seem to be appropriate for GTK.


#10

That is a good point I missed in the original comment, thanks (and you can tell I’m not a Gtk+ hacker). I will investigate a generation mode that would emit Rust API using the “global context object” pattern: