How Glib-rs works, part 1: Type conversions

- Tags: gnome, rust

During the GNOME+Rust hackfest in Mexico City, Niko Matsakis started the implementation of gnome-class, a procedural macro that will let people implement new GObject classes in Rust and export them to the world. Currently, if you want to write a new GObject (e.g. a new widget) and put it in a library so that it can be used from language bindings via GObject-Introspection, you have to do it in C. It would be nice to be able to do this in a safe language like Rust.

How would it be done by hand?

In a C implementation of a new GObject subclass, one calls things like g_type_register_static() and g_signal_new() by hand, while being careful to specify the correct GType for each value, and being super-careful about everything, as C demands.

In Rust, one can in fact do exactly the same thing. You can call the same, low-level GObject and GType functions. You can use #[repr(C)]] for the instance and class structs that GObject will allocate for you, and which you then fill in.

You can see an example of this in gst-plugins-rs. This is where it implements a Sink GObject, in Rust, by calling Glib functions by hand: struct declarations, class_init() function, registration of type and interfaces.

How would it be done by a machine?

That's what Niko's gnome-class is about. During the hackfest it got to the point of being able to generate the code to create a new GObject subclass, register it, and export functions for methods. The syntax is not finalized yet, but it looks something like this:

gobject_gen! {
    class Counter {
        struct CounterPrivate {
            val: Cell<u32>
        }

        signal value_changed(&self);

        fn set_value(&self, v: u32) {
            let private = self.private();
            private.val.set(v);
            // private.emit_value_changed();
        }

        fn get_value(&self) -> u32 {
            let private = self.private();
            private.val.get()
        }
    }
}

I started adding support for declaring GObject signals — mainly being able to parse them from what goes inside gobject_gen!() — and then being able to call g_signal_newv() at the appropriate time during the class_init() implementation.

Types in signals

Creating a signal for a GObject class is basically like specifying a function prototype: the object will invoke a callback function with certain arguments and return value when the signal is emitted. For example, this is how GtkButton registers its button-press-event signal:

  button_press_event_id =
    g_signal_new (I_("button-press-event"),
                  ...
                  G_TYPE_BOOLEAN,    /* type of return value */
                  1,                 /* how many arguments? */
                  GDK_TYPE_EVENT);   /* type of first and only argument */

g_signal_new() creates the signal and returns a signal id, an integer. Later, when the object wants to emit the signal, it uses that signal id like this:

GtkEventButton event = ...;
gboolean return_val;

g_signal_emit (widget, button_press_event_id, 0, event, &return_val);

In the nice gobject_gen!() macro, if I am going to have a signal declaration like

signal button_press_event(&self, event: &ButtonPressEvent) -> bool;

then I will need to be able to translate the type names for ButtonPressEvent and bool into something that g_signal_newv() will understand: I need the GType values for those. Fundamental types like gboolean get constants like G_TYPE_BOOLEAN. Types that are defined at runtime, like GDK_TYPE_EVENT, get GType values generated at runtime, too, when one registers the type with g_type_register_*().

Rust type GType
i32 G_TYPE_INT
u32 G_TYPE_UINT
bool G_TYPE_BOOLEAN
etc. etc.

Glib types in Rust

How does glib-rs, the Rust binding to Glib and GObject, handle types?

Going from Glib to Rust

First we need a way to convert Glib's types to Rust, and vice-versa. There is a trait to convert simple Glib types into Rust types:

pub trait FromGlib<T>: Sized {
    fn from_glib(val: T) -> Self;
}

This means, if I have a T which is a Glib type, this trait will give you a from_glib() function which will convert it to a Rust type which is Sized, i.e. a type whose size is known at compilation time.

For example, this is how it is implemented for booleans:

impl FromGlib<glib_ffi::gboolean> for bool {
    #[inline]
    fn from_glib(val: glib_ffi::gboolean) -> bool {
        !(val == glib_ffi::GFALSE)
    }
}

and you use it like this:

let my_gboolean: glib_ffi::gboolean = g_some_function_that_returns_gboolean ();

let my_rust_bool: bool = from_glib (my_gboolean);

