Choosing Rust

Rust is a multi-paradigm, strongly-typed, general-purpose programming language designed for performance, safety, in particular memory safety and safe concurrency without the need for a garbage collector or a runtime.

It incorporates features found in high-level languages (especially functional ones from the ML family) in a low-level package that allows precise control over memory and system resources.

Rust logo

Today, in 2021, Rust is one of the fastest growing programming languages, taking areas such as backend development and new crypto development by a storm. Ever since Rust 1.0 was released, it has been voted the most loved programming language on StackOverflow every single year.

In this chapter, we will go over what makes Rust a great choice for development, and, conversely, where it falls behind,

Let's start with a brief history of Rust.

History of Rust

Rust was first created as a pet project of Graydon Hoare in 2006. He decided to name it Rust after a species of fungi and because the string rust is a sub-string of robust.

Rust fungus CC-BY copyright of Pango Rust fungus under the microscope

Originally, the language took a lot after the functional programming language ML, in particular OCaml (an OO dialect of ML), and the first Rust compiler was also implemented in OCaml.

In 2010, Mozilla officially announced the project, and development began on a self-hosting compiler targeting LLVM. Over the course of the next five years, Rust went through many backwards incompatible iterations, and many features that were added were later cut, such as:

  • garbage collection
  • type-states
  • interfaces
  • green threads
  • many pointer types
  • early form of async
  • classes
  • objects

Steve Klabnik, one of the Rust core team members has stated in his History of Rust talk that the early form of Rust with garbage collector resembled many of the features Go has today.

This early similarity might have been the catalyst for future comparisons between Rust and Go, despite each language targeting different domains and having different goals.

In 2015, Rust has finally released 1.0, which is the form of Rust we are familiar with to this day.

Despite the language going through many forms (9 years of development), it's guiding principles and goals have not changed.

Guiding principles

No compromises and rigorous design process

The first and foremost principle that followed Rust's development is taking no compromises. This is most apparent in refusal of traditional programming trade-offs.

Here is a sample of trade-offs that are often true:

  • Usually, having a memory safe language means giving up precise memory control and performance, and a garbage collector is utilized.

  • Employing a number of functional features often leads to a more difficult or convoluted design for programmers

  • Writing highly-concurrent code means opening yourself up to data races, race conditions, and complexity with properly distributing data across threads.

  • Performant, strictly-typed and functionally-leaning languages often lead to increased development times.

None of these are true in Rust (well the last one a little bit). Rust developers have iterated on solutions for these problems for years until they reached a solution that didn't require any compromises.

Today, Rust has adopted a model of open-source governance and new features go through a lengthy and elaborate process of selection, design, implementation and testing before they are even included in nightly as opt-in through language features. For every compiler version, a crater run is performed, which looks for regressions across the whole Rust package registry crates.io and Rust code it can find on Github.

Generally, this means that even nightly is safe for non-critical development, and the stable release channel is a great choice for just about any application.

You can see the upcoming Rust and changes in the Request For Commentary repository on GitHub: https://rust-lang.github.io/rfcs/

Pre-existing academic research over new inventions

Rather than inventing new features, Rust prefers taking existing ideas from the computer science academic circles and implementing them in a way that is user accessible.

For example, many of the memory safety features of Rust were taken from the language Cyclone, a safe dialect of C designed in AT&T Labs Research and Cornell University. As another example, Rust's channels and concurrency features were inspired by Rob Pike's and Phil Winterbottom's series of concurrent programming languages - Newsqueak, Alef, and Limbo - they developed at Bell Labs.

Other influences can be found here.

Selling points

Strong static analysis

Rust is somewhat known for slow compilation speeds, one can expect to wait minutes even for not so big projects when compiling release builds from scratch.

One of the greatest time consumers is Rust's static analysis step. Rust does very precise checking of your code, aggressively optimizing and resolving what it can at compile time.

Here is one of my favorite examples, simple mathematical algorithms:

/// factorial implemented with an iterator
fn factorial_iter(num: usize) -> usize {
    (1..num)
        .fold(1, |acc, x| acc * x)
}

