Updated Thoughts on using GTK in Rust
I've used it a little bit more now, and my opinions have developed

25 April 2017

A few months back, I started using GTK inside a Rust project I was working on. At the time, I wrote a post about how I was making it all fit together. Making this work might be old news to some, but I’ve been working in web-based user interfaces for a long time and most of my usual tricks from HTML and JavaScript can’t just be taken directly into GTK and Rust. Some of my opinions have changed a bit since I started, and I’d like to give an update.

All of the examples here are taken from the project I’m working on, Rusty Microphone. You can see the full source code listing on GitHub.

A Recap of the Basics

I use GTK from Rust using the Gtk-rs crate. To get going, add it to the dependencies in your project’s Cargo.toml file like so:

[dependencies]
gtk = "0.1.1"

Next, create a function for launching your GUI.

extern crate gtk;
use gtk;
use gtk::prelude::*;

fn start_gui() -> Result<(), String> {
    try!(gtk::init().map_err(|_| "Failed to initialize GTK."));

    // do any app setup here
    // create interface elements, add callbacks, etc

  
    // Your program will sit in gtk::main()
    // until something calls gtk::main_quit()
    gtk::main();
    Ok(())
}

The rest of this post is about how to manage Rust between gtk::init() and gtk::main().

Scrap the XML. Long live strong types.

In my very first attempt, I had a function to construct my GUI which returned only the window. The signature looked like this:

fn create_window() -> gtk::Window

The major problem with this approach is that when I started hooking up functionality to the GUI, I needed a reference to the individual components. The window actually has a reference to all of its child components, so in theory you can get to them all from the window, but this resulted in code that would break if the layout was changed.

I solved this at the time using a GTK function that let me construct the interface from an XML string that looked something like this:

const GUI_XML: &'static str = r#"
<interface>
  <object class="GtkWindow" id="window">
    <property name="title">Rusty Microphone</property>
    <child>
      <object class="GtkComboBoxText" id="dropdown">
      </object>
    </child>
  </object>
</interface>
"#;

let gtk_builder = gtk::Builder::new_from_string(GUI_XML);

let dropdown: gtk::ComboBoxText = try!(
    gtk_builder.get_object("dropdown")
        .ok_or("GUI does not contain an object with id 'dropdown'")
);

The problem with this is that every time I wanted to get a reference to a component, I had to do the error handling around “what if I had a typo in the element’s id?”, which is a repetitive pain.

My new approach, which works much better, is to create a struct to model the parts of the interface that I care about. Then, I create a constructor function for the struct. The constructor can care about the precise layout of the components into the window, including any layout elements that I won’t ever need to reference in the rest of my GUI. At the end, it should return an object with all of the UI elements I actually want to add functionality to.

struct RustyUi {
    dropdown: gtk::ComboBoxText,
    pitch_label: gtk::Label,
    pitch_error_indicator: gtk::DrawingArea,
    oscilloscope_chart: gtk::DrawingArea,
    freq_chart: gtk::DrawingArea,
    correlation_chart: gtk::DrawingArea,
    oscilloscope_toggle_button: gtk::Button,
    freq_toggle_button: gtk::Button,
    correlation_toggle_button: gtk::Button
}