Booleans in glib and Rust have different sizes, and also different values. Glib's booleans use the C convention: 0 is false and anything else is true, while in Rust booleans are strictly false or true, and the size is undefined (with the current Rust ABI, it's one byte).

Going from Rust to Glib

And to go the other way around, from a Rust bool to a gboolean? There is this trait:

pub trait ToGlib {
    type GlibType;

    fn to_glib(&self) -> Self::GlibType;
}

This means, if you have a Rust type that maps to a corresponding GlibType, this will give you a to_glib() function to do the conversion.

This is the implementation for booleans:

impl ToGlib for bool {
    type GlibType = glib_ffi::gboolean;

    #[inline]
    fn to_glib(&self) -> glib_ffi::gboolean {
        if *self { glib_ffi::GTRUE } else { glib_ffi::GFALSE }
    }
}

And it is used like this:

let my_rust_bool: bool = true;

g_some_function_that_takes_gboolean (my_rust_bool.to_glib ());

(If you are thinking "a function call to marshal a boolean" — note how the functions are inlined, and the optimizer basically compiles them down to nothing.)

Pointer types - from Glib to Rust

That's all very nice for simple types like booleans and ints. Pointers to other objects are slightly more complicated.

GObject-Introspection allows one to specify how pointer arguments to functions are handled by using a transfer specifier.

(transfer none)

For example, if you call gtk_window_set_title(window, "Hello"), you would expect the function to make its own copy of the "Hello" string. In Rust terms, you would be passing it a simple borrowed reference. GObject-Introspection (we'll abbreviate it as GI) calls this GI_TRANSFER_NOTHING, and it's specified by using (transfer none) in the documentation strings for function arguments or return values.

The corresponding trait to bring in pointers from Glib to Rust, without taking ownership, is this. It's unsafe because it will be used to de-reference pointers that come from the wild west:

pub trait FromGlibPtrNone<P: Ptr>: Sized {
    unsafe fn from_glib_none(ptr: P) -> Self;
}

And you use it via this generic function:

#[inline]
pub unsafe fn from_glib_none<P: Ptr, T: FromGlibPtrNone<P>>(ptr: P) -> T {
    FromGlibPtrNone::from_glib_none(ptr)
}

Let's look at how this works. Here is the FromGlibPtrNone trait implemented for strings.

1
2
3
4
5
6
7
impl FromGlibPtrNone<*const c_char> for String {
    #[inline]
    unsafe fn from_glib_none(ptr: *const c_char) -> Self {
        assert!(!ptr.is_null());
        String::from_utf8_lossy(CStr::from_ptr(ptr).to_bytes()).into_owned()
    }
}

Line 1: given a pointer to a c_char, the conversion to String...

Line 4: check for NULL pointers

Line 5: Use the CStr to wrap the C ptr, like we looked at last time, validate it as UTF-8 and copy the string for us.

Unfortunately, there's a copy involved in the last step. It may be possible to use Cow<&str> there instead to avoid a copy if the char* from Glib is indeed valid UTF-8.

(transfer full)

And how about transferring ownership of the pointed-to value? There is this trait:

pub trait FromGlibPtrFull<P: Ptr>: Sized {
    unsafe fn from_glib_full(ptr: P) -> Self;
}

And the implementation for strings is as follows. In Glib's scheme of things, "transferring ownership of a string" means that the recipient of the string must eventually g_free() it.

1
2
3
4
5
6
7
8
impl FromGlibPtrFull<*const c_char> for String {
    #[inline]
    unsafe fn from_glib_full(ptr: *const c_char) -> Self {
        let res = from_glib_none(ptr);
        glib_ffi::g_free(ptr as *mut _);
        res
    }
}

Line 1: given a pointer to a c_char, the conversion to String...

Line 4: Do the conversion with from_glib_none() with the trait we saw before, put it in res.

Line 5: Call g_free() on the original C string.

Line 6: Return the res, a Rust string which we own.

Pointer types - from Rust to Glib

Consider the case where you want to pass a String from Rust to a Glib function that takes a *const c_char — in C parlance, a char *, without the Glib function acquiring ownership of the string. For example, assume that the C version of gtk_window_set_title() is in the gtk_ffi module. You may want to call it like this:

fn rust_binding_to_window_set_title(window: &Gtk::Window, title: &String) {
    gtk_ffi::gtk_window_set_title(..., make_c_string_from_rust_string(title));
}

Now, what would that make_c_string_from_rust_string() look like?

  • We have: a Rust String — UTF-8, known length, no nul terminator

  • We want: a *const char — nul-terminated UTF-8

So, let's write this:

1
2
3
4
5
fn make_c_string_from_rust_string(s: &String) -> *const c_char {
    let cstr = CString::new(&s[..]).unwrap();
    let ptr = cstr.into_raw() as *const c_char;
    ptr
}

Line 1: Take in a &String; return a *const c_char.

Line 2: Build a CString like we way a few days ago: this allocates a byte buffer with space for a nul terminator, and copies the string's bytes. We unwrap() for this simple example, because CString::new() will return an error if the String contained nul characters in the middle of the string, which C doesn't understand.

Line 3: Call into_raw() to get a pointer to the byte buffer, and cast it to a *const c_char. We'll need to free this value later.

But this kind of sucks, because we the have to use this function, pass the pointer to a C function, and then reconstitute the CString so it can free the byte buffer:

let buf = make_c_string_from_rust_string(my_string);
unsafe { c_function_that_takes_a_string(buf); }
let _ = CString::from_raw(buf as *mut c_char);

The solution that Glib-rs provides for this is very Rusty, and rather elegant.

Stashes

We want:

  • A temporary place to put a piece of data
  • A pointer to that buffer
  • Automatic memory management for both of those

Glib-rs defines a Stash for this:

1
2
3
4
5
6
pub struct Stash<'a,                                 // we have a lifetime
                 P: Copy,                            // the pointer must be copy-able
                 T: ?Sized + ToGlibPtr<'a, P>> (     // Type for the temporary place
    pub P,                                           // We store a pointer...
    pub <T as ToGlibPtr<'a, P>>::Storage             // ... to a piece of data with that lifetime ...
);