/// factorial implemented with a loop and a mutable variable
fn factorial_loop(num: usize) -> usize {
    let mut sum = 1;

    for x in 2..num {
        sum *= x;
    }

    sum
}

/// fibbonaci implementation with a loop
fn fibbonaci(n: usize) -> usize {
    let mut a = 1;
    let mut b = 1;

    for _ in 1..n {
        let old_a = a;
        a = b;
        b += old_a;
    }

    b
}

fn main() {
    let x = factorial_iter(12);
    let y = factorial_loop(20);
    let fib = fibbonaci(35);

    println!("factorial 1: {}, factorial 2: {}, fibbonaci: {}", x, y, fib);
}

If you compile this in release mode and inspect the generated assembly, you might be surprised to see this:

subq	$120, %rsp
movq	$479001600, (%rsp)               # <- look here,
movabsq	$2432902008176640000, %rax       # <- here,
movq	%rax, 8(%rsp)
movq	$14930352, 16(%rsp)              # <- and here

If you run the code as well, you will find that these numbers are the factorial of 12, factorial of 20, and 35th Fibonacci number, respectively. That's right, Rust figured out it can evaluate all this code at compile-time, so it did, and now this program now runs finishes instantaneously, as no calculation is being done at runtime.

This is a short contrived example, but Rust resolves a lot of code at compile-time. Compile time resolution can be enforced by using the const keyword on functions, but not all language features are yet supported in const contexts. Rust's compile-time analysis features are utilized by many Rust libraries to achieve high performance or ensure correctness.

For example, the Diesel database ORM uses this to validate queries during compilation and achieve performance surpassing the fastest C frameworks by pretty much eliminating all query-generating code by compile-time evaluation. Here's a talk which explains this process on an example query

Zero-cost abstractions

This is one of the most advertised points about Rust. Having zero-cost abstraction means, in layman's terms, that the "flavor" of your code does not affect performance. It doesn't matter what language features you use (ie. loops vs iterators), or how many abstractions you have that make your program make sense to you, help clarity and ensure safety, it still compiles down to the same (or very similar assembly), the abstractions that you create do not appear in the final binary.

Apart from Rust being smart, deleting and rewriting your code by itself, Zero-Sized Types are commonly used to model type states, a kind of zero-cost abstraction.

A great example comes from the Rust Embedded book:

Type states are also an excellent example of Zero Cost Abstractions - the ability to move certain behaviors to compile time execution or analysis. These type states contain no actual data, and are instead used as markers. Since they contain no data, they have no actual representation in memory at runtime:

#![allow(unused)]
fn main() {
use core::mem::size_of;

let _ = size_of::<Enabled>();    // == 0
let _ = size_of::<Input>();      // == 0
let _ = size_of::<PulledHigh>(); // == 0
let _ = size_of::<GpioConfig<Enabled, Input, PulledHigh>>(); // == 0
}

Defining a ZST:

#![allow(unused)]
fn main() {
struct Enabled;
}

Structures defined like this are called Zero Sized Types, as they contain no actual data. Although these types act "real" at compile time - you can copy them, move them, take references to them, etc., however the optimizer will completely strip them away.

In this snippet of code:

#![allow(unused)]
fn main() {
pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
    self.periph.modify(|_r, w| w.input_mode().high_z());
    GpioConfig {
        periph: self.periph,
        enabled: Enabled,
        direction: Input,
        mode: HighZ,
    }
}
}

The GpioConfig we return never exists at runtime. Calling this function will generally boil down to a single assembly instruction - storing a constant register value to a register location. This means that the type state interface we've developed is a zero cost abstraction - it uses no more CPU, RAM, or code space tracking the state of GpioConfig, and renders to the same machine code as a direct register access.

Safe and efficient multi-threaded and asynchronous programming (aka Fearless concurrency)

Rust's goal of memory safety through strict analysis and powerful type-system eventually led Rust language developers to the conclusion that the same tools can be used to help manage concurrency problems (concurrency meaning concurrency and parallelism, Rust community often uses these terms interchangeably).

