Go forward in time to June 2017.
I ran into an interesting bug in my Rust code for librsvg. I had these structures in the C code and in the Rust code, respectively; they are supposed to be bit-compatible with each other.
/* C code */ /* Keep this in sync with rust/src/viewbox.rs::RsvgViewBox */ typedef struct { cairo_rectangle_t rect; gboolean active; } RsvgViewBox; /* Rust code */ /* Keep this in sync with rsvg-private.h:RsvgViewBox */ #[repr(C)] pub struct RsvgViewBox { pub rect: cairo::Rectangle, pub active: bool }
After I finished rustifying one of the SVG element types, a test started failing in an interesting way. The Rust code was generating a valid RsvgViewBox structure, but the C code was receiving it with a garbled active field.
It turns out that Rust's bool is not guaranteed to repr(C) as anything in particular. Rust chooses to do it as a single byte, with the only possible values being 0 or 1. In contrast, C code that uses gboolean assumes that gboolean is an int (... which C allows to be zero or anything else to represent a boolean value). Both structs have the same sizeof or mem::size_of, very likely due to struct alignment.
I'm on x86_64, which is of course a little-endian platform, so the low byte of my gboolean active field had the correct value, but the higher bytes were garbage from the stack.
The solution is obvious in retrospect: if the C code says you have a gboolean, bite the bullet and use a glib_sys::gboolean.
There are impl FromGlib<gboolean> for bool and the corresponding impl ToGlib for bool trait implementations, so you can do this:
extern crate glib; extern crate glib_sys; use self::glib::translate::* let my_gboolean: glib_sys::gboolean = g_some_function_that_returns_gboolean (); let my_rust_bool: bool = from_glib (my_gboolean); g_some_function_that_takes_gboolean (my_rust_bool.to_glib ());
... Which is really no different from the other from_glib() and to_glib() conversions you use when interfacing with the basic glib types.
Interestingly enough, when I had functions exported from Rust to C with repr(C), or C functions imported into Rust with extern "C", my naive assumption of gboolean <-> bool worked fine for passed arguments and return values. This is probably because C promotes chars to ints in function calls, and Rust was looking only at the first char in the value. Maybe? And maybe it wouldn't have worked on a big-endian platform? Either way, I was into undefined behavior (bool is not something you repr(C)), so anything goes. I didn't disassemble things to see what was actually happening.
There is an interesting, albeit extremely pedantic discussion, in this bug I filed about wanting a compiler warning for bool in a repr(C). People suggested that bool should just be represented as C's _Bool or bool if you include <stdbool.h>. BUT! Glib's gboolean predates C99, and therefore stdbool.h.
Instead of going into a pedantic rabbit hole of whether Glib is non-standard (I mean, it has only been around for over 20 years), or whether C99 is too new to use generally, I'll go with just using the appropriate conversions from gtk-rs.
Alternative conclusion: C99 is the Pont Neuf of programming languages.
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.
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.
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.
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.
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.
I'll tell you in the next blog post!
Go backward in time to February 2017.
Federico Mena-Quintero <federico@gnome.org> Wed 2017/Apr/05 16:18:55 CDT