days
0
-9
hours
0
-5
minutes
0
-1
seconds
0
-1
search
Safe systems programming

Why you should take a closer look at Rust 1.0

Huon Wilson
One image via Shutterstock

Blazingly fast performance, prevention of nearly all segfaults and low-level control and performance without sacrificing safety or abstractions – these are the promises made by the 1.0 release of Rust. And that’s just the start.

The Rust programming language has just reached 1.0, after several years of iterative improvement. It is a modern systems language, designed to give you low-level control, high performance and powerful concurrency – combining many of the best features from other languages, while losing the worst pitfalls of traditional systems languages like C or C++. To do this, it overcomes many of the traditional trade-offs, providing:

  • Memory safety without garbage collection
  • Concurrency without data races
  • Abstraction without overhead
  • Stability without stagnation

No garbage collection

Garbage collection is a powerful tool in software engineering, freeing you from worrying about keeping track of memory manually and allowing you to get on with the task of writing great code. It’s great when it works, but garbage collectors have real downsides that make them inappropriate for many areas. Things like operating systems, embeddable libraries and (soft) real-time applications often need a greater degree of control and predictability than garbage collection can offer.

Rust allows developers to forgo a garbage collector entirely, without being thrown back into a world of forgotten frees, dangling pointers and segfaults. The key concepts in Rust are ownership and borrowing. These ideas are ubiquitous in programming and an important part of modern C++, but unlike other industry languages, Rust puts them front and centre, statically checking and leveraging them to guarantee memory safety without a garbage collector, something that has been previously unthinkable.

Rust’s idea of ownership is that each value has exactly one parent that has complete control. As values get reassigned, placed into data structures or passed into functions, they move and are statically no longer accessible via their original path. And if they are not moved away at the end of a scope, they are automatically destroyed. To make ownership work at scale, Rust also provides ways to temporarily “borrow” (make a pointer to) a value for the duration of a scope.

As a bonus, ownership replaces more than just garbage collection. It is vital to Rust’s concurrency guarantees, and even removes other classes of bugs like iterator invalidation. It also applies to resources other than memory, freeing you from managing when to close sockets or files, for example.

Concurrency

As mentioned, ownership also ensures your concurrent programs won’t fall prey to some of the most insidious problems that can occur in them: data races. And all while maintaining a weak memory model, close to that used by hardware.

Getting started with concurrent programs in Rust is simple, pass a closure to a function from the standard library:

use std::thread;

fn main() {

    let some_string = "from the parent";

    thread::spawn(move || {

        // run on a new thread

        println!("printing a string {}", some_string);

    });

}

One of the tenets of many languages designed for concurrent programming is that shared state should be minimized or even outlawed entirely, in favor of techniques like message passing. Ownership means that values in Rust have a single owner by default, so sending a value to a new thread through a channel will ensure the original thread doesn’t have access to it: statically disallowing sharing.

However, message passing is just one tool in your toolbox: shared memory can be immensely useful. The type system ensures that only thread-safe data can actually be shared between threads. For example, the standard library offers two sorts of reference counting: Arc provides thread-safe shared memory (immutable by default), while the Rc type offers a performance boost over Arc by forgoing the synchronization needed for thread-safety. The type system statically ensures that it is not possible to accidentally send an Rc value from one thread to another.

When you do want to mutate memory, ownership provides further help. The standard library Mutex type takes a type parameter for the data that is to be protected by the lock. Ownership then ensures that this data can only be accessed when the lock is held; you cannot accidentally release the lock early. This sort of access-control guarantee falls automatically out of Rust’s type system and is used in many places through the standard library itself and more broadly.

Zero-cost abstractions Performance and predictability is one of the goals of Rust, and an important step to achieving that while still offering the safety and power required is zero-cost abstractions à la C++. Rust lets you construct high-level, generic libraries that compile down to specialized code you might have written more directly for each case.

To do this, Rust gives precise control over memory layout: data can be placed directly on the stack or inline in other data structures, and heap-allocations are much rarer than in most managed languages, helping achieve good cache locality, an extremely large performance factor on modern hardware.

This simple, direct layout of data means optimizers can reliably remove layers of function calls and types, to compile high-level code down to efficient and predictable machine code. Iterators are a primary example of this, the following code is an idiomatic way to sum the squares of a sequence of 32-bit integers:

fn sum_squares(nums: &[i32]) -> i32 {

    nums.iter()

        .map(|&x| x * x)

        .fold(0, |a, b| a + b)

}

This always runs as a single pass over the slice of integers, and is even compiled to use SIMD vector instructions when optimizations are on.

Powerful Types

Traditionally, functional programming languages offer features like algebraic data types, pattern matching, closures and flexible type inference. Rust is one of the many recent languages that don’t fit directly into the functional mould that have adopted those features, incorporating all of them in a way that allows for flexible APIs without costing performance.

The iterator example above benefits from many of these ideas: it is completely statically typed, but inference means that types rarely have to be written. Closures are also crucial, allowing the operations to be written succinctly.

Algebraic data types are an extension of the enums found in many mainstream languages, allowing a data type to be composed of a discrete set of choices with information attached to each choice:

struct Point {

    x: f64,

    y: f64

}

enum Shape {

    Circle {

        center: Point,

        radius: f64

    },

    Rectangle {

        top_left: Point,

        bottom_right: Point

    },

    Triangle {

        a: Point,

        b: Point,

        c: Point

    }

}

Pattern matching is the key that makes manipulating these types easy, if shape is a value of type Shape, then you can handle each possibility:

match shape {

    Shape::Circle { radius, .. } => println!("found a circle with radius {}", 

radius),

    Shape::Rectangle { top_left: tl, bottom_right: br } => {

        println!("found a rectangle from ({}, {}) to ({}, {})",

                 tl.x, tl.y,

                 br.x, br.y)

    }

    Shape::Triangle { .. } => println!("found a triangle"),

}

The compiler ensures that you handle all cases (a catch-all clause is opt-in), greatly aiding refactoring.

These enums also allow Rust to forgo the so-called billion dollar mistake: null references. References in Rust will never be null, with the Option type allowing you to opt-in to nullability in a type-safe and localized manner.

Conclusion

Rust is sponsored by Mozilla, which is interested in a language that can replace C++’s performance and zero-cost abstractions for web browser development, while guaranteeing memory safety and easing concurrent programming.

Rust fills a niche sometimes considered impossible: providing low-level control and performance without giving up safety or abstractions. Of course, there’s no free lunch: the compiler has a reputation as being a demanding assistant who doesn’t tolerate even any risk, and the ownership model, being a bit unfamiliar, takes some time to learn.

The 1.0 release comes with the core language and libraries being tested and refined, and, importantly, the first guarantee of stability: code that compiles now should compile with newer versions for the foreseeable future. However, this release doesn’t mean the language is done: Rust is adopting a train model, with new releases every six weeks. New and unstable features can be explored via regular pre-release betas and nightlies. There is a standard package manager, Cargo, which has been used to build up a growing ecosystem of libraries.

Like the language, this ecosystem is young, so there’s not yet the wide breadth of tooling and packages that many older languages offer (although a performant, easy FFI helps with the latter). Nonetheless, the language itself is powerful, and a good way to do low-level development without the traditional danger.

Author
Huon Wilson
Huon Wilson is a computational statistics post-graduate student, previously a computational algebra honours student, as well as a member of Rust’s core team.

Leave a Reply

Be the First to Comment!

avatar
400
  Subscribe  
Notify of