The tools used to achieve this are traits and standard library types. In Rust, all types might be automatically marked with these traits:

  • Send - if it is safe to send this data to another thread
  • Sync - if it is safe to share this data between threads (a type is Sync only if and only if a reference to it is Send)

Since these traits are empty, they are a zero-cost abstraction as well.

Rust will not allow sharing/sending data between threads for types that are not Send and Sync. If you need to share data which isn't intrinsically safe to share (such as when you need to mutate data from several threads), Rust forces you to use concurrency-enabling structures, such as mutexes, read-write locks, atomically-counted reference pointers, or condvars. Or to just use atomic data structures in general.

No data that is not atomic or wrapped in a safe locking structure can be mutated.

This, in conjunction with borrowing and lifetime rules (that is, the ownership system), prevents data races. Data races are defined as the following behaviors:

  • two or more threads concurrently accessing a location of memory
  • one or more of them is a write
  • one or more of them is unsynchronized

Data races are examples of undefined behavior, and so they are impossible to create in safe Rust.

However, it is important to keep in mind that Rust does not prevent general race conditions, which are situations that produce different results depending on the order in which operations are executed. As stated in the nomicon:

This is pretty fundamentally impossible, and probably honestly undesirable. Your hardware is racy, your OS is racy, the other programs on your computer are racy, and the world this all runs in is racy. Any system that could genuinely claim to prevent all race conditions would be pretty awful to use, if not just incorrect.

Explicitness

There is very little things in Rust that are implicit. No implicit conversions, no implicit fallibility through exceptions or NULLS. Generally, you only need to look at a function's signature to know if it can fail:

#![allow(unused)]
fn main() {
use std::io::Error as IoError;
trait Example { // ignore what a trait is for now ;-)
/// A plain function returning a string
fn this_will_never_fail() -> String;

/// Option replaces the concept of NULL from other languages
/// It either contains `Some(value)` or `None` at all
fn this_operation_might_not_produce_a_value() -> Option<String>;

/// A failing operation should indicate why it is failing and how.
/// In Rust, this is indicated with the `Result<T, E>` type.
/// The function in this contrived scenario might be reading a file
/// from the disk, so it may return an IO error
fn this_operation_might_fail() -> Result<String, IoError>;
}
}

Keep in mind: When we are talking about fallibility here, we mean recoverable errors. Rust has a concept of irrecoverable errors called panics, which usually end either the source thread or the whole process. More on them in a later chapter.

