How Glib-rs works, part 2: Transferring lists and arrays

- Tags: gnome, rust

(First part of the series, with index to all the articles)

In the first part, we saw how glib-rs provides the FromGlib and ToGlib traits to let Rust code convert from/to Glib's simple types, like to convert from a Glib gboolean to a Rust bool and vice-versa. We also saw the special needs of strings; since they are passed by reference and are not copied as simple values, we can use FromGlibPtrNone and FromGlibPtrFull depending on what kind of ownership transfer we want, none for "just make it look like we are using a borrowed reference", or full for "I'll take over the data and free it when I'm done". Going the other way around, we can use ToGlibPtr and its methods to pass things from Rust to Glib.

In this part, we'll see the tools that glib-rs provides to do conversions of more complex data types. We'll look at two cases:

And one final case just in passing:

Passing arrays from Glib to Rust

We'll look at the case for transferring null-terminated arrays of strings, since it's an interesting one. There are other traits to convert from Glib arrays whose length is known, not implied with a NULL element, but for now we'll only look at arrays of strings.

Null-terminated arrays of strings

Look at this function for GtkAboutDialog:

/**
 * gtk_about_dialog_add_credit_section:
 * @about: A #GtkAboutDialog
 * @section_name: The name of the section
 * @people: (array zero-terminated=1): The people who belong to that section
 * ...
 */
void
gtk_about_dialog_add_credit_section (GtkAboutDialog  *about,
                                     const gchar     *section_name,
                                     const gchar    **people)

You would use this like

const gchar *translators[] = {
    "Alice <alice@example.com>",
    "Bob <bob@example.com>",
    "Clara <clara@example.com>",
    NULL
};

gtk_about_dialog_add_credit_section (my_about_dialog, _("Translators"), translators);

The function expects an array of gchar *, where the last element is a NULL. Instead of passing an explicit length for the array, it's done implicitly by requiring a NULL pointer after the last element. The gtk-doc annotation says (array zero-terminated=1). When we generate information for the GObject-Introspection Repository (GIR), this is what comes out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<method name="add_credit_section"
        c:identifier="gtk_about_dialog_add_credit_section"
        version="3.4">
  ..
    <parameter name="people" transfer-ownership="none">
      <doc xml:space="preserve">The people who belong to that section</doc>
      <array c:type="gchar**">
        <type name="utf8" c:type="gchar*"/>
      </array>
    </parameter>

You can see the transfer-ownership="none" in line 5. This means that the function will not take ownership of the passed array; it will make its own copy instead. By convention, GIR assumes that arrays of strings are NULL-terminated, so there is no special annotation for that here. If we were implementing this function in Rust, how would we read that C array of UTF-8 strings and turn it into a Rust Vec<String> or something? Easy:

let c_char_array: *mut *mut c_char = ...; // comes from Glib
let rust_translators = FromGlibPtrContainer::from_glib_none(c_char_array);
// rust_translators is a Vec<String>

Let's look at how this bad boy is implemented.

First stage: impl FromGlibPtrContainer for Vec<T>

We want to go from a "*mut *mut c_char" (in C parlance, a "gchar **") to a Vec<String>. Indeed, there is an implementation of the FromGlibPtrContainer trait for Vecs here. These are the first few lines:

