My journey with Rust in 2017 — The good, the bad, the weird

Dieter Kaestner
codeburst
Published in
13 min readJan 4, 2018

--

Note: This post is not only about Rust, but rather my experiences, problems and solutions throughout 2017.

My problems

In February 2017 I registered my own company. It was a really weird step not to be told what to do anymore and having to survive through your own merits. I felt unsafe, inexperienced and not sure what to do. I had a business plan, yes, but I had never written programs full time before, only side projects in C++. So I just started doing what I could do best — writing C++ at full speed ahead. But the more I wrote, the more I understood that I did not know why I was doing certain things and just did them because I learned that if you see Error A, do thing B and hope that it’ll make the compiler complain less. I needed someone or something to guide me, to tell me why things are wrong — not just ad-hoc solutions.

Another problem I noted was that it seemed impossible to me to integrate any libraries from other people — a common solution are header-only libraries. Or you just downloaded a .dll file, hoped that it was the compiled for the exact same compiler you are using, hoped that it didn’t have any dependencies. Easy updating of libraries? Dependency management? Reproducible builds across machines? Forget it. Eventually, I moved to Linux, hoping that the dependency story would be better — it is, unless you need a library that doesn’t have a package, that’s when it becomes hell. Yes, I probably could have learned CMake and Make. But if a dependency doesn’t even build, that won’t help you much.

I was very concerned about performance, which is why I chose C++ in the first place. You see, at my old job, we had to use a cartography program to do semi-automatic label-placement. It was completely single-threaded and slow as hell, (not only due to very frequent disk access). The program was extremely slow. I should mention that the application was taking heavy advantage of Win32 functions (we had to run it in a VM). Ex. it used Windows’ PDF driver to export a PDF (which could take up to half an hour). I don’t know if it was due to Windows or the application programmers, but I attributed Windows’ built-in functions with being slow and having shitty PDF output, which is why I refused to use anything related to Win32 or .NET.

So in short, what were the main problems I had, which Rust excelled at?

  • I feared that garbage-collected languages were not fast enough (given the previous experience). I wanted bare-metal performance (like C or C++).
  • Dependency management in C++ was — and still is — hell. I am not alone with this opinion.
  • Error messages in C++ are cryptic. Yes, I know about C++20 concepts. Get back to me in 2025 when they are finally implemented.

There are other reasons, but I think you can see why I was frustrated with C++. So I quit the old company and C++ is awful, now what?

The good

I came across Rust by reading a random comment. In general, I have three steps towards evaluating any new technology / library / framework / language:

  • Do I need it? What does it do better than the current thing I’m using? Are there tradeoffs to be aware about?
  • Can I replicate the “Hello World” example? How hard is it to set up? Is the build easily reproducible? Does it integrate with what I already have or do I have to jump through hoops?
  • Can I “discover” the API? All parts of an API are important, but some are more relevant than others (to a beginner). Many libraries / technologies fail at that.

Rust did not fail any of these tests. But there are tradeoffs. I needed a PDF library in order to produce maps. Rust didn’t have one. I first tried integrating libharu and podofo but couldn’t generate the necessary bindings. On the other hand, I had time, lots of time and I wanted to make it perfect. There was no PDF library I knew of that supported PDF layers, which are incredibly important for maps.

Here we get to the first point why Rust has an awesome ecosystem: Building blocks. There was a library called lopdf which provides the basic blocks to serialize and deserialize PDF objects. That’s it, it doesn’t do anything else. And this is awseome, because I could build on top of that. I didn’t have to write my own serializer. Composition over reinventing the wheel, as I would say. Since you don’t have to do much work to track dependencies, it does make things very easy to compose and split libraries, enabling you to pick-and-choose what you need.

So two months later, I got done making my library called printpdf, which had the features I wanted. Now what? I still didn’t have any maps. Looking back, I then took the completely wrong approach toward software management - I tried to design everything upfront, with UML and cute diagrams. Sadly, I lost the diagram, but looking back I was extremely inexperienced. If you are working in a group, this might be something to show off to your manager, but in practice it simply doesn’t work out unless you already have extensive domain knowledge.

Use the technologies you already know when starting a (commercial) project.

This was a quote from a presentation at GDC 2017. If you want to finish a project, you need knowledge about the domain and tools you are working with. I needed an editor to edit my maps.

Now you might say, hold on, why don’t you use ArcGIS or QGIS or any of that? Why are you trying to reinvent the wheel? ArcGIS and QGIS (two popular GIS desktop applications) are full of features. But they are not well suited for cartography, which is a shame. QGIS PDF output is horrible to work with in Illustrator. Every time I tried importing the file, the line thickness would be different than what I expected. QGIS options for semi-automatic label placement are limited. Either you go full automatic, in which case you just use what the algorithm spits out. Or you need to place the labels manually. QGIS has no understanding of automating map styling (i.e. sharing settings across maps, map templates, automated map creation). I also needed version-control for maps. Maps need to be updated from time to time and you don’t want to redo the whole map. You need to view changes between maps, changes to the map layout. Plus I’d still have to write all the other tools for street indexes, heightmap rendering, labeling for contours, etc. It took QGIS 4 years to implement a simple feature where the line thickness gets scaled according to your zoom scale (like if you zoom in on a printed map). Which is, you know, kind of essential for correct label placement. In short, I wasn’t convinced that QGIS would get me to my goal. In the end, nobody cares how you make your map, only the end result counts.