Rust is also explicit about how data is handled:

  • You know when data is moved, when data is cloned, when data is passed by reference. Rust does none of these operations implicitly
  • No implicit type conversions are done, even between primitive types. This encourages the programmer to handle these conversions (and their possible fallibility) explicitly and cover all cases, or decide which behavior is preferred (for example if adding numbers over the type's limit should wrap around, saturate or error)
  • Whether data can be mutated has to be stated explicitly the moment a binding is declared, and immutable is the default. This helps prevent unintentional mutability and opens up a pathway for more compiler optimizations.

Elaborate type system

With the last point, we delved into the field of Rust's type system. Rust has is a strongly-typed language with good generics, allowing it to encode a lot of information in the types.

Information encoded in types is type-checked and verified at compile-time. Good type design in Rust obviates the need for certain tests for relations between data.

Thanks to traits, which in layman terms are comparable to interfaces or Haskell's typeclasses, we can model things such as this:

  • typenum - type-level numbers evaluated at compile-time
  • frunk - strongly-typed functional tools

Other applications are possible, for example:

  • Encoding permissions in associated types
  • Extremely performant and safe lazy iterators
  • Enforcing sequences of events with ordered types

Rust also has a rich selection in its standard library:

  • Several enum types (Rust's enumerations resemble algebraic sum types found in functional languages) like Option and Result for expressing state of data
  • Many pointer types
  • Several string types
  • Many traits (typeclass) for describing the behavior, properties or semantics of other types

While it may seem complicated to have many types, the benefit is that it allows precise control over data, and sets what assumptions you can make.

For instance, here is a view of a couple pointer types:

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::sync::Arc;
use std::pin::Pin;
struct T;

let x = &T; // this is an immutable borrow pointer, I know no one can modify this while I hold it
let mut x = &mut T; // this is a mutable reference, I know no one else has access to this value now;
let x = &T as *const T; // C-ish const pointer, most operations with this require unsafe {}
let mut x = &mut T as *mut T ; // C-ish pointer, most operations with this require unsafe {}
let x = Box::new(T); // Heap allocated pointer
let x = Rc::new(T); // Reference-counted pointer, handles to it can be freely shared, de-allocated when references reach 0
let x = Arc::new(T); // Atomically reference-counted pointer, wraps data immutably in a manner safe across thread boundary
let x = Pin::new(Box::new(T)); // A pinned pointer, data pointed to by this pointer are guaranteeed not to move
}

By selecting the correct pointer type, I can have the behavior I need and only that behavior.

Minimal modular standard library but batteries-included toolchain

A long time ago, a maxim was invented in the Python project:

The standard library is where code goes to die

This was a consequence of a batteries-included approach to the standard library. However, once you include something in the standard library, it is suddenly tied to many other things, and updating it or changing it completely becomes a difficult task because you can assume that every project written in the language might depend on this code (or one of the project dependencies does...).

Furthermore, the standard library is not disjoint from the language itself. Each version of the standard library is bound to one version of the language. Which means that you can't opt to use newer or older standard library while staying at language version you are comfortable with.

Rust tries to prevent this by keeping standard library absolutely minimal, and have it only cover the most basic development tasks centered about the language itself, IO and basic OS interaction.

This functionality is then separate into their own recommended crates, which are sometimes called "blessed". For example, these are:

  • rand - random number generation
  • chrono - proper, timezone-aware datetime management
  • log - a logging facade
  • regex - regular expressions
  • dirs - standard locations of directories such as cache or home directory

While this may seem cumbersome, it has a large benefit in separating from the language and standard library versions itself, allowing you to use a version of your choosing or use a completely different implementation altogether. Another benefit is that these now become opt-in, and your compiler won't waste time on it and your binaries won't include unnecessary code, if you don't need such functionality at all.

In embedded contexts, it is important to note that while the standard library comes pre-compiled by default, dependencies are always compiled on your build machine when you are building the crate. This can allow you to do the following:

  • Compiling in a mode suited to your needs, ie. compiling for size or replacing stack unwinding logic with abort in constrained environments
  • Choosing which features of the library to enable to prevent unused machine code in binary
  • Swapping out core components like the allocator, a particular dependency, or which version and how the crate links to a system library
  • Having the desired amount of debugging information even for dependencies. This extends to being able to use analytic tools on the crates you depend on, to find potential issues, or make decisions to optimize your dependency tree. Compare with C libraries which generally come pre-compiled in your system.

However, the toolchain itself is batteries included, and contains the following tools:

  • package manager
  • code formatter
  • language server implementation
  • auto-completer
  • linter
  • a tool for automatically fixing common mistakes and unambiguous warnings

This means that Rust has a great tooling support and can be used in many editors/IDEs, you can find a list of compatible editors here. Arguably, the best support for Rust has Visual Studio Code via the Rust-Analyzer extension.

Great documentation

Rust has greatly integrated documentation. In fact, documentation is a native language feature, done through special documentation comments.

By making documentation a feature of the language itself, the following is possible:

  • All documentation is explicitly tied to specific symbols in the source code
  • Rust's documentation features have type lookup, just like the language, allowing for efficient cross-linking between different parts of the documentation
  • All code examples are by-default checked for validity, if the example wouldn't compile, then the crate itself doesn't pass testing
  • It is possible to prevent undocumented code from compiling, essentially enforcing 100% public API documentation coverage.

Here's a snippet of how documenting is done on the code side of things:

//! This is an interior doc comment
//!
//! It documents either the module, or the item it is found in.
//! Here's a runnable code example:
//! ```rust
//! fn main() {
//!     println!("Hello, world! This is Rust in documentation,
//!                  if I make a mistake here,
//!                  this won't compile ;-)");
//!
//!     // let's use are cool function here, if its API changes,
//!     // this example will need to be updated()
//!     let sum = my_cool_fun(3, 40);
//!
//!     println!("{}", sum);
//! }
//! ```

/// This is an exterior comment documenting a function
///
/// A function adding the sum of all numbers between two numbers
pub fn my_cool_fun(a: i32, b: i32) -> i32 {
    (a..b).sum()
}

fn main() {
    println!("sum of numbers between 10 and 100: {}", my_cool_fun(10, 100));
}

All documentations is then taken and translated into a website form, which allows effective search, and inspecting relevant sections of the source code.

Rust itself is documented this way, check out the following links:

Third-party crates have the same documentation:

For purposes of less code-bound and more elaborate guide-level documentation, Rust also includes a tool for writing online books, mdBook. This page that you are on right now is written in mdBook also, but here is a couple other examples:

Donwsides

Learning curve

As it is often stated, Rust has a steeper learning curve, especially the concepts of borrowing, lifetimes, ownership and move semantics over copy semantics take longer to get adjusted to. Rust's syntax looks deceptively similar to C-like languages, which can lead C/C++ programmers to jump in head-first, and that usually leads to frustration.

Rust really is a language that requires some study first.

Longer development times

Rust is not a language suited for rapid prototyping. As it constantly requires you to handle every possible error, or every possible value, and to prevent possible memory issues by abiding to the borrowchecker, development takes longer.

Since Rust prefers expressiveness, some time is also needed for properly specifying types and doing proper conversions. In fact, unlike most other languages, Rust does not even implicitly convert between numeric types:

fn main() {
    let y: f32 = 6;    // does not compile {{integer}} != f32;

    let byte: u8 = 15;
    let my_num: i32 = 10000 + byte; // does not compile,
                                    // expected i32, got u8
}

However, if you try to run this example, you will see that Rust is quite helpful with its error messages, including suggested changes to fix this issue.

Some people compare the Rust compiler to a tough sensei that beats you with a stick for every indiscretion, but ends up making you the fighter that wins the King of Iron Fist Tournament ;-)

