TL;DR

Rust’s compiler enforces stricter rules. Apparently a compiler does a much better job than a human at following rules. Rust programmers learn and follow these rules, but their mind load is reduced overall.

Rust as a Systems Programming Language

Rust is a language for systems programming. Systems programming is resource-constrained programming. It is programming when every byte and every CPU cycle counts.

Rust a new tool that eliminates major, well-understood problems that have plagued a whole industry for decades.

Navigating “Programming Rust, Second Edition”:

  • Read the first five chapters through in order.
  • Chapters 6 through 10 cover the basics of the language. It’s all right to skim a little here, but don’t skip the chapter on error handling.
  • Chapter 11 covers traits and generics. Chapter 12 shows how traits support operator overloading, and Chapter 13 covers many more utility traits. Understanding traits and generics unlocks the rest of the book. Closures and iterators, two key power tools that you won’t want to miss, are covered in Chapters 14 and 15, respectively.
  • You can read the remaining chapters in any order, or just dip into them as needed.

Attribution: Programming Rust, Second Edition by Jim Blandy, Jason Orendorff, and Leonora F. S. Tindall (O’Reilly). Copyright 2021 Jim Blandy, Leonora F. S. Tindall, and Jason Orendorff, 978-1-492-05259-3.

1. Free of Undefined Behavior

The following C program compiles without errors or warnings:

1
2
3
4
5
6
7
8
int main(int argc, char **argv) {
    unsigned long a[1];
    a[3] = 0x7ffff7b36cebUL;
    return 0;
}
// On Jim’s laptop this morning, this program printed:
// undef: Error: .netrc file is readable by others.
// undef: Remove password or make file unreadable by others.

The array a is only one element long, so using a[3] is, according to the C programming language standard, undefined behavior:

Undefined behavior: Behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements.

Undefined behavior doesn’t just have an unpredictable result: the standard explicitly permits the program to do anything at all.

  • In this case, storing this particular value in the fourth element of this particular array happens to corrupt the function call stack such that returning from the main function, instead of exiting the program gracefully as it should, jumps into the midst of code from the standard C library for retrieving a password from a file in the user’s home directory.

C and C++ are the industry standards for systems programming. They have hundreds of rules for avoiding undefined behavior. These rules are mostly common sense: don’t access memory you shouldn’t, don’t let arithmetic operations overflow, don’t divide by zero, and so on. But the compiler does not enforce these rules. The responsibility for avoiding undefined behavior falls entirely on the programmers.

Assuming that you can avoid undefined behavior in C and C++ is like assuming you can win a game of chess simply because you know the rules.

The Rust language makes you a simple promise: if your program passes the compiler’s checks, it is free of undefined behavior. Dangling pointers, double-frees, and null pointer dereferences are all caught at compile time. Array references are secured with a mix of compile-time and run-time checks, so there are no buffer overruns.

In order to make stronger guarantees about your program’s behavior, Rust imposes more restrictions on your code than C and C++ do. But the language overall is flexible and expressive.

Being able to trust the language to catch more mistakes encourages us to try more ambitious projects. Modifying large, complex programs is less risky when you know that issues of memory management and pointer validity are taken care of. Debugging is much simpler when the potential consequences of a bug don’t include corrupting unrelated parts of your program.

In practice, taking undefined behavior off the table substantially changes the character of development for the better.

2. Easier Concurrency

Concurrency is notoriously difficult to use correctly in C and C++. Developers usually turn to concurrency only when single-threaded code has proven unable to achieve the performance they need. But parallelism is too important to modern machines to treat as a method of last resort.

The same restrictions that ensure memory safety in Rust also ensure that Rust programs are free of data races. You can share data freely between threads, as long as it isn’t changing. Data that does change can only be accessed using synchronization primitives. All the traditional concurrency tools are available: mutexes, condition variables, channels, atomics, and so on. Rust simply checks that you’re using them properly.

This makes Rust an excellent language for exploiting the abilities of modern multicore machines. The Rust ecosystem offers libraries that go beyond the usual concurrency primitives and help you distribute complex loads evenly across pools of processors, use lock-free synchronization mechanisms like Read-Copy-Update, and more.

3. Fast

Systems programming is often concerned with pushing the machine to its limits.

Rust shares the ambitions Bjarne Stroustrup articulates for C‍++ in his paper “Abstraction and the C++ Machine Model”:

In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

Rust shares the ambitions.

If you are ready to make the investment to design your program to make the best use of the underlying machine’s capabilities, Rust supports you in that effort. Rust is designed with efficient defaults and gives you the ability to control how memory gets used and how the processor’s attention is spent.

4. Easier Collaboration

Rust’s support for code sharing and reuse is great.

Rust’s package manager and build tool, Cargo, makes it easy to use libraries published by others on Rust’s public package repository, the crates.io website.

  • You simply add the library’s name and required version number to a file, and Cargo takes care of downloading the library, together with whatever other libraries it uses in turn, and linking the whole lot together.
  • Think of Cargo as Rust’s answer to NPM or RubyGems, with an emphasis on sound version management and reproducible builds.

The language itself is also designed to support collaboration: Rust’s traits and generics let you create libraries with flexible interfaces so that they can serve in many different contexts. And Rust’s standard library provides a core set of fundamental types that establish shared conventions for common cases, making different libraries easier to use together.


References