2022-09-28 (Edited: 2025-04-15)

Rust

Table of Contents

1. General Background

Rust is a general purpose programming language. The development of Rust started in 2006 by a single developer Graydon Hoare within the Mozilla Research group. The first stable release was in 2015. This is when all the features were finalized and backward compatibility was guaranteed.

The motivation behind creating Rust was to have a language that provides: Performance, Type Safety, Memory safety and Concurrency.

It is a general purpose language, that has seen more usage in the following domains:

  • Systems Programming
  • High Performance application
  • Web servers
  • Embedded Systems
  • CLI Applications

Rust boasts a good optimizing compiler (based on LLVM) and has a number of key defining features:

  • Strong Type system
  • Pattern matching
  • Memory Safety
    • Ownership Rules
    • Lifetimes in the Type System
  • Traits
  • Meta-programming
  • Async/Await for concurrency

Fun fact: the name rust comes from a fungus that is robust, distributed and parallel.

2. Type System

Rust has a very strong and expressive type system. It is static (every variable has a fixed type) & strong (everything is checked during compile time). The types are inferred as far as possible, thus improving developer ergonomics. There are no implicit type casting, and thus this avoid many bugs. It has Algebraic Data Types, which is a fancy way of saying that you have product type (struct) that group multiple different types together and also you have sum types (enum) that denote choice among multiple different types. Apart from that there are Generics (just like templates in C++).

All this leads to type safe language that is still very expressive.

2.1. Enums

Enums have a peculiar feature in Rust. They can store auxiliary data along side the enum tags. And combined with the pattern matching feature, it give a really nice way to handle a lot of common programming idioms.

enum Event {
    Quit,
    Click(Position),
    Key(char)
}

fn handle_event(event: Event) {
    match event {
        Event::Quit => println!("Received Quit event."),
        Event::Click(pos) => println!("Mouse clicked at: ({}, {})", pos.x, pos.y),
        Event::Click
        Event::Key(c) => println!("Key pressed: '{}'", c),
    }
}

3. Memory Safety

Talking about Rust is incomplete without talking about Memory Safety. Rust is not a garbage collected language, but still you don't have to do manual memory management. Everything is freed at the right place. Array bounds are checked and the language guarantees memory safety (thus no use after free, and no double freeing).

So, how does having no garbage collector and not having to do manual memory management go hand in hand? There are two key concepts for this: Ownership and Lifetime.

4. Ownership

4.1. What is Ownership?

  • Every value has an owner
  • There can be only one owner at a time
  • When the owner goes out of scope, the value is dropped
{                                   // s is not valid here, it’s not yet declared
    let s = String::from("hello");  // s is valid from this point forward
    ...
}                                   // s is now freed


  • When the variable s goes out of scope, Rust calls the function drop and then the memory used by String is freed. This is similar to RAII (Resource acquisition is initialization) pattern in C++.

4.2. Assignments

  • When we assign another value to s, the previous value is dropped.
  • When we move s to another variable, s is no longer valid.
  • This way there is no double freeing.
{
    let s1 = String::from("hello");   // s1 is valid from this point forward
    let s2 = s1;                      // s1 is moved to s2.
                                      // i.e. s2 points to same data as s1
                                      // and thus s1 is now invalid.
    ...

}// s2 is dropped, s1 was invalid so there is no double free

4.3. Function calls

  • When a value is passed to function, the function takes ownership of that value. Think of it as the assignment case, where the value is not assigned to the function argument.
  • The value can't be used outside the function
  • Unless you return that value from the function and assign it to a variable
fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // and so s is no longer valid here

} // Because s's value was moved, no need to free

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope thus dropped

4.4. References

But passing values in and out of function is cumbersome, and if we don't want the function to take ownership of those values instead of giving ownership, let the function borrow.

fn main() {
    let s = String::from("hello");  // s comes into scope

    borrow(&s);                     // Reference to s is passed to function

} // s is dropped here

fn borrow(some_string: &String) {
    println!("{some_string}");
} // some_string is a reference, no need to drop

4.5. Mutable Reference

  • Only one mutable reference can exist at a time
  • Whoever has a mutable reference is guaranteed that the value won't change without them changing it
  • You can't even create immutable reference after when there is a mutable reference.
  • Whoever has a immutable reference is guaranteed that the value won't change while they have it.

5. Lifetime

  • Every reference in Rust has a lifetime (the scope for which the reference is valid)
  • Think of lifetime as extension to the type rules.

The following doesn't work because r has a lifetime of 'a but x has a smaller lifetime of 'b. This is called borrow checking. i.e. while borrowing value (using references) the borrowed thing must live at least as long as the borrower.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

The following would have cause a use after free issue, if Rust didn't have lifetime feature. But since swap_tokens specifies that it take two robots with the same lifetime, the swap_tokens call is invalid and the compiler throw an error during compilation time.

struct Robot<'a> {
    token: &'a Token,
    name: String
}

fn main() {
    let token1 = create_token();
    let mut robot1 = Robot { token: &token1, name: "R1".to_string()};
    {
        let token2 = create_token();
        let mut robot2 = Robot {token: &token2, name: "R2".to_string()};

        swap_tokens(robot1, robot2); // Compiler will throw an error here
    }

    ...
}

fn swap_tokens<'a>(robot1: &mut Robot<'a>, robot2: &mut Robot<'a>) {
    let t1 = robot1.token;
    let t2 = robot2.token;
    robot1.token = t2;
    robot2.token = t1;
}

6. Traits

Rust doesn't have traditional OOP (i.e. no classes, objects). Instead the data and methods are separated (just like in Go). You define struct and then define method on them.

And instead of Classes we have Traits that are like interface in Go (only difference is that there can be default implementation to). Traits can be composed.

struct Dog {
    name: String
}

impl Dog {
    fn bark(&self) {...}
}

trait Motion {
    fn walk(&self, speed: u32);
    fn run(&self) {
        self.walk(100);
    }
}

impl Motion for Dog {
    fn walk(&self) { ... }
}


7. Metaprogramming

Rust has a strong and powerful metaprogramming features that is usually missing from many programming languages. Metaprogramming is "writing programs that write programs". There are macros that take Rust code and generate another rust code. As an example, there is a library that a Struct and creates json serializer and deserializer for that.

#[derive(Serialize, Deserialize)]
struct Config {
    port: u16,
    host: String,
}

fn do_stuff() {
    let config: Config = from_str(json_str)?;
    let json = to_string(&config)?;
}

Similarly, metaprogramming is used in libraries as:

  • CLI argument parsing
  • Web route annotations
  • Generate bindings to C

8. Adoption

Overall, Rust is very much liked by developers. One survey (by StackOverflow) showed that about 28 percent of developers want to develop in Rust. And many companies are also using Rust. In summary, Rust is an awesome, highly performant language that seems to have a bright future.

9. References

  • Klabnik, Steve, and Carol Nichols. The Rust programming language. No Starch Press, 2023.
  • Matzinger, Claus. Rust Programming Cookbook: Explore the latest features of Rust 2018 for building fast and secure apps. Packt Publishing Ltd, 2019.
  • Xu, Hui, et al. "Memory-safety challenge considered solved? An in-depth study with all Rust CVEs." ACM Transactions on Software Engineering and Methodology (TOSEM) 31.1 (2021): 1-25.
  • Bin Uzayr, Sufyan. Mastering Rust: A Beginner's Guide. CRC Press, 2022.

Backlinks


You can send your feedback, queries here