Young ecosystem

Rust has a relatively young ecosystem. Many tools are somewhat new, many commonly used libraries are pre-1.0, and breaking changes are to be expected.

Furthermore, more niche areas are often not covered. The ecosystem is rapidly evolving, with carefully designed libraries and frameworks appearing constantly to satisfy different needs, but you may run into an issue which will require you to write your own solution.

Considerations for Implementing Rust in production

NEUTRAL: Learning curve pt.2

Implementing Rust in Practice will likely take longer than most mainstream languages. Especially learning to co-exist with the infamous borrowchecker takes adjustment, especially for programmers coming from high-level, dynamically-typed languages.

This is also due to Rust being a very strict language that prefers its idioms. It is usually very easy to do things the Rust way, and difficult to do it otherwise. There is a common saying in the community that Rust makes it quite hard to write really bad code.

This however has its benefits:

  • Code written in Rust is easier to maintain
  • Certain issues that require time are completely eliminated - memory safety issues, concurrency, going around like a detective looking for things which might throw uncaught exceptions
  • Foreign Rust code is more trustworthy than for example foreign C code

After overcoming the initial learning curve, however, some of the time is regained, because apart from getting the code to compile, programmers mostly only need to worry about logic errors.

GOOD: Backwards compatibility

Since Rust 1.0, the language is completely backwards compatible. You can run code written in 2015 without a hitch today, it will just work. This is mostly due to an amount of foresight and planning of Rust's future.

Currently, Rust is developed in a way that revolves mostly around non-breaking additions, such as adding new types and trait implementations that weren't previously available, optimizations, or syntax extensions in places which previously wouldn't compile (for example or-pattern anywhere, which is syntax that would previously result in a syntax error).