... and the piece of data must be of of the associated type ToGlibPtr::Storage, which we will see shortly.

This struct Stash goes along with the ToGlibPtr trait:

pub trait ToGlibPtr<'a, P: Copy> {
    type Storage;

    fn to_glib_none(&'a self) -> Stash<'a, P, Self>;  // returns a Stash whose temporary storage
                                                      // has the lifetime of our original data
}

Let's unpack this by looking at the implementation of the "transfer a String to a C function while keeping ownership":

1
2
3
4
5
6
7
8
9
impl <'a> ToGlibPtr<'a, *const c_char> for String {
    type Storage = CString;

    #[inline]
    fn to_glib_none(&self) -> Stash<'a, *const c_char, String> {
        let tmp = CString::new(&self[..]).unwrap();
        Stash(tmp.as_ptr(), tmp)
    }
}

Line 1: We implement ToGlibPtr<'a *const c_char> for String, declaring the lifetime 'a for the Stash.

Line 2: Our temporary storage is a CString.

Line 6: Make a CString like before.

Line 7: Create the Stash with a pointer to the CString's contents, and the CString itself.

(transfer none)

Now, we can use ".0" to extract the first field from our Stash, which is precisely the pointer we want to a byte buffer:

let my_string = ...;
unsafe { c_function_which_takes_a_string(my_string.to_glib_none().0); }

Now Rust knows that the temporary buffer inside the Stash has the lifetime of my_string, and it will free it automatically when the string goes out of scope. If we can accept the .to_glib_none().0 incantation for "lending" pointers to C, this works perfectly.

(transfer full)

And for transferring ownership to the C function? The ToGlibPtr trait has another method:

pub trait ToGlibPtr<'a, P: Copy> {
    ...

    fn to_glib_full(&self) -> P;
}

And here is the implementation for strings:

impl <'a> ToGlibPtr<'a, *const c_char> for String {
    fn to_glib_full(&self) -> *const c_char {
        unsafe {
            glib_ffi::g_strndup(self.as_ptr() as *const c_char, 
                                self.len() as size_t)
                as *const c_char
        }
    }

We basically g_strndup() the Rust string's contents from its byte buffer and its len(), and we can then pass this on to C. That code will be responsible for g_free()ing the C-side string.

Next up

Transferring lists and arrays. Stay tuned!