(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 valueT
comes from Rust itself, so we know everything about it! -
ForeignOwned(*mut T)
- this boxed valueT
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 valueT
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!