fn create_window(microphones: Vec<(u32, String)>) -> RustyUi {
    let window = gtk::Window::new(gtk::WindowType::Toplevel);
    window.set_title("Rusty Microphone");
    window.connect_delete_event(|_, _| {
        gtk::main_quit();
        Inhibit(false)
    });

    // vbox is a layout element that we don't actually need to
    // address directly when we're hooking up functionality
    let vbox = gtk::Box::new(gtk::Orientation::Vertical, 5);
    window.add(&vbox);

    let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 2);
    vbox.add(&hbox);

    // we want a reference to this dropdown later, so it's on the
    // object we return
    let dropdown = gtk::ComboBoxText::new();
    dropdown.set_hexpand(true);
    set_dropdown_items(&dropdown, microphones);
    hbox.add(&dropdown);
  
    let oscilloscope_toggle_button = gtk::Button::new_with_label("Osc");
    hbox.add(&oscilloscope_toggle_button);
    let freq_toggle_button = gtk::Button::new_with_label("FFT");
    hbox.add(&freq_toggle_button);
    let correlation_toggle_button = gtk::Button::new_with_label("Corr");
    hbox.add(&correlation_toggle_button);

    let pitch_label = gtk::Label::new(None);
    vbox.add(&pitch_label);

    let pitch_error_indicator = gtk::DrawingArea::new();
    pitch_error_indicator.set_size_request(600, 50);
    vbox.add(&pitch_error_indicator);

    let oscilloscope_chart = gtk::DrawingArea::new();
    oscilloscope_chart.set_size_request(600, 250);
    oscilloscope_chart.set_vexpand(true);
    vbox.add(&oscilloscope_chart);
  
    let freq_chart = gtk::DrawingArea::new();
    freq_chart.set_size_request(600, 250);
    freq_chart.set_vexpand(true);
    vbox.add(&freq_chart);

    let correlation_chart = gtk::DrawingArea::new();
    correlation_chart.set_size_request(600, 250);
    correlation_chart.set_vexpand(true);
    vbox.add(&correlation_chart);

    window.show_all();
  
    RustyUi {
        dropdown: dropdown,
        pitch_label: pitch_label,
        pitch_error_indicator: pitch_error_indicator,
        oscilloscope_chart: oscilloscope_chart,
        freq_chart: freq_chart,
        correlation_chart: correlation_chart,
        oscilloscope_toggle_button: oscilloscope_toggle_button,
        freq_toggle_button: freq_toggle_button,
        correlation_toggle_button: correlation_toggle_button
    }
}

Having a reference for callbacks

The other problem that I only briefly touched on in my last post about GTK was being able to have mutable references to your application in multiple places. Generally, Rust only allows there to be one mutable reference to something at a time, and will enforce this constraint with a compile time error. You can, however, move this check to run time by using a standard library component called RefCell.

RefCell on its own, however, is not enough. You also need to be able to have multiple references to the same object. Going back to Rust’s rules around ownership and borrowing, a borrowed value may not outlive its owner, and if we need to access the application state from a callback this is hard to guarantee. We solve this hurdle by using Rc, a reference counted pointer. If you’re familiar with smart pointers from C++, think of shared_ptr. We can make a copy of the Rc, the callback can own the copy, and it will still point to the same application state as everything else since we only copied the pointer. Then, if we need to, we can use the RefCell to make the reference mutable.

To recap, between Rc and RefCell, I can construct an application state object which

  1. I can give to a callback
  2. Inside the callback, I can mutate the application state if necessary

Let’s take a look at this in action.

struct ApplicationState {
    pa: pa::PortAudio,
    pa_stream: Option<pa::Stream<pa::NonBlocking, pa::Input<f32>>>,
    ui: RustyUi
}

pub fn start_gui() -> Result<(), String> {
    let pa = try!(::audio::init().map_err(|e| e.to_string()));
    let microphones = try!(::audio::get_device_list(&pa).map_err(|e| e.to_string()));

    try!(gtk::init().map_err(|_| "Failed to initialize GTK."));

    let state = Rc::new(RefCell::new(ApplicationState {
        pa: pa,
        pa_stream: None,
        ui: create_window(microphones)
    }));

    let (mic_sender, mic_receiver) = channel();

    // notice the .clone(). The pointer is copied and ownership of the
    // copy of the pointer is given to the function.
    connect_dropdown_choose_microphone(mic_sender, state.clone());

    gtk::main();
    Ok(())
}

