The Rust+GNOME Hackfest in Mexico City, part 1

Last week we had a Rust + GNOME hackfest in Mexico City (wiki page), kindly hosted by the Red Hat office there, in its very new and very cool office in the 22nd floor of a building and with a fantastic view of the northern part of the city. Allow me to recount the event briefly.

Inexplicably, in GNOME's 20 years of existence, there has never been a hackfest or event in Mexico. This was the perfect chance to remedy that and introduce people to the wonders of Mexican food.

My friend Joaquín Rosales, also from Xalapa, joined us as he is working on a very cool Rust-based monitoring system for small-scale spirulina farms using microcontrollers.

Alberto Ruiz started getting people together around last November, with a couple of video chats with Rust maintainers to talk about making it possible to write GObject implementations in Rust. Niko Matsakis helped along with the gnarly details of making GObject's and Rust's memory management play nicely with each other.

GObject implementations in Rust

During the hackfest, I had the privilege of sitting next to Niko to do an intensive session of pair programming to function as a halfway-reliable GObject reference while I fixed my non-working laptop (intermission: kids, never update all your laptop's software right before traveling. It will not work once you reach your destination.).

The first thing was to actually derive a new class from GObject, but in Rust. In C there is a lot of boilerplate code to do this, starting with the my_object_get_type() function. Civilized C code now defines all that boilerplate with the G_DEFINE_TYPE() macro. You can see a bit of the expanded code here.

What G_DEFINE_TYPE() does is to define a few functions that tell the GType system about your new class. You then write a class_init() function where you define your table of virtual methods (just function pointers in C), you register signals which your class can emit (like "clicked" for a GtkButton), and you can also define object properties (like "text" for the textual contents of a GtkEntry) and whether they are readable/writable/etc.

You also define an instance_init() function which is responsible for initializing the memory allocated to instances of your class. In C this is quite normal: you allocate some memory, and then you are responsible for initializing it. In Rust things are different: you cannot have uninitialized memory unless you jump through some unsafe hoops; you create fully-initialized objects in a single shot.

Finally, you define a finalize function which is responsible for freeing your instance's data and chaining to the finalize method in your superclass.

In principle, Rust lets you do all of this in the same way that you would in C, by calling functions in libgobject . In practice it is quite cumbersome. All the magic macros we have to define the GObject implementation boilerplate in gtype.h are there precisely because doing it in "plain C" is quite a drag. Rust makes this no different, but you can't use the C macros there.

A GObject in Rust

The first task was to write an actual GObject-derived class in Rust by hand, just to see how it could be done. Niko took care of this. You can see this mock object here. For example, here are some bits:

#[repr(C)] pub struct Counter { parent: GObject, } struct CounterPrivate { f: Cell<u32>, dc: RefCell<Option<DropCounter>>, } #[repr(C)] pub struct CounterClass { parent_class: GObjectClass, add: Option<extern fn(&Counter, v: u32) -> u32>, get: Option<extern fn(&Counter) -> u32>, set_drop_counter: Option<extern fn(&Counter, DropCounter)>, }

Here, Counter and CounterClass look very similar to the GObject boilerplate you would write in C. Both structs have GObject and GObjectClass as their first fields, so when doing C casts they will have the proper size and fields within those sub-structures.

CounterPrivate is what you would declare as the private structure with the actual fields for your object. Here, we have an f: Cell<u32> field, used to hold an int which we will mutate, and a DropCounter , an utility struct which we will use to assert that our Rust objects get dropped only once from the C-like implementation of the finalize() function.

Also, note how we are declaring two virtual methods in the CounterClass struct, add() and get() . In C code that defines GObjects, that is how you can have overridable methods: by exposing them in the class vtable. Since GObject allows "abstract" methods by setting their vtable entries to NULL , we use an Option around a function pointer.

The following code is the magic that registers our new type with the GObject machinery. It is what would go in the counter_get_type() function if it were implemented in C:

lazy_static! { pub static ref COUNTER_GTYPE: GType = { unsafe { gobject_sys::g_type_register_static_simple( gobject_sys::g_object_get_type(), b"Counter\0" as *const u8 as *const i8, mem::size_of::<CounterClass>() as u32, Some(CounterClass::init), mem::size_of::<Counter>() as u32, Some(Counter::init), GTypeFlags::empty()) } };

If you squint a bit, this looks pretty much like the corresponding code in G_DEFINE_TYPE(). That lazy_static!() means, "run this only once, no matter how many times it is called"; it is similar to g_once_*() . Here, gobject_sys::g_type_register_static_simple() and gobject_sys::g_object_get_type() are the direct Rust bindings to the corresponding C functions; they come from the low-level gobject-sys module in gtk-rs.

Here is the equivalent to counter_class_init() :

impl CounterClass { extern "C" fn init(klass: gpointer, _klass_data: gpointer) { unsafe { let g_object_class = klass as *mut GObjectClass; (*g_object_class).finalize = Some(Counter::finalize); gobject_sys::g_type_class_add_private(klass, mem::size_of::<CounterPrivate>>()); let klass = klass as *mut CounterClass; let klass: &mut CounterClass = &mut *klass; klass.add = Some(methods::add); klass.get = Some(methods::get); klass.set_drop_counter = Some(methods::set_drop_counter); } } }

Again, this is pretty much identical to the C implementation of a class_init() function. We even set the standard g_object_class.finalize field to point to our finalizer, written in Rust. We add a private structure with the size of our CounterPrivate ...

... which we later are able to fetch like this:

impl Counter { fn private(&self) -> &CounterPrivate { unsafe { let this = self as *const Counter as *mut GTypeInstance; let private = gobject_sys::g_type_instance_get_private(this, *COUNTER_GTYPE); let private = private as *const CounterPrivate; &*private } } }

I.e. we call g_type_instance_get_private() , just like C code would, to get the private structure. Then we cast it to our CounterPrivate and return that.

But that's all boilerplate

Yeah, pretty much. But don't worry! Niko made it possible to get rid of it in a comfortable way! But first, let's look at the non-boilerplate part of our Counter object. Here are its two interesting methods:

mod methods { #[allow(unused_imports)] use super::{Counter, CounterPrivate, CounterClass}; pub(super) extern fn add(this: &Counter, v: u32) -> u32 { let private = this.private(); let v = private.f.get() + v; private.f.set(v); v } pub(super) extern fn get(this: &Counter) -> u32 { this.private().f.get() } }

These should be familar to people who implement GObjects in C. You first get the private structure for your instance, and then frob it as needed.

No boilerplate, please

Niko spent the following two days writing a plugin for the Rust compiler so that we can have a mini-language to write GObject implementations comfortably. Instead of all the gunk above, you can simply write this:

extern crate gobject_gen; use gobject_gen::gobject_gen; use std::cell::Cell; gobject_gen! { class Counter { struct CounterPrivate { f: Cell<u32> } fn add(&self, x: u32) -> u32 { let private = self.private(); let v = private.f.get() + x; private.f.set(v); v } fn get(&self) -> u32 { self.private().f.get() } } }

This call to gobject_gen!() gets expanded to the the necessary boilerplate code. That code knows how to register the GType , how to create the class_init() and instance_init() functions, how to register the private structure and the utility private() to get it, and how to define finalize() . It will fill the vtable as appropriate with the methods you create.

We figured out that this looks pretty much like Vala, except that it generates GObjects in Rust, callable by Rust itself or by any other language, once the GObject Introspection machinery around this is written. That is, just like Vala, but for Rust.

And this is pretty good! We are taking an object system in C, which we must keep around for compatibility reasons and for language bindings, and making an easy way to write objects for it in a safe, maintained language. Vala is safer than plain C, but it doesn't have all the machinery to guarantee correctness that Rust has. Finally, Rust is definitely better maintained than Vala.

There is still a lot of work to do. We have to support registering and emitting signals, registering and notifying GObject properties, and probably some other GType arcana as well. Vala already provides nice syntax to do this, and we can probably use it with only a few changes.

Finally, the ideal situation would be for this compiler plugin, or an associated " cargo gir " step, to emit the necessary GObject Introspection information so that these GObjects can be called from other languages automatically. We could also spit C header files to consume the Rust GObjects from C.

And the other people in the hackfest?

I'll tell you in the next blog post!