However, sometimes, breaking changes are necessary, these mostly come in the following forms:

To be able to make these changes without losing backwards compatibility, editions (also sometimes called epochs) were introduced. Rust currently has three editions:

Editions only exist in syntax and high-level internal representation, they are eventually transformed into the same IR, and no further distinctions are made in the compiler.

This has a couple neat implications:

  • All editions receive security fixes and optimizations
  • All editions receive new features that are not a breaking change
  • Editions are completely compatible with one another:
    • Rust 2015 crate can depend on a Rust 2021 library
    • Rust 2018 crate can depend on a Rust 2015 library
    • and so on..

Cross-version has the benefit of not rushing large projects into keeping up with the latest edition. When your codebase is ready for it, it can be ported, but until then, it will continue receiving all the important patches, new features and will be able to use the latest libraries.

It also prevents a Python 2 vs Python 3 schism from occurring in Rust.

The Rust toolchain also contains the tool rustfix for automatically fixing certain common and clear errors/lints, and this includes edition differences, which makes moving to a new edition of Rust much easier.

BAD: The ecosystem

For many applications, Rust isn't quite there yet with libraries and frameworks. Many currently used frameworks are not stable (or past 1.0) yet, so it is fair to expect breaking changes in API, which could occur quite often. This means that to get the security fixes that you need, you have to invest in rewrites of your codebase to be compatible with the latest version.

Cargo with its lock file feature prevents your code from breaking spontaneously, if used correctly, however crates have many dependencies between each other, and you may run into the following situation:

  • You want to use new crate A, or a new version of a crate A
  • crate A depends on crate B, which you already depend on in an older version
  • Other crates you depend on depend on that particular old version of crate B
  • New version of crate B depended on by crate A is not semver compatible with the old one

Two things result from this:

  1. If the version requirement in your Cargo manifest is too loose, Cargo will bump the version and your code suddenly no longer compiles
  2. Cargo cannot find an appropriate version for crate B and dependency resolution fails

The result is the same: You need to update a significant portion of your dependencies and do a rewrite, or depend on a very old version of A (if such a version even exists), which is prone to contain bugs, or exploits, fixes for which will not be back-ported to the old version.

GOOD: Zero-overhead FFI and interop tooling

It is easy to integrate Rust into existing projects by leveraging its Foreign Function Interface (FFI). Rust can expose its symbols in a way that's consistent with C, which makes it easy to integrate into any language that has C interoperability.

Rust's zero-cost abstractions, lack of runtime and garbage collector allow for writing code that integrates into critical components of projects written in other languages.

While the highest benefit seems to be derived in languages that are on the slower side, such as Python or Ruby, libraries for integrating Rust have been created for many languages:

..and many more

The best support exists for C/C++, as no special "glue" is required. Rust also has several tools which help the process of interop with C and C++ considerably:

  • c2rust - a tool for migrating C code to Rust, produces a compilable Rust implementation of given C code, usually only refactoring needs to be done afterwards
  • bindgen - automatically generates Rust FFI bindings to C (and some C++) libraries

This allows for sort of a creeping Rust pattern, where you don't have to commit to Rust completely, but only rewrite a particular component, usually one where performance and robustness is critical, and then continue with more parts of the codebase. This is the route taken by NPM for example.

Interoperability also leads to a lesser cost of rewriting in Rust, as you can break your codebase down into components, even if it is a monolithic application, and then assign different priorities to each component, continually swapping out legacy code for Rust implementations, until you arrive at a fully Rust codebase.

GOOD: Built-in testing, benchmarking, documentation and dependency management

Rust comes bundled with a package manager and build system in one called Cargo. This package manager is available on almost all platforms and makes building both your and 3rd party code effortless by automatically pulling in dependencies, managing language features, and allowing easy deployment of your packages to public registries, most often the official crates.io.i

Tests, benchmarks and documentation are first-class lang features, and the language has means for checking their validity (and e.g. tools for verifying code coverage). While documentation is showcased higher in section Great documentation, this is how tests and benchmarks look in their simplest form:

