Five years of Rust - a cross-platform programming language for the next decade(s)
Last month, the Rust programming language celebrated its 5th anniversary. I’ve joined this journey roughly halfway through, and since then I had the opportunity to work on various Rust projects, but also to witness many improvements in the language, as well as in Rust libraries. Given what’s already possible to do in Rust today and this trajectory of improvements, I think that Rust will continue to grow into a cross-platform and full-stack programming language of choice for the next decade (at least).
Therefore, to celebrate these five years, I’ve decided to write a series of blog posts explaining why I think Rust is a language of choice, and where to find relevant resources to use Rust across platforms and along the software stack. In this first post of the series, after a brief summary of what is Rust and why it’s relevant, I’ll go through the various platforms where Rust can already be used. And Rust already supports so many platforms that I’m sure I’ll forget some!
Don’t forget to read the second post, where I’ll focus on the full-stack aspect of Rust.
What is Rust?
In this section, I give a few insights about Rust if you’re not familiar with it, or want to refresh your memory. Otherwise, feel free to skip to the next section.
A very brief history
When we say that Rust just celebrated 5 years, we mean 5 years of stable Rust. However, looking back at the archives, the project started much earlier than that - it takes time for software to evolve into something stable!
The first release tag on the GitHub repository is more than 8 years old (release-0.1). And the first git commit on this repository actually celebrates 10 years today (I was not even at university!). Before that, there is even a Rust prehistory repository, which dates back to 2006. Back then, I had never programmed in my life!
It’s also notable that within these 5 years, two editions of Rust have been released: edition 2015 (Rust 1.0) and edition 2018 (Rust 1.31). Although the new edition was not a huge breaking change – one can use 2015 and 2018 packages together in a Rust application – it shows an interesting velocity in the development of Rust.
My 2.5 years of Rust experience are not a lot compared to all of that history. But if you’re interested in learning more, the Five Years of Rust blog post gives a good summary of the stable history.
Why Rust? Performance, reliability, productivity: choose three
Rust positions itself as an effective programming language targeting all of performance, reliability and productivity.
Performance means that Rust programs should be able to run without overhead, both in terms of pure speed or memory usage. As such, Rust is first of all compiled, as opposed to interpreted. Indeed, interpreted languages like JavaScript or Python necessarily incur a performance overhead, even with the best-in-class JIT design, for example due to dynamic typing, which prevents some compiler optimizations. Second, Rust uses manual memory management, as opposed to a garbage collector. Even though garbage collectors can be designed with a low speed overhead, this requires a lot more memory – think 5x more memory1.
The downside of compiled languages with manual memory management is that, traditionally, the likes of C and C++ have been the source of security vulnerabilities, because manual memory management is hard. Indeed, about 70% of serious vulnerabilities in software like Chromium, Firefox or Microsoft products are memory safety problems.
And this is where Rust comes into place: thanks to a stronger model of memory ownership and borrowing, baked into the type system, Rust allows you to write reliable programs even with manual memory management. This is the fundamental novelty of Rust, but as we’ll see in this series of posts, Rust has many more advantages than that.
On the flip side, there is a slight developer overhead in understanding how the ownership and borrowing rules work, but Rust has improved a lot on that front. More importantly, if you care about performance there will always be some developer overhead: either you use a fully managed language like JavaScript but you will scratch your head when your application gets slow, or you use something a bit more efficient like Java or Go but you can still end up fighting the garbage collector if you’re constrained by RAM, or you use C/C++ which will be efficient but will fire back later when security vulnerabilities are discovered in your software. Or you spend some time learning Rust, but later don’t have to worry much about neither performance nor reliability.
The third pillar of Rust is productivity. This comes by giving access to both low-level primitives – as we’ve seen with efficient memory management – and high-level abstractions – made possible thanks to a strong type system. This ability to span the whole abstraction spectrum makes software development in Rust productive. And the good thing is if you consider writing code and software performance as a whole, being more productive in writing reliable code actually increases software performance.
An insightful example is the following, which compares the productivity of C and Rust – two languages that shouldn’t compromise on performance.
Bryan Cantrill explains in this talk that a program written in Rust without even applying all possible optimizations was 35% faster than an equivalent well-optimized C program.
The reason was that Rust implements sorted maps as a B-tree under a nice abstraction.
While B-tree maps are quite efficient, it’s much harder to write a generic B-tree library in C than in Rust due to the type system, so C programs often end up with less optimized structures like red-black trees.
Another example is the HashMap
structure of Rust, which now implements a port of Google’s Swiss Tables, and is therefore more efficient than std::unordered_map
in C++.
Quite insightful!
Who uses Rust?
According to the 2020 StackOverflow Developer Survey, Rust is used by approximately 5% of respondents, and is the most loved programming language, with 86% of Rust users (among the respondents) willing to continue programming in Rust. There can certainly be a bit of bias in here, as Rust is a new language with a small developer base, so the developers who use it are more likely to be casually curious – programming in Rust in their free time rather than a professional context. They could also work on relatively new code – without all the technical debt accumulated by older code that could generate more frustration in general.
But an interesting insight is that Rust has held this “most loved” language title for many years in StackOverflow’s survey.
And the satisfaction has grown from 83% to 86% in a year, while the developer base grew from 3% to 5% of respondents.
So it’s quite possible that the recent improvements of Rust – in particular the async
/await
feature – have brought more satisfaction!
A truly cross-platform language
The first aspect that makes Rust a programming language of choice for a wide range of use cases is that it works on many platforms. The list of supported platforms is just huge!
An important aspect to keep in mind is that because Rust is a compiled language, the compiler has to output a slightly different binary for each of these platforms.
The produced binaries indeed have to take into account things like the target CPU and the available libraries in the target OS.
And I still find it amazing that all of these platforms are supported by a single compiler.
If you want to compile code for a Cortex-M4 CPU, all you have to do is adding this target platform with rustup target add thumbv7em-none-eabi
and you’re good to go!
Given this long list of platforms, I’ll now try to categorize them in a (non-exhaustive!) list to summarize the use cases.
Desktop systems
The three most common systems for desktops (including laptops and servers) are in the tier 1 of supported platforms. If you have a Windows, Linux or OSX system released in the last 10 years, Rust is “guaranteed to work” on it, thanks to exhaustive automated testing.
Besides these fully supported systems, you will find many more systems available in tier 2 and tier 3. For example, you can still write Rust programs for Windows XP (tier 3). And beyond these tiers, someone even managed to compile a Rust program for Windows 98.
Mobile applications
Although the Rust compiler itself doesn’t ship on mobile, you can cross-compile Rust code to use in Android and iOS applications. The integration model is essentially to build a native library written in Rust (rather than in C/C++), and invoke it from the main Android/iOS application.
- On Android, you can get started with this tutorial, and use the JNI crate to interact with Java from Rust code.
- On iOS, there is a similar tutorial (note that I haven’t tested it).
Web front-end with WebAssembly
WebAssembly, also known as Wasm, it is a growing alternative to JavaScript, notably on the web front-end, as it is supported by all the major browsers. Wasm defines a simple stack-based virtual machine, which can be run potentially more efficiently by browsers than JavaScript, due to avoiding all the scripting and text-based language parts that add complexity to JavaScript. And most importantly, it allows one to develop programs in languages like C++ or Rust, and then compile them down to Wasm. By writing a Rust program targeting Wasm, your web front-end can benefit from Rust’s already rich library ecosystem. I haven’t looked at it in detail yet, but there is an online book to get started with WebAssembly in Rust.
Embedded systems
On the opposite end of the software spectrum are embedded systems. They constitute the lowest level of software that interacts with hardware components, which includes things like firmware, operating system kernels or bootloaders. There are multiple resources to get started with Rust in embedded systems.
- The Rust embedded working group, which coordinates various efforts.
- Real Time For the Masses (documentation), to build a simple “bare metal” application on an embedded system.
- Tock (source code), an embedded operating system which allows to run multiple mutually distrustful applications concurrently. Disclaimer: I’ve contributed to it.
An interesting aspect of Rust is that even though a rich standard library is available in the language, it is not supported by all target platforms, and in particular not on embedded platforms.
Instead, one can compile Rust code in no_std
mode, where only some core libraries are available.
Generally speaking, no_std
means that you don’t get access to operations that need a supporting OS, such as reading/writing files or even printing to the terminal output, because there is simply no supporting OS at the “bare metal” level of software.
With no_std
, you can simply do some “basic” operations such as manipulating memory, performing arithmetic, and of course having control flow.
You can also use some specific CPU instructions via inline assembly (with llvm_asm!
or the newer asm!
).
It turns out that these “basic” operations are actually enough to start programming on an embedded system!
In short, the no_std
mode allows Rust to be used on embedded systems.
Compare this to languages like Java or Python, for which you need to ship a virtual machine and/or a runtime on a platform before you can run programs on it.
For example, concepts like java.io.File
simply don’t exist without a supporting OS to provide a file system.
Bindings with other programming languages
Some of the target systems I mentioned so far are made possible by the ability of Rust to interface with other programming languages. For example, Android applications are traditionally written in Java, so your Rust code needs to interface with Java in order to integrate it in an Android application.
The first language to mention is C, with which Rust can interface by means of the so-called foreign function interface (a.k.a. FFI).
You can find more documentation in the Rustonomicon, but essentially you can compile a Rust library and expose it as if it was a C library.
And vice-versa, inside of your Rust code you can call functions from a C library.
This is done via simple annotations such as extern "C"
or #[no_mangle]
, as well as support libraries such as the std::ffi
module and the libc crate.
Once Rust can interface with C, it can interface with any programming language that has an interface with C, which includes many languages. Here are some examples.
- Java. The JNI crate, mentioned above for Android, allows to interact with Java programs, via the Java Native Interface (originally written to interface with C).
- Python. The PyO3 crate provides binding with the Python interpreter.
- C++.
It’s generally complex to map all of the concepts of the C++ language to Rust. Besides bindgen and cbindgen which mostly interact with the C subset of C++, we can mention the cxx crate, which tries to map more advanced C++ classes such as
std::string
orstd::vector
directly to Rust. I also recommend to watch An unholy fusion of Rust and C++ in physx-rs, a talk going through how to glue a large C++ SDK into a Rust crate. Another tool that was just released is thesafer_ffi
project. - Other languages. There are many more bindings available, such as with the V8 JavaScript interpreter, OCaml, Ruby, R, etc.
Other targets
The breadth of targets Rust can be used for seems to be limitless, and while doing more research for this blog post, I found the following interesting targets (disclaimer: I haven’t tested them).
- Writing BPF code in Rust. BPF is a virtual machine running inside of the Linux kernel and which allows to run user-defined programs to monitor events in the kernel. It’s useful for applications such as monitoring performance, implementing load balancing, mitigating DDoS, etc. And you can now write such programs in Rust!
- General Purpose GPU computing (GPGPU). As an example, the accel crate provides a GPGPU computing platform based on CUDA. With it, you can write programs in Rust and run them on a GPU that supports CUDA.
Remarks on cross compilation
To conclude this – already quite long – section about platforms supported by Rust, I want to mention a few elements that make Rust shine as a cross-platform language.
First, Rust hides a lot of platform-specific elements behind meaningful abstractions in the standard library.
For example, the u32::trailing_zeros
function, which counts the number of zero bits at the end of a 32-bit integer, looks quite specific.
One could implement that as a loop to check and count the bits individually, but it turns out that most CPUs have a dedicated instruction to perform this operation faster than the “naive” loop.
So Rust makes it available behind this trailing_zeros
abstraction, which itself dispatches to either a CPU-specific instruction, or to more instructions if not directly supported.
In C/C++, you would have to support that yourself, and either have plenty of custom #ifdef
in your code to check for various compilers and target systems, or just give up and implement the slow naive loop.
As another example, the std::fs::Metadata
type represents filesystem metadata concepts that exist on all of the supported operating systems, such as the creation/modification time, or whether the path represents a file or a directory.
Using it in your code is totally transparent, regardless of the target (Windows, Linux or OSX).
Then, you have access to OS-specific elements in std::os::unix::fs::MetadataExt
, std::os::linux::fs::MetadataExt
or std::os::windows::fs::MetadataExt
.
So Rust seamlessly supports platform-specific as well as cross-platform APIs.
The fact that there is a single Rust compiler is actually a good thing here: you don’t have to check for the specifics of multiple compilers as you would do in C++ to use non-standard features of some compilers, or as you would do in JavaScript to support older or exotic web browsers.
Last, if you want to learn more about cross-compilation in Rust, I recommend you to read Everything you need to know about cross compiling Rust programs by Jorge Aparicio.
Conclusion
I hope that this blog post convinced you that Rust is a programming language of choice if you’re targeting multiple platforms, whether on desktop, mobile, web, embedded systems, or even more exotic “platforms” like writing BPF code for the Linux kernel.
In my next blog post, we’ll discover why Rust is suitable for many application domains: from CLI to GUI, the Web, science, and many more!
This post was edited to take into account feedback on reddit, in particular in the “Why Rust?” section.
-
See Quantifying the Performance of Garbage Collection vs. Explicit Memory Management, paper published at OOPSLA’05 by Matthew Hertz and Emery D. Berger. ↩
Comments
To react to this blog post please check the Reddit thread and the Twitter thread.
You may also like