How glib-rs works, part 3: Boxed types

- gnome, rust

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

Now let's get on and see how glib-rs handles boxed types.

Boxed types?

Let's say you are given a sealed cardboard box with something, but you can't know what's inside. You can just pass it on to someone else, or burn it. And since computers are magic duplication machines, you may want to copy the box and its contents... and maybe some day you will get around to opening it.

That's a boxed type. You get a pointer to something, who knows what's inside. You can just pass it on to someone else, burn it — I mean, free it — or since computers are magic, copy the pointer and whatever it points to.

That's exactly the API for boxed types.

typedef gpointer (*GBoxedCopyFunc) (gpointer boxed);
typedef void (*GBoxedFreeFunc) (gpointer boxed);

GType g_boxed_type_register_static (const gchar   *name,
                                    GBoxedCopyFunc boxed_copy,
                                    GBoxedFreeFunc boxed_free);

Simple copying, simple freeing

Imagine you have a color...

typedef struct {
    guchar r;
    guchar g;
    guchar b;
} Color;

If you had a pointer to a Color, how would you copy it? Easy:

Color *copy_color (Color *a)
{
    Color *b = g_new (Color, 1);
    *b = *a;
    return b;
}

That is, allocate a new Color, and essentially memcpy() the contents.

And to free it? A simple g_free() works — there are no internal things that need to be freed individually.

Complex copying, complex freeing

And if we had a color with a name?

typedef struct {
    guchar r;
    guchar g;
    guchar b;
    char *name;
} ColorWithName;

We can't just *a = *b here, as we actually need to copy the string name. Okay:

ColorWithName *copy_color_with_name (ColorWithName *a)
{
    ColorWithName *b = g_new (ColorWithName, 1);
    b->r = a->r;
    b->g = a->g;
    b->b = a->b;
    b->name = g_strdup (a->name);
    return b;
}

The corresponding free_color_with_name() would g_free(b->name) and then g_free(b), of course.

Glib-rs and boxed types

Let's look at this by parts. First, a BoxedMemoryManager trait to define the basic API to manage the memory of boxed types. This is what defines the copy and free functions, like above.

pub trait BoxedMemoryManager<T>: 'static {
    unsafe fn copy(ptr: *const T) -> *mut T;
    unsafe fn free(ptr: *mut T);
}

Second, the actual representation of a Boxed type:

pub struct Boxed<T: 'static, MM: BoxedMemoryManager<T>> {
    inner: AnyBox<T>,
    _dummy: PhantomData<MM>,
}

This struct is generic over T, the actual type that we will be wrapping, and MM, something which must implement the BoxedMemoryManager trait.

Inside, it stores inner, an AnyBox, which we will see shortly. The _dummy: PhantomData<MM> is a Rust-ism to indicate that although this struct doesn't actually store a memory manager, it acts as if it does — it does not concern us here.

The actual representation of boxed data

Let's look at that AnyBox that is stored inside a Boxed:

enum AnyBox<T> {
    Native(Box<T>),
    ForeignOwned(*mut T),
    ForeignBorrowed(*mut T),
}

We have three cases:

  • Native(Box<T>) - this boxed value T comes from Rust itself, so we know everything about it!

  • ForeignOwned(*mut T) - this boxed value T came from the outside, but we own it now. We will have to free it when we are done with it.

  • ForeignBorrowed(*mut T) - this boxed value T came from the outside, but we are just borrowing it temporarily: we don't want to free it when we are done with it.

For example, if we look at the implementation of the Drop trait for the Boxed struct, we will indeed see that it calls the BoxedMemoryManager::free() only if we have a ForeignOwned value:

impl<T: 'static, MM: BoxedMemoryManager<T>> Drop for Boxed<T, MM> {
    fn drop(&mut self) {
        unsafe {
            if let AnyBox::ForeignOwned(ptr) = self.inner {
                MM::free(ptr);
            }
        }
    }
}

If we had a Native(Box<T>) value, it means it came from Rust itself, and Rust knows how to Drop its own Box<T> (i.e. a chunk of memory allocated in the heap).

But for external resources, we must tell Rust how to manage them. Again: in the case where the Rust side owns the reference to the external boxed data, we have a ForeignOwned and Drop it by free()ing it; in the case where the Rust side is just borrowing the data temporarily, we have a ForeignBorrowed and don't touch it when we are done.

Copying

When do we have to copy a boxed value? For example, when we transfer from Rust to Glib with full transfer of ownership, i.e. the to_glib_full() pattern that we saw before. This is how that trait method is implemented for Boxed:

impl<'a, T: 'static, MM: BoxedMemoryManager<T>> ToGlibPtr<'a, *const T> for Boxed<T, MM> {
    fn to_glib_full(&self) -> *const T {
        use self::AnyBox::*;
        let ptr = match self.inner {
            Native(ref b) => &**b as *const T,
            ForeignOwned(p) | ForeignBorrowed(p) => p as *const T,
        };
        unsafe { MM::copy(ptr) }
    }
}

See the MM:copy(ptr) in the last line? That's where the copy happens. The lines above just get the appropriate pointer to the data data from the AnyBox and cast it.

There is extra boilerplate in boxed.rs which you can look at; it's mostly a bunch of trait implementations to copy the boxed data at the appropriate times (e.g. the FromGlibPtrNone trait), also an implementation of the Deref trait to get to the contents of a Boxed / AnyBox easily, etc. The trait implementations are there just to make it as convenient as possible to handle Boxed types.

Who implements BoxedMemoryManager?

Up to now, we have seen things like the implementation of Drop for Boxed, which uses BoxedMemoryManager::free(), and the implementation of ToGlibPtr which uses ::copy().

But those are just the trait's "abstract" methods, so to speak. What actually implements them?

Glib-rs has a general-purpose macro to wrap Glib types. It can wrap boxed types, shared pointer types, and GObjects. For now we will just look at boxed types.

Glib-rs comes with a macro, glib_wrapper!(), that can be used in different ways. You can use it to automatically write the boilerplate for a boxed type like this:

glib_wrapper! {
    pub struct Color(Boxed<ffi::Color>);

    match fn {
        copy => |ptr| ffi::color_copy(mut_override(ptr)),
        free => |ptr| ffi::color_free(ptr),
        get_type => || ffi::color_get_type(),
    }
}

This expands to an internal glib_boxed_wrapper!() macro that does a few things. We will only look at particularly interesting bits.

First, the macro creates a newtype around a tuple with 1) the actual data type you want to box, and 2) a memory manager. In the example above, the newtype would be called Color, and it would wrap an ffi:Color (say, a C struct).

        pub struct $name(Boxed<$ffi_name, MemoryManager>);

Aha! And that MemoryManager? The macro defines it as a zero-sized type:

        pub struct MemoryManager;

Then it implements the BoxedMemoryManager trait for that MemoryManager struct:

        impl BoxedMemoryManager<$ffi_name> for MemoryManager {
            #[inline]
            unsafe fn copy($copy_arg: *const $ffi_name) -> *mut $ffi_name {
                $copy_expr
            }

            #[inline]
            unsafe fn free($free_arg: *mut $ffi_name) {
                $free_expr
            }
        }

There! This is where the copy/free methods are implemented, based on the bits of code with which you invoked the macro. In the call to glib_wrapper!() we had this:

        copy => |ptr| ffi::color_copy(mut_override(ptr)),
        free => |ptr| ffi::color_free(ptr),

In the impl aboe, the $copy_expr will expand to ffi::color_copy(mut_override(ptr)) and $free_expr will expand to ffi::color_free(ptr), which defines our implementation of a memory manager for our Color boxed type.

Zero-sized what?

Within the macro's definition, let's look again at the definitions of our boxed type and the memory manager object that actually implements the BoxedMemoryManager trait. Here is what the macro would expand to with our Color example:

        pub struct Color(Boxed<ffi::Color, MemoryManager>);

        pub struct MemoryManager;

        impl BoxedMemoryManager<ffi::Color> for MemoryManager {
            unsafe fn copy(...) -> *mut ffi::Color { ... }
            unsafe fn free(...) { ... }
        }

Here, MemoryManager is a zero-sized type. This means it doesn't take up any space in the Color tuple! When a Color is allocated in the heap, it is really as if it contained an ffi::Color (the C struct we are wrapping) and nothing else.

All the knowledge about how to copy/free ffi::Color lives only in the compiler thanks to the trait implementation. When the compiler expands all the macros and monomorphizes all the generic functions, the calls to ffi::color_copy() and ffi::color_free() will be inlined at the appropriate spots. There is no need to have auxiliary structures taking up space in the heap, just to store function pointers to the copy/free functions, or anything like that.

Next up

You may have seen that our example call to glib_wrapper!() also passed in a ffi::color_get_type() function. We haven't talked about how glib-rs wraps Glib's GType, GValue, and all of that. We are getting closer and closer to being able to wrap GObject.

Stay tuned!