#![allow(unused)]
fn main() {
#[test]
fn my_test() {
    panic!("this test will fail");
}

#[bench]
fn my_benchmark(b: &mut Bencher) {
    b.iter(|| println!("cpu intensive operation"));
}
}

Depending on where these are located in the package structures, this can be either unit or integration tests

Domains most suited for Rust

Rust is most commonly used in the following areas:

  • Backend development where performance matters: For example AWS, Cloudflare npm and Coursera use Rust on the backend in performance-sensitive and robustness-requiring situations. We at Braiins also use Rust for network backend development
  • Low-level and OS development - Android now officially supports Rust for developing the OS itself, Fuchsia OS also has parts written in Rust. Independent Rust OS-dev projects have also popped up, most notably Redox OS and Tock
  • Embedded development / operating in constrained environments - Rust's modular nature allows stripping it down to bare essentials, and swapping pretty much everything for custom implementations. Many libraries support running without standard library and manual memory management, C interop and inline assembly allow for fine control over hardware in bare-metal environments. At Braiins, Rust is the preferred language for embedded development

Domains not suited for Rust

  • AI/Machine Learning - while Rust does have some support for ML, it is still in its infancy, and the theoretical performance benefits are far outweighed by the lack of ML ecosystem in Rust
  • Frontend Web Development - using Rust on the frontend is possible and feasible from a internal code-reuse viewpoint (you can share type definitions between frontend and backend effortlessly), however, there is hardly any framework at the time of this writing that would allow rapid prototyping (there are Rust frontend frameworks), and most web libraries either have missing bindings or lack Rust equivalents. Rust is also lacking in terms of surrounding tooling such as tools like webpack, CSS preprocessors, asset preprocessors and so on. This is mostly due to the fact that Rust WebAssembly support has been stable for a relatively short time
  • Areas that require standardization and/or certified compiler - Some areas of development require compilers certified for functional safety, for example some areas of medicine. No Rust compiler is currently certified, and there exists no official Rust standard. While standardization is planned, it is still likely years away.

Rust installation

There is a couple ways to install Rust. If you are using a mainstream Linux distribution, Rust is likely to be in its package repositories.

Because Rust updates quite often, and you may want to keep multiple toolchains (some analysis tools require nightly, and some crates for formal verification are pinned to particular toolchain versions because they import compiler internals), I suggest installing rustup.

If your distribution does not have a rustup package, or if you are using Windows and MacOS, visit the website for rustup: https://rustup.rs/

Keep in mind that there are two dependencies you need to satisfy on any system to compile Rust:

  • gcc, clang or MSVC C compiler
  • git

Sometimes, one or both of these dependencies might be bundled with the Rust distribution for your system. Rust also depends on LLVM for its machine code generation, but that is always bundled.

Conclusion

Rust makes an excellent choice for many applications. While you can use it for pretty much anything, it is most suited for areas listed above in the section Domains most suited for Rust and perhaps least for areas listed in Domains not suited for Rust.

It is up to every programmer, and by extension company or organization, to decide whether the benefits of Rust is something they are interested in when faced with the cost of switching to a different technology, one which has a steeper learning curve.

On the other hand, the issues that Rust solves are critical and not having them is a long-term benefit. In a post from 2019, Mozilla revealed that 73.9% of security bugs would have been prevented by Rust in Firefox's style component alone.

The CVE repository at the time of this writing lists 6386 memory corruption vulnerabilities, caused by use after frees, possible double frees, buffer overflows, a large number of this would have been prevented by Rust. Memory issues may cause security vulnerabilities and arbitrary code execution, which allows malicious actors to cause significant damage.

In light of this, why not give Rust a go? ;-)

Exercises

  1. Without even needing to have Rust installed, you can run Rust code in your browser by using the official Rust Playground. Visit it, run the Hello, world!, and maybe try experimenting a little.
  2. Try installing Rust on your machine :)

Supplementary materials

  • Presentation
  • Handout