So let’s get make an graphical editor in Rust then, shall we? Or, in other words, let’s get to

The bad

GUI libraries in Rust. Well, where do I start? There are none. Well, that’s not quite true:

  • limn is currently the most promising pure-Rust library I’ve seen. What it does is that it takes Firefox’s rendering engine webrender (only the rendering, no JS or layout) and uses an Android-like layout algorithm (cassowary) for layouting. It is however, very early in development. The layout still has bugs.
  • conrod is heavily focused on games. It uses a directed graph to determine if elements need to be re-styled and how they are positioned in relation to each other. I tried it and it was a pain to develop for. Every widget needs an ID to be represented in the graph. At the time I tried it (a year ago), doing this with dynamic insertion and removal (generating dynamic IDs at runtime) needed a heap of boilerplate code to get working. It was also under rapid development, so I wasn’t sure about the longevity of my UI.
  • GTK and QT bindings. The reason I did not choose these was rather simple: In order to draw a map efficiently I need OpenGL. Neither GTK nor QT (at least the Rust bindings) have good OpenGL integration.

Short, I needed OpenGL for efficient, fast drawing, there was no way around it. So let’s build a GUI framework from scratch then, shall we? Here’s your recipe:

  • 1 OpenGL shader that can translate from pixels to screen-space coordinates
  • 1 OpenGL shader that does the same, but for textures
  • 2½ layout frameworks to determine the coordinates of your squares to render on the screen (more on that later)
  • 500g AABB tests for hit testing + 1 windowing framework
  • 5 teaspoons of function pointers and one mutable reference to the application state
  • A pinch of insanity

I had no experience in building GUI frameworks, so what do you do? Build a prototype to gain domain knowledge. I refactored and refactored my GUI again and again. I made a simple 2D game for the recent Ludum Dare to test my GUI concepts. What I found was an architecture that is neither retained nor immediate.

Both retained mode and immediate mode don’t work very well with Rust’s borrowing model, the reason being that UIs are in essence two-directional (output / input) while Rust’s borrowing model is optimal for one direction (either output or input). Rust requires you to do output (where you usually have an immutable reference to the GUI somehow) and input (where you need to have callbacks that can change your GUI and application state). So you can create callbacks that can change the whole application, but then you run into borrowing issues. You can solve them by spamming RefCell everywhere or you could try and adjust the model.

A lot of the problems came from the way I thought about GUIs. A button is an object, right? Wrong. Start to think of your GUI as a big, big function. Because that’s what it is, in the end, a rendering loop. Each button or widget or thing is simply a sub-function that describes how it is laid out. A UI is (in my model) an “immutable view into your applications state”. What I ended up with was something like this diagram:

I call this design “functional UI programming”. This model assumes that the UI is redrawn at 60fps, but there are ways to use it in a retained way. UI state handling is not an easy task. For simplicity I have left out a lot of opportunities for caching. For example, if there are no relevant InputEvents for a frame, you can go straight to the render function. The goal is to minimize the amount of state that one function has to touch. The layout function doesn’t have to know about the whole application state. The input filtering doesn’t know about the application state. The UIState is in some way a duplicated subset of the AppState, in order to limit what other functions can touch. Events can run in an asynchronous way (if you want) because the UI gets automatically updated every frame if you modify the ApplicationState.

The difference to immediate-mode GUIs is that immediate-mode libraries do input handling, state mutation and rendering all in one step per-element. This doesn’t work well with batching (for reducing draw calls). Immediate-mode GUIs also lock you down to use one rendering backend (since a function to draw a button is has to know how to draw itself), functional GUIs allow for multiple backends — the rendering function doesn’t have to be in the same library as the function returning the UIDescription.

Update (November 2018): This UI framework is now open-sourced at https://azul.rs/.

The weird

With that problem out of the way, I finally had my application. It does exactly what I want, nothing less and nothing more. Now I wanted to talk about the dark sides of Rust and critique the language itself — not things that are particularly relevant to me, just things that I noted along the way — they might endanger your project, should you choose to write it in Rust:

Non-cursor based sets / maps

