Getting Rusty
I'm hearing more and more about Rust. I began hearing about it while finishing engineering school, when my local Robotic engineering club was poking around and putting Rust in its embedded computers on robots (I think it was pretty ambitious at the time, kudos to the team). Now, Rust is propagating everywhere, from CLI tools to web applications, and even the Linux kernel is now getting some Rust. Seeing all the craze about the language, all the "blazing fast" and "reliable" softwares appearing with this language, I wanted to take a look at it and make my own opinion about it. More generally, I see two main reasons to take a look at it :
- I feel that Rust is becoming something big in software development. Taking only a look at its adoption level, the software development community seems to be unanimous about the language (sure, there are some detractors, but that's true for everything).
- as a software engineer and as a hacker, I must keep learning new technologies, to maintain my skills the pleasure I take in working on projects. As of today, C is my daily driver, and except so tooling in Bash or Python, I do not know many other languages, so Rust may be a good target.
So here is how I have dived into Rust !
Disclaimer : the rest of the article only expresses my experience while discovering Rust
Why Rust
One important goal for me when discovering Rust was to convince myself about the advantages of Rust. Sure, you can find many sources which list the features of the language, but I needed "to capture the concepts", to get real examples of why it could be better than my current tools. After experimenting with it, I am now convinced about its benefits, and here are the main advantages I see today :
- memory management : Rust has no garbage collector. Instead, it relies on monitoring data ownership and scopes to check if data is still in use. Moreover, it prevents undesired "side effects" when sharing data amongst multiple modules, by establishing "borrow" rules and lifetimes. I will not lie, I found it quite difficult at the beginning and I am still learning to properly work with those rules, but I now see the avoided burdens when executing similar tasks in C.
- error management : tired of dealing manually with errors returned by APIs, nullable values, pointers initialized or not ? Rust comes with built-in data structures and mecanisms for all of that. (see Result<T>, Option<T>, pattern matching, etc).
- polymorphism : while POO developers seem to be sometimes disappointed by Rust, as a C developper I find it quite satisfying. Rust is not object oriented if you follow the traditional definition. However, it provides basic polymorphism genericity through Traits, which is an interface mecanism, and Generics
- tooling : getting started with Rust has been made pretty straightforward. You install rustup, which allows you to install very simply your compiler for desired target. It comes with cargo, the swiss army knife of Rust development. Many other tools come around, like clippy, rust-analyzer or rustfmt.
- compiler : one big change from C is that rustc compiler detects many design errors right at build time (the memory management point above largely benefits from it), instead of runtime. It may comes at cost of larger build time, but the gain is surely worth it. Moreover, rustc (the compiler) is generally very explicit about errors causes, and even educative about it.
- crates : Rust libraries, or "crates", are shared on a central registry called crates.io. cargo allows to pull and track all your dependencies with crates.io
- documentation : Rust ecosystem seems to have established a high standard on documentation. I feel that many crates still lack of example code, but generally speaking the documentation is pretty good, especially for official general documentation (example : Rust Book)
- testing included in ecosystem : Rust has been designed with TDD in back of the mind. You can execute your red-green-refactor cycle with basic tools such as cargo, without setting up external dependencies
Different flavors of Rust
Learning Rust is a thing, applying it on a concrete project is another. As a consequence, and because of my embedded engineer background, I have felt quite overwhelmed by the quantity of topics to learn to be able to use Rust for projects similar to professional projects I work on. In retrospect, I feel that, for embedded application, Rust learning process can be divided into small steps. Taking shortcuts and trying to reach the final target looks like a mistake on the long run : you can miss some core concepts or mix some elements without really knowing what you are doing. Based on my experience, I would advise the following schedule :
Learn "standard" Rust
By standard, I mean learning by developing an application or a tool meant to be run on standard computer, relying on Rust standard library (std). For this purpose, Rust Book is the perfect entry point. It takes time to explain all the core principles of Rust language
Learn embedded Rust for Linux
Now that you now how to develop basic applications with Rust, time to try to cross-compile an application for a Single Board Computer (SBC) like a Raspberry Pi. That is not a big step compared to the previous one, but it involves project and toolchain configuration, as well as some tooling to keep the development process smooth. Moreover, it allows to start taking a look at available crates on crates.io to start dealing with some hardware.
Learn embedded Rust for bare-metal
Time to dive in what I would call the "hard side of Rust" : applications running with the notorious "#![no_std]" feature. Since the beginning of the learning process, applications have relied on std, which in turn is based on features provided by underlying operating system. As a consequence, bare-metal applications can not rely on std. Instead, we use the "core" library. Such applications need to be build with a specific layout to be able to boot : you have to define a memory layout, an entry point, define the default panic behaviour... and so on. Moreover, dynamic allocation is by default absent (because "core" does not provide heap allocation), so you do not have access to cool features like all types implementing the `Clone` traits, the smart pointers, etc. You either need to work with variables allocated on stack only, or to declare/use custom allocators. This is this step which made me think that it is important to properly separate the learning steps in Rust, to properly understand what features are "general" in Rust, and what are the specificities linked to no_std applications.
Learn to sprinkle Rust in existing codebase
Finally, time to bear with one important constraint of (embedded) development in current industry context, especially when you try to use a young technology. Most of the time, you will rely on SDKs (Software Development Kit) and/or libraries distributed by a industrial partner, specific to the processor or a hardware/software component you are using. You will not be able to develop your application without this provided software, because it provides core functionalities to your application : connectivity, video, or even board support package... Let's face it : such provided software will not be in Rust. It will mostly be provided in C, as it has been for many years in embedded development. Maybe some Rust alternatives will exist for largely used processors/components, but it is probably not the best solution to use if you want to keep support from your provider. Fortunately, Rust provides a mechanism which allows to call C in Rust, named Foreign Function Interface (FFI). While it makes it possible to use C libraries in Rust, it requires quite a big amount of work to make it possible. Rust provides tools to ease the process, like bindgen, but there is a big part of manual work to make the C code comply with Rust mechanisms. Indeed, since C code does not allow Rust compiler to enfore all its checks at build time, you can not call directly C code from Rust. Instead, you will have to start flirting with unsafe Rust, which is a Rust language inside Rust language, to proceed : you will have to develop "safe" wrapper around your C code, in which you will ensure yourself (instead of letting the compiler do it) that all calls and data processing are done safely. My feel here is the following : when finally reaching this point, pros and cons of Rust must be properly evaluated. If it becomes too cumbersome to develop wrappers around C libraries (because said libraries are way to big, or too numerous), is it time to reconsider the hybrid C/Rust approach ? Would it be better to go full Rust, and if so, are we able to redevelop the full embedded stack ? If so, at what cost ? If it is not possible or acceptable, is it still a good idea to force Rust introduction in this project ?
My learning path
Now that I have introduced my ideal learning steps for Rust, how did I really proceed to learn Rust ? Here are the experiments I have lead to initiate and increase my Rust knowledge.
- first of all, I have read the Rust Book. This is the de facto introduction to Rust, and is pretty well written. I would advise to take time to properly configure an IDE with all tooling (on my side, a VSCode configuration with rust-analyser, clippy, and basic cargo tasks)
- before jumping into a concrete project, I felt that I needed to play on properly bounded exercises to make sure I have understood the core concepts. This is where I have found Exercism. It is a programming community aiming to support software development learning in many languages through real mentoring. You can select exercises sorted by languages and difficulty, submit a solution for a specific exercise, and ask for mentoring for a real person to review your solution. All mentors are volunteers, and you can even be a student in a language you are discovering, while being a mentor in your predilection language.
- Then I found another resources to apply my fresh Rust knowledge : the Advent of Code. During December, the author of this website post a challenge each day, which is mostly a question expected a specific numeric answer. If you are stubborn enough and have a lot of time to loose, you can take a pencil and a sheet of paper, and try to find the solution manually, but the problems are designed to be almost impossible to solve by hand : you need to develop a program to find the solution. I loved three things about this challenge :
- you are given an example of input and expected output: the goal is obviously to make you work in TDD style. You know your code is valid only when you have some tests proving that it can compute the correct output for provided example inputs.
- the riddle is personalized for each used : the input is generated for each user, and is way bigger than the example input. So you can not simply copy another challenger answer, because you do not have the same input
- each day's challenge is in two parts, the second part being hidden while you are still solving the first part, and it can be a real pain if you did take shortcuts in your first part implementation (e.g. : brute-forcing a guess), so it involves design decisions and refactoring skills.
The challenge becomes increasingly difficult as we approach the final day (Decembre 25th). I have been able to reach day 18 before giving up. Those challenges were an awesome way of experimenting TDD in Rust !
My Advent Of Code 2021 Progression. All in Rust !
- Next step has been to develop/port some real tooling in Rust. From this intention is born redge. redge is the Rust port of edge200_exporter, a python tool which allows me to upload my cycling activity from a non-connected GPS to Strava, through its public API. This project allowed me to explore some famous crates to realize common operations : manage files, manage command line parameters, call a REST API, etc. It has been a delight to find and use the appropriate crates and use it with Rust standard patterns !
- Time to get my hands dirty. Next step has been to try some code onto bare-metal. That is the part where I mixed the few last steps of the previous section of this article, so I have been struggling a bit (okay, a lot). The target was the following : to deploy some Rust code on an almost bare-metal target (it was using an RTOS), while keeping most of the C code. I quite managed to do something, but did not reach far and used most of my time trying to wire C code into Rust code and vice versa. When I realized the mistake, I took some steps back : I pulled out a STM32F4 discovery board gathering dust in a cabinet, and experimented with pure Rust, bare-metal application, playing with peripheral crates, embedded-hal and board crate.
Yes, the good old blinky
Conclusions and next steps
After a few months of play with Rust, I am quite (very) happy with Rust in general. Coming from C, the learning curve can be quite steep at the beginning, but there's a sentiment of power once you start to find and apply development patterns : you create and implement structures that make sense, learn to pass data while keeping the ownership and borrowing rule in mind, propagate errors properly between your layers... However, I find it still difficult to use it in bare-metal projects, especially while thinking about industrial challenges : Rust is still pretty young, so it will not make C disappear in the next years. If we have to deal with manufacturer stacks to make some hardware work, we will have to deal with C and Rust cohabitation, which seems to be hard, maybe to the point of excluding Rust. As a consequence, I feel that the pros and cons must be evaluated carefully before starting an embedded project in Rust, but of course, this is only my opinion after having only poked around a bit with it ;) I guess that the mere existence of dedicated companies like Ferrous Systems proves I still need to explore Rust in embedded to get its true potential.
I want to pursue Rust learning. I am trying to define a cool project where Rust would fit. I think I will take a look at how Rust has been introduced in Linux kernel, and maybe boot a Raspberry Pi with a Rust-capable kernel.