fn connect_dropdown_choose_microphone(mic_sender: Sender<Vec<f64>>, 
                                      state: Rc<RefCell<ApplicationState>>) {
    // We need to copy the pointer for the state here because
    // ownership of state pointer itself is going to be taken over by
    // the callback.
    let outer_state = state.clone();
    let ref dropdown = outer_state.borrow().ui.dropdown;

    // putting 'move' on the lambda makes it take ownership of any
    // variables it references. This is necessary, since as a callback
    // it will run after the outer function completes.
    dropdown.connect_changed(move |dropdown: &gtk::ComboBoxText| {
        match state.borrow_mut().pa_stream {
            Some(ref mut stream) => {stream.stop().ok();},
            _ => {}
        }
        let selected_mic = match dropdown.get_active_id().and_then(|id| id.parse().ok()) {
            Some(mic) => mic,
            None => {return;}
        };
        let stream = ::audio::start_listening(
            &state.borrow().pa,
            selected_mic, mic_sender.clone()
        ).ok();
        if stream.is_none() {
            writeln!(io::stderr(), "Failed to open audio channel").ok();
        }
        state.borrow_mut().pa_stream = stream;
    });
}

Threading isn’t Scary

All of GTK’s callbacks will happen on the same thread. That saved us from some headaches up to now, since Rc doesn’t work across multiple threads. If we want to start doing some heavy processing, then it’s time to start learning how to use multiple threads.

Rust provides two major approaches to communicating between threads in its standard library: message passing, and shared memory. If you try to use either inappropriately, the compiler will tell you about it and refuse to continue. I’m going to postpone writing about this to a future post, but for a brief summary to get you started

  • Do message passing with a channel
  • For shared memory between threads, swap out Rc and RefCell for Arc and RwLock

GTK itself is ‘thread-aware’, but isn’t thread safe out of the box. For now, it seems the best way to handle it is to keep all GTK related calls to the same thread, and push off any heavy processing to other threads.

Mutability rules get weird when calling C code

At some point while using GTK, I wrote these two lines of code, and it compiled:

let dropdown = gtk::ComboBoxText::new();
dropdown.set_hexpand(true);

This looks like a mistake. I’m declaring my dropdown as immutable, but clearly I’m mutating it on just the next line. I decided to take a closer look at what was happening under the covers.

Most of the GTK calls that I’m using in Rust, like set_hexpand are actually just a thin wrapper provided by Gtk-rs that calls out to the real GTK library which was written in C. Here is the actual Rust code doing that call:

fn set_hexpand(&self, expand: bool) {
    unsafe {
        ffi::gtk_widget_set_hexpand(self.to_glib_none().0, expand.to_glib());
    }
}

Unfortunately, the real GTK doesn’t really keep track of mutable state as strictly as Rust does. The code is wrapped in an unsafe block to indicate that the normal rules may not apply (you need this because Rust can’t control what C code is doing). If this function was just declared as taking in a mutable reference, this would all be a bit clearer.

To be fair, GTK is a massive library, so I’m grateful to have access to all the APIs even if it does have a few pitfalls here and there. Just be aware that some of the constraints that you get used to relying on when writing Rust might not hold when you’re calling external libraries, especially when you’re calling out to non-Rust code.

The way forward

While I still don’t feel I’m necessarily at the ‘best’ way of doing a GTK interface using Rust, I have reached the point where I can add new functionality and refactor without worrying that everything is broken.

For right now, with the current scale of my user interface (which is fairly simple), this approach is working well for me. I imagine that as I move into more complex interfaces, I will need to develop more robust patterns to keep the complex things simple enough to work with.


If you liked this article, please share it on Twitter, Facebook, Google+, or by using the Permalink.


You can send me comments on this post at , or @JWorthe.

Do you find this website useful? Check out my support page for options on how to buy me a cup of coffee.


More on Worthe It

Previous Post

09 Apr 2017

Automated deployments, static websites, and a little bit of FTP
Next Post

11 May 2017

Learn from my mistake, and think about the risks being managed
Latest Post

19 Sep 2017

An awesome tool for software documentation and visualizing graphs
Browse the Blog Archive

16 Dec 2014 - 19 Sep 2017

See all of the stuff I've written and put on this site.