Rust is a systems programming language, that describes itself as a ‘safe, concurrent, practical language’.
Because Rust supports cross compilation since its early stages and provides a platform agnostic standard library, it seems to be a perfect fit to develop native, high-performant graphical desktop applications.
I therefore set out to explore the realms of GUI development with Rust to gain an increased understanding of its quirks and evaluate its readiness for being used in our projects.
Getting a feeling for Rust
So, before I go over how to build a GUI with Rust, let’s quickly iterate over some aspects of the language, that make it pleasant to work with.
So, what makes Rust cool and why should I use it for that project?
… or rather the lack thereof.
In safe rust there is no null-pointer. This is done by the following:
If you allocate a resource (e.g. a String), you need to fill it with a value (this is called RAII). But what to do if the content is not known yet? Rust provides an
Option type, an enum either containing
None . So is
null just renamed to
None? Fortunately not. To use the contents of an
Option you need to explicitly handle the case that it is
None, for example by using a
match statement, providing an alternative content or using
.unwrap() means, that you allow your code to panic (rust-slang for a more-or-less controlled crash) at this point. This is (obviously) bad practice, easily discoverable by static analysis (aka. grep in this case) and a deliberate action, that forces you to be aware that this can go wrong at that place.
One of the main goals of the language is safety. This is achieved by using a combinations of multiple concepts, such as the ownership system, lifetimes and explicit mutability. With this, rust does not only ensure safety at compile time but also makes it easy to create clean, well-behaved code and hard(er) to create an unmaintainable mess.
If you are starting out with rust, have a look at the ownership system, since it is an integral component of the language and is the enabler for most the nice features. In combination with the already mentioned RAII paradigm, the ownership system can prevent nullpointers and dangling pointers, just to name two representatives of a whole zoo of nasty problems.
Since those concepts are covered in depth in other places (e.g. here if you like the official docs, here if you like animated stick figures and here if you are interested in the internals and don’t mind wrapping your head around some pointers) and require some time and thinking to understand, let’s have a look at two other things here: immutable references and iterators, since they play together nicely.
Consider the following snippet:
The function parameters
b are of type
&str, meaning an immutable reference to a string slice, the borrowed form of a string in rust. Since I used
& instead of
&mut, the reference to the String is immutable, assuring you that if you hand the reference to some code, the contents of the String won’t be changed.
On the other side, when declaring the function like this, you ensure that you are not going to edit the strings, making it way easier to reason about the code. Doing this is not new, Ada( although with a bit weird syntax), allows the same reasoning on method parameters (
out parameters, have a look at this if you are interested) but in rust the borrowing system also expands to all references, enabling the enforcement of the famous borrowing-rules.
In the sample above, I further make use of the iterators of rust:
.chars() giving access to an iterator over (utf8) characters of a string. When zipped together, two iterators form another iterator of tuples, denoted by the
(x, y) notation. Since they are a feature of the language, one can avoid constructs like
Pair<A,B> and thanks to type inference, one doesn’t even has to specify the types, since they are clear to the compiler.
In the above, when iterating over the read-only string slices, one does not even has to think about problems like a ConcurrentModificationException, known from the Java world, since the compiler would complain if we tried to modify the contents of our strings.
Multithreading, Iterators (again) and pattern matching
Since I do not want the UI to block while saving a potentially large image, I designed the application to run a special Thread responsible for generating previews and saving the resulting images. In hindsight this turned out to be quite unnecessary since the
image crate is so fast, there is no noticeable delay when saving reasonably sized images. None-the less it shows how easy it is to use multithreading in rust:
Let’s walk through the above code step by step: To facilitate inter-thread communication, there is an mpsc channel, allowing the UI to send messages from the
tx (transmitter) to the
rx (receiver). Then a new thread is spawned, passing it a closure with a capture environment. That thread then declares its local state consisting of the
lines variable. It then iterates over all elements received by the channel, calling the code starting in L14 with the received request in
req. The code then matches the
req variable to several possible types representing the different kinds of requests that are then executed.
Rust makes it hard to have shared state, pushing you towards separating your state into the threads it belongs and letting them communicate over channels. By doing so, the compiler eliminates race conditions, lost-updates and deadlocks already at compile time and makes it way easier to reason about the code.
If you want or need to have shared state, first reason if you can’t solve it with channels, think if your plan might be a bad idea (I often realized this in hindsight) and only then have a look at the Mutex.
An alternative to using the pattern-matching above, would be using Traits.
Testing is an integral requirement for reliable software. Therefore it is understandable that rust makes it easy for developers to write several kinds of tests, reducing the burden of getting yourself to write them. Integration-tests are located in a separated directory tree, while unit tests can be written directly at the end of a file. Consider the following example:
This test ensures that the resulting preview image has the desired dimensions. Since the
generate_preview function is private (lacking the
pub keyword), it makes sense to test its functionality in the same place as from where it can be used.
What bothers me so far with rust are the verbose signatures for some functions, just have a look at this behemoth:
This handler is responsible to handle an
EventMotion within a GTK-Component (
Fixed) in relation to a the background dimensions of type
(u32,u32), select the
TextArea that the user has clicked-on (defined by the
text_idx variable) from a Vector behind a mutable reference (since I want to be able to move the text are and therefore need to have mutable access). Part of this clumsy-ness is due to the fact that
gtk-rs takes ownership of the handler-closures and one therefore needs to pass references to it, that it can take ownership of.
To start developing with Rust, one needs to set up the development environment, thanks to rustup, this works without much hassle. Once rustup is installed you can select the desired toolchains and components, e.g.
rust-doc to have the documentation available offline and
rust-src to download the sources of the standard libraries.
To create a GUI application, there are several libraries (in rust slang, ‘crates’) available.
Thanks to the active and open Rust community, there exist sites like this or this one that aggregate information about libraries and resources to a specific topic.
If you need to search for a specific library, crates.io is your place-to-go, providing search, categories and all sorts of information about available crates. In search of a GUI-library, I took a quick look at the ecosystem, below you can find a quick line-up of available solutions for low-level and high-level approaches.
For cross-plattform GUI-development there are several options available, like ‘gtk-rs’ providing bindings to GTK3+ or ‘conrod’, a low-level library directly interfacing with OpenGL or comparable technologies like Vulkan.
A layer above, there is ‘relm’, a library based on
gtk-rs. At the time writing, it was not in a mature enough state to support some widgets I thought might be needed.
I decided to use gtk-rs since it is a mature library and provides a reasonable layer of abstraction for our use case, thereby hopefully providing a sweet spot between stability and ease of use.
Unfortunately, since the
gtk-rslibrary is a wrapper around a C/C++ library, the generated bindings are not necessarily very rust-y and one will most likely encounter some weird-looking code, that would have been prevented with a higher-level abstraction.
gtk-rs is a wrapper around the official GTK3+ libraries. This means, that it creates Rust functions that are layered on top of the C functions provided by the libraries via the Foreign Function Interface (FFI) of Rust.
This enables us to use safe Rust types and functions, while gtk-rs encapsulates the inherently unsafe FFI ones (since Rust cannot guarantee for the safety of external C++ code).
With this setup, one can reuse the GTK libraries without modification but that also means, that they need to be available at compile- or run-time, depending on the generated executable.
Not only has there to be some GTK library be available, it is required that the gtk and gtkrs libraries are ABI-compatible for the desired architecture and platform.
GTK provides us with a handy tool ‘glade’ that allows us to design GUIs in a graphical manner and save the specification in an XML file. That XML file can then be used by
gtk-rs to create the whole UI, providing an elegant way to define the layout and properties of widgets in a way that is programming language agnostic.
A minimal example application, loading the UI definition from glade and doing something when a button is pressed, can look like this:
The comments in the example above explain all the important parts, just remember to call
show_all() on the window, otherwise your UI will be empty.
To connect our Rust code to the UI, I subscribe to events generated by GTK and tell the library to execute one of our closures, for example like this:
However, if I want to manipulate widgets from within the closure, e.g. for changing the size of the displayed preview depending on the image selected in the file chooser dialog, I need to take ownership of the reference to the
window reference (this is partly due to the influences of the used C++ library). Closures in Rust are all-or-nothing regarding moves, meaning one can either take ownership of all the used references or of none.
This leads to pretty ugly cloning of
Rc<RefCell<...>> types for each closure and variable, so one can make use of another nice feature of Rust: macros.
In the code sample below,
clone! is a macro invocation (as denoted by the exclamation mark), that clones the supplied references (lines and window) for this closure. This non-idiomatic code is necessary due to the library design of GTK, since I only interface with it via FFI and therefore have to adhere to their paradigms, that have not been designed with Rust in mind.
gtk-rs and Rust feels a bit alien for the first few hours. Some functions are not quite well documented in the Rust crate but have to be looked up in the gtk libs, that the Rust functions wrap. But since
gtk-rs is open-source, one is free to create PRs improving the docs.
For example, did you know that you can (or need to) enable certain types of events like scrolling per input device in GTK?
After a few hours however, it feels quite natural to subscribe to the events of the UI and handle them in functions.
Since I developed the application on a linux installation, running it on linux was a breeze:
cargo build --release creates an executable that can be copied somewhere and you can directly execute it. However, because the application should run on different architectures (e.g. ARM) or other operating systems, a bit more work is required.
Compiling for Windows
One option to create a windows executable would be to install the toolchain on a windows machine and build the application there. Since my Windows VM wasn’t that performant, I refrained from compiling it in there. This is even more noticeable, when you think of compiling to ARM on a RaspberryPi, which would take ages.
So to create windows binaries nonetheless, Rust comes with pretty nice support for crosscompilation. Rust needs to have the toolchain for the target system installed, consisting of a triple
x86_64-unknown-linux-musl to use the musl ABIs instead of the default gcc ones (note that libraries compatible to the chosen ABIs must be present on the system). So to compile for windows, the mingw toolchain can be installed:
aurman -S mingw-w64-gcc-base for the toolchain and
rustup target add x86_64-pc-windows-gnu for the corresponding Rust target.
As mentioned above, the GTK libs need to be available for the ABI, on Archlinux they can be installed with
aurman -S mingw-w64-gtk3 (and all of the dependencies). There is a nice tutorial available on the gtk-rs website.
Please note that Rust applications also have a dependency to a recent glib. So you can copy the resulting executable from one Linux machine to another but be aware that there might be problems on ancient machines with old glib versions.
The final executable can, depending on the platform, can be augmented with fancy things like icons.
I encountered some limitations however, for us mostly regarding the tooling around Rust.
Closed source crates
The problem here is two-fold: cargo is expecting the source code for the crates your program is depending on, so it can compile it (thereby ruling out the problem of having libraries with non-matching architectures or versions installed). The other is, that you cannot simply distribute the resulting binary as a library for others to link against, since Rust does not (yet?) have stable ABIs in its libraries. A current work-around is exporting C interfaces from Rust and calling them (wrapped in unsafe code) from the other program. Although this seems to be a problem, it is understandable that the Rust community, backed by Mozilla, does not focus on expanding closed-source code capabilities whilst they advocate for free and open-source software.
One of the mayor drawbacks I faced when developing with Rust, coming from Java, was the availability of IDEs.
Although there exists a multitude of tools and integrations in that direction, the integrations are not as mature as in the java ecosystem yet. There exists a nice plugin for IntelliJ that supports extensive refactoring capabilities for Rust like `extract method` which is greatly helpful when figuring out the types of your functions.
The plugin however, does not support debugging and breaks code completion when there are multiple modules involved. VSCode is supporting debugging but does not have the nice refactoring functionality of IntelliJ. Which one (or both) you want to use, depends on your use-case and need for an integrated debugger. Note that (mainly thanks to the safety of the Rust language) I did not need to use a debugger once for this project.
If you dislike using a heavy IDE, you are free to use the RLS with a plugin from your favorite editor, for example with vim, if you can tolerate a bit of tinkering. For debugging you can user GDB or LLDB directly if you feel limited by the IDE support here.
I expect to see further improvements in the near future in this area, since Rust experiences a growing adaption in various industry sectors. A prominent example is dropbox, they use rust in parts of their file-storage solution. An other success-story is npm, using rust for speeding up their registry.
Now, after writing the prototype, is Rust ready for being used as a programming language for native GUI applications?
Well, basically yes, but it is not always fun to figure out how things are supposed to work. Here it is worth noting, that the Rust community is extremely helpful and nice, you can join some IRC channel and ask even the most basic questions and there will be someone happy to help you out.
Perhaps the state of GUI development will improve when crates like azul mature to a point where they provide feature-rich, stable versions and thereby remove the dependency to native libraries like
gtk-rs requires, greatly simplifying the development process and providing a ‘purer’ Rust development experience. Perhaps have a look at this writeup to get an overview of alternatives to the used gtk-rs and fee free to contribute to some project, if you are interested in this topic.
However, if you already have a desktop application written in Rust, you can easily implement new features in rust and integrate that parts in your existing application, gradually increasing the components written in Rust.
Our endeavor to to explore the readiness of Rust shall not end with GUI development. In a future post, I am going to explore how easily one can create a highly performant web-service written in Rust, profiting from the safety of the language.