impl <P: Ptr, PP: Ptr, T: FromGlibPtrArrayContainerAsVec<P, PP>> FromGlibPtrContainer<P, PP> for Vec<T> {
    unsafe fn from_glib_none(ptr: PP) -> Vec<T> {
        FromGlibPtrArrayContainerAsVec::from_glib_none_as_vec(ptr)
    }

So... that from_glib_none() will return a Vec<T>, which is what we want. Let's look at the first few lines of FromGlibPtrArrayContainerAsVec:

1
2
3
4
    impl FromGlibPtrArrayContainerAsVec<$ffi_name, *mut $ffi_name> for $name {
        unsafe fn from_glib_none_as_vec(ptr: *mut $ffi_name) -> Vec<Self> {
            FromGlibContainerAsVec::from_glib_none_num_as_vec(ptr, c_ptr_array_len(ptr))
        }

Aha! This is inside a macro, thus the $ffi_name garbage. It's done like that so the same trait can be implemented for const and mut pointers to c_char.

See the call to c_ptr_array_len() in line 3? That's what figures out where the NULL pointer is at the end of the array: it figures out the array's length.

Second stage: impl FromGlibContainerAsVec::from_glib_none_num_as_vec()

Now that the length of the array is known, the implementation calls FromGlibContainerAsVec::from_glib_none_num_as_vec()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    impl FromGlibContainerAsVec<$ffi_name, *const $ffi_name> for $name {
        unsafe fn from_glib_none_num_as_vec(ptr: *const $ffi_name, num: usize) -> Vec<Self> {
            if num == 0 || ptr.is_null() {
                return Vec::new();
            }

            let mut res = Vec::with_capacity(num);
            for i in 0..num {
                res.push(from_glib_none(ptr::read(ptr.offset(i as isize)) as $ffi_name));
            }
            res
        }

Lines 3/4: If the number of elements is zero, or the array is NULL, return an empty Vec.

Line 7: Allocate a Vec of suitable size.

Lines 8/9: For each of the pointers in the C array, call from_glib_none() to convert it from a *const c_char to a String, like we saw in the first part.

Done! We started with a *mut *mut c_char or a *const *const c_char and ended up with a Vec<String>, which is what we wanted.

Passing GLists to Rust

Some functions don't give you an array; they give you a GList or GSList. There is an implementation of FromGlibPtrArrayContainerAsVec that understands GList:

impl<T> FromGlibPtrArrayContainerAsVec<<T as GlibPtrDefault>::GlibType, *mut glib_ffi::GList> for T
where T: GlibPtrDefault + FromGlibPtrNone<<T as GlibPtrDefault>::GlibType> + FromGlibPtrFull<<T as GlibPtrDefault>::GlibType> {

    unsafe fn from_glib_none_as_vec(ptr: *mut glib_ffi::GList) -> Vec<T> {
        let num = glib_ffi::g_list_length(ptr) as usize;
        FromGlibContainer::from_glib_none_num(ptr, num)
    }

The impl declaration is pretty horrible, so just look at the method: from_glib_none_as_vec() takes in a GList, then calls g_list_length() on it, and finally calls FromGlibContainer::from_glib_none_num() with the length it computed.

I have a Glib container and its length

In turn, that from_glib_none_num() goes here:

impl <P, PP: Ptr, T: FromGlibContainerAsVec<P, PP>> FromGlibContainer<P, PP> for Vec<T> {
    unsafe fn from_glib_none_num(ptr: PP, num: usize) -> Vec<T> {
        FromGlibContainerAsVec::from_glib_none_num_as_vec(ptr, num)
    }

Okay, getting closer to the actual implementation.

Give me a vector already

Finally, we get to the function that walks the GList:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
impl<T> FromGlibContainerAsVec<<T as GlibPtrDefault>::GlibType, *mut glib_ffi::GList> for T
where T: GlibPtrDefault + FromGlibPtrNone<<T as GlibPtrDefault>::GlibType> + FromGlibPtrFull<<T as GlibPtrDefault>::GlibType> {

    unsafe fn from_glib_none_num_as_vec(mut ptr: *mut glib_ffi::GList, num: usize) -> Vec<T> {
        if num == 0 || ptr.is_null() {
            return Vec::new()
        }
        let mut res = Vec::with_capacity(num);
        for _ in 0..num {
            let item_ptr: <T as GlibPtrDefault>::GlibType = Ptr::from((*ptr).data);
            if !item_ptr.is_null() {
                res.push(from_glib_none(item_ptr));
            }
            ptr = (*ptr).next;
        }
        res
    }

Again, ignore the horrible impl declaration and just look at from_glib_none_num_as_vec().

Line 4: that function takes in a ptr to a GList, and a num with the list's length, which we already computed above.

Line 5: Return an empty vector if we have an empty list.

Line 8: Allocate a vector of suitable capacity.

Line 9: For each element, convert it with from_glib_none() and push it to the array.

Line 14: Walk to the next element in the list.

Passing containers from Rust to Glib

This post is getting a bit long, so I'll just mention this briefly. There is a trait ToGlibContainerFromSlice that takes a Rust slice, and can convert it to various Glib types.

  • To GSlist and GList. These have methods like to_glib_none_from_slice() and to_glib_full_from_slice()

  • To an array of fundamental types. Here, you can choose between to_glib_none_from_slice(), which gives you a Stash like we saw the last time. Or, you can use to_glib_full_from_slice(), which gives you back a g_malloc()ed array with copied items. Finally, to_glib_container_from_slice() gives you back a g_malloc()ed array of pointers to values rather than plain values themselves. Which function you choose depends on which C API you want to call.

I hope this post gives you enough practice to be able to "follow the traits" for each of those if you want to look at the implementations.

Next up

Passing boxed types, like public structs.

Passing reference-counted types.

How glib-rs wraps GObjects.