Rusts BTreeMap<T> and BTreeSet<T> do not support cursors / markers (in C++ called “iterators”) like C++` std::list does. For example, if you insert into a std::list — in C++ you can get an iterator back to the position where the element was inserted. You can for example, insert and immediately compare the iterator to the next / previous element (where you inserted). This is not possible in the Rust standard library (there is a library called intrusive_collections which allows this, though.

Poor std::collections

std::collections is missing some elements. There is no multimap, for example, which is a shame (it exists in an external crate, but come on). Yes, HashMap, BTreeMap, BTreeSet and Vec are mostly sufficient for 90% of my datastructure uses, but having to import crates that have loads of dependencies from random authors is sometimes tedious.

No rand() in the standard library + unnecessary cryptography

This was a deliberate decision, but I personally do not like it, mainly because the rand library is cryptographically accurate and therefore slow. Same with the std::HashMap which (by default) randomizes the order to prevent against attacks. Now, it does allow for alternative hash functions and you can use non-cryptographic rand libraries, but you always have to keep in mind that the standard functions are slow by default.

cargo does not allow compiling to WASM and into a regular library

I tried compiling a math-heavy library that I use for regular development to WASM. Turns out, you can only set the library once, globally, not dependent on the target architecture. So a library can be either for WASM or a regular rust library, but not both. This is an issue for cargo now, expect it to be fixed in the future.

Abstractions are not always zero-cost

This is on library authors, not on the language itself. Sometimes library authors abstract libraries so much that they land in another astral plane. One example was winit — a framework to create cross-platform windows, like SDL. Every window framework ever allows you to query the coordinates of the mouse cursor (like 200 px left, 400 pixel top). Plus, windows usually have a “message loop” in which you only get a “mouse moved” event when it’s absolutely necessary. winit did this too, until a certain version (0.5.11 I believe). It had two functions for this: wait_events (blocks until new events come it) andpoll_events (immediately returns, but there might not be any events to process). All was fine and well.

But then the authors threw the functions out and replaced them by functions that return the change in pixels. What change, you ask? The change in pixels from the last position where winit queried the cursor. winit suddenly returned fractional values (i.e. “mouse cursor moved by 0.2 pixels”) — which is physically impossible. But winit will even send you events like “mouse cursor moved by 0.0 pixels”. Meaning, there is no blocking anymore — if you don’t use std::thread::sleep, your CPU will burn at 100% because of your window framework. Everything got worse. Now I have to check if the difference is 0.0 to check if the mouse hasn’t moved. Which is just ridiculous.

The reason for this change was a problem in how resizing / repainting on MacOS works. So because of MacOS I now have 100% CPU usage, great. I had to do a lot of backports of fixes to older versions of winit because of stupid things like this. 5 versions later it’s still not fixed and probably never will be. Sometimes I don’t understand people.

Rust doesn’t have a math library like libm

One of the things to watch out for should you want to do embedded programming or WebAssembly (where size is important). Want to calculate an arctangent without the standard library? No dice. There is a library called “m”, but it is lacking a lot of features and development has slowed down to a halt. You can use rust intrinsics, which limits you to the nightly or beta compiler, but not all functions are implemented using intrinsics plus you are basically making your own math library right now, so what’s the point? Rust still links into C for mathematical functions, which is … not good.

rustc is slow

One of the reasons I’ve heard is that rustc can’t pass mutability information to LLVM because of an LLVM bug. So LLVM goes ahead an does its own analysis all over again. This should be fixed when rustc is upgraded from LLVM4 to LLVM5 (already fixed upstream) but I don’t get (if this is true) why this isn’t the #1 priority. I mean, cargo check makes development cycles managable, but if you need to actually check if something is working and need to compile your stuff, it’s still pretty slow.

Link-time optimization is pretty much impossible after a certain amount of lines, it just takes too long. ThinLTO still segfaults for me, so there’s that. On the other hand you could argue that bad compile times are a sign of too bloated libraries — split your code into smaller libraries and it will compile faster.

Some tricks

To end this (overly long) article on a happy note, I wanted to include some tricks I’ve learned.

Varying floating point accuracy with “fsize”

This enables users of a library to either use double-precision or single-precision with a features = [“use_double_precision"] flag. This is like typedef in C.

The public-in-private hack

Warning: this can be misused and a source of frustration.

Here you see that we return a public type, but this type is in a private module. This means that PublicStruct never shows up in the documentation, nor can a user instantiate it. As a return type, it is fairly harmless, but as a function argument it can make public functions impossible to call (from other libraries) because a user outside of your library cannot construct a PublicStruct.

The marker enum

This enum cannot be instantiated. It is not really a thing and yet it is a type. This can be used in markers (things that “mark” a concrete type for a generic one), for example this is useful when working with PhantomData types.

Conclusion

Rust is (for me) definitely a step up from C++, but it still has flaws. Libraries are young and for easier things there seem to be hundreds of solutions (*ahem* web frameworks *ahem*) while for more complex topics there may be none or only unstable ones (things like GUIs, async I/O, coroutines, compile-time functions). For my purposes, I think it was the right language. Yes, I could have probably done it in $LANGUAGE, too. But through practice I’ve gained a better understanding of how things work under the hood and I don’t fear cryptic compiler messages anymore. In a way, Rust let’s me fear less (pun intended).

Happy New Year!

--

--