Skip to content

Rust Quick Start

Tool Chain Installation

TIP

If Rust is not installed on EOS, you can install it under your home directory by running the following command:

bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

or read the instructions at the Rust Installation page After the installation, you must restart your terminal before you can use the Rust toolchain.

Use the follow steps to install Rust on your own computer:

  1. Download Rustup for your platform (Linux, Windows 32/64-bit, OSX) to install the following development tools:

    • cargo: package manager
    • rustc: Rust compiler
    • rustfmt: source code formatter
    • rustdoc: document generator
    • rustup: tool chain manager
  2. Install the tool chain for your OS platform

    bash
    rustup toolchain install stable

On Windows, at some point in the installation you will be asked to install C++ Build Tools for Visual Studio 2019.

Choice of IDE

  1. VSCode with rust-analyzer plugin (Extension ID rust-lang.rust-analyzer)
  2. CLion with Rust plugin

Setting Up A Project

  1. Run cargo (from a command line) to create a new project directory

    bash
    cargo new my-first-rust
    cd my-first-rust

    The above step creates a new directory my-first-rust with the following files/directories:

    • Cargo.toml: the project configuration file
    • src/main.rs: a "Hello World" program in Rust

    In addition cargo also initialized your project as a (local) git repository.

  2. Compile and run the program

    bash
    cd my-first-rust  # change into the project directory
    cargo build       # compile the code
    cargo run         # run

    Although you may directly use rustc to compile your code, using cargo is more preferred since as your project becomes more complex and depends on third party libraries, cargo will handle downloading these libraries for you.

Application Memory Model

Data manipulated by a program (regardless of the programming language) are typically allocated in one of the following memory areas: data section, stack, and heap:

  • Data items whose type, size, and lifetime are known at compile time are generally allocated in the data section and embedded into the program binary executable. For example, in a "hello world" program, the type and size of the text "hello world" is known at compile-time.

  • If any of these details cannot be determined at compile-time and known only at runtime, then the data items are allocated either on the stack or the heap:

    • Stack is the memory area for fixed size data (easier to manage)
    • Heap is the memory area for data whose size may change at runtime (requires extra work to manage)

    TIP

    It is important to note that the last two bullet points emphasize the location of data (and not the variable) and the memory location of the two may differ.

rs
let a = 71;
let b = String::from("Seventy one");

For instance, in the above snippet:

  • Both the variable a and the numeric value 71 are allocated on the stack since both of them have fixed size.
  • The variable b (which typically requires a pointer and a length) is allocated on the stack (since both the pointer and the integer length require a fixed-size memory), but the string text "Seventy One" is allocated on the heap to allow to change size (more or fewer bytes).

Managing The Heap

Different languages take different approach in managing the heap space

  • Java uses new to allow users to allocate more space in the heap and it depends on its garbage collector to clean up the heap
  • C programmers are required to manually manage the heap space via malloc() and free()
  • Likewise, C++ developers depend on new and delete for similar purposes

Without automatic garbage collection, C and C++ programs are vulnerable to memory leaks. However, adding garbage collector degrades the runtime performance of programs. Rust takes a totally different approach via its borrow checker algorithm which runs only at compile-time, thus it has no implact on runtime performance. To understand the main concept behind borrow checker, you must first understand Rust data ownership.

Concept Unique to Rust: Data Ownership

Rust is usually advertised as a language for system programmings due to its memory-safety feature. However, this feature is made possible by a unique philosophy inherent in Rust: data ownership. Data in Rust are associated with a unique owner (Rust does not honor joint ownership). The concept can be applied to any data, but its practical use is more important for heap allocated data. To understand this concept, it is important that we separate data values from the variables which hold them.

In the above snippet:

  • The numeric data 71 is theoretically owned by the variable a. However since the data is allocated on the stack rules of ownership is less important.
  • The string text "Seventy one" (on the heap) is owned by the variable b

Like any physical objects:

  • Ownership can transfer from one person to another (like selling your car to another person)
  • Objects can be loaned to another person without transferring ownership (like loaning your car to another person)

Rust data ownership follows similar principles.

The Rust compiler keeps track of data ownership for us through its borrow checker routine and it enforces the following policy:

  1. Each data value (on the heap) has a unique owner any time
  2. When the owner goes out of scope its data will be de-allocated from the heap (or dropped in Rust lingo)

The last policy makes it possible for Rust to implement memory safety without being dependent on a garbage collector (like Java) while at the same time off-loading the developer responsibility to manually manage memory de-allocation (like free() in C or delete in C++).

In the following snippet:

  • The memory holding numeric value 71 is dropped when the main function goes out of scope
  • The string object holding the text "Seventy One" is dropped after "Metal" is printed but before "Oxide" is printed
rs
fn main() {
    let a = 71;
    {
        let b = String::from("Seventy One");
        println!("Metal");
    }
    println!("Oxide");
}

Since borrow checker runs only at compile-time, it does not affect run-time performance whatsoever.

Data Ownership: Copy vs. Move

In many other languages, the assignment operator copies data from source (the right hand side of =) to the destination (the left hand side variable)

The Rust single data ownership policy dictates the actual runtime behavior of the above operations:

  • When the data source is allocated on the stack, a simple copy will take place
  • When the data source is allocated on the heap, a "move" will take place, meaning data ownership is also transferred from the current owner to the receiving variable. Internally, the "move" is implemented as copying the address and the length of the string text (from b to y) and invalidating b's pointer. Each on of these micro operations costs O(1), regardless the size of the string text. Invalidating b's pointer prevents double de-allocations that may happen in other languages.
c
let a = 71;
let x = a;
println!("Copper: {} and {}", a, x);
let b = String::from("Seventy one");
let y = b;
println!("Iron: {} and {}", b, y);

When the above snippet is compiled, Rust borrow checker will produce the following error messages:

rs
let b = String::from("Seventy One");
    - move occurs because `b` has type `String`, which does not implement the `Copy` trait
let  y = b;
         - value moved here
println!("Iron: {} and {}", b, y);
                            ^ value borrowed here after move

The cause of the error:

  • The ownership of string text "Seventy One" was moved from b to y. Therefore when println attempts to print b's data, it is no longer owned by b.
  • Hence, an attempt to print (the data of) b failed

The principle of copy and move also applies to function calls and returns.

rs
fn main() {
    let a = 71;
    let b = String::from("Seventy One");
    copper_miner(a);    // a is copied
    iron_worker(b);     // b is moved
    println!("After a={}", a);
    println!("After b={}", b); // Error: value borrows here after move
}

fn copper_miner(z: i32) {
    println!("Mining {}", z);
}

fn iron_worker(z: String) {
    println!("Building {}", z);
} // At runtime the string data "Seventy One" will be deallocated here

Upon returning from iron_worker to main, the text "Seventy One" will no longer exist!

References and Borrowing

In some use cases we need to pass actual arguments to a function without transferring ownership. In which case we have to use references (&) and write the code as follows:

  • In the function call, the actual argument is written with a & prefix before the variable name.
  • In the function declaration, the formal parameter is written with a & prefix before the parameter type. Elsewhere in the function, we don't use & when referring to the parameter
rs
fn main() {
    let b = String::from("Seventy One");
    iron_worker(&b);     // b is borrowed, main retains ownership
    println!("After b={}", b); // No Error
} // At runtime the string data "Seventy One" will be deallocated here

fn iron_worker(z: &String) {
    println!("Building {}", z);
}  // since z is NOT the owner of string text "Seventy One",
   // it was de-allocated when iron_worker goes out of scope

When references are used inside the iron_worker function above, z is actually a pointer to the incoming string variable b where it holds the pointer to the actual text data Seventy One.

Data Mutability

By default all variable declared in Rust are immutable, its data value cannot be altered. To allow updates to a variable data, it must be declared as mutable using the keyword mut:

let a = 71;             // immutable data
let mut count = 0;      // mutable data

// later in code
count = count + 1;

Own and Modify

If we need to transfer data ownership to a function while also let the function modify the data, we have to declare the formal parameter of the function with the mut keyword:

rs
fn main() {
    let b = String::from("Seventy One");
    iron_worker(b); // "Seventy One" is moved
    // b no longer owns "Seventy One"
}

// Allow iron_worker to own and modify
fn iron_worker(mut z: String) {
    z.push_str(" done!");
    println!("Building {} ", z);
} // string text "Seventy One done!" will be deallocated here

Borrow and Modify

Finally, if we want to allow a function borrow and modify the data, we have to use mutable references by inserting &mut in the following two places:

  1. When declaring the formal parameter in the function header
  2. When supplying the actual argument in the function invocation
rs
fn main() {
    let mut b = String::from("Seventy One");
    iron_worker(&mut b); // b is borrowed and altered
}

fn iron_worker(z: &mut String) {
    z.push_str(" done!");
    println!("Building {} ", z);
} // the string text "Seventy One done!" is NOT deallocated here

TIP

Pay attention to the declaration b inside the last two main functions:

  • Under "Own & Modify", the main is not required to declare b as mutable. This is because main does not alter b's data, the iron_worker does.
  • Under "Borrow & Modify", the main function must declare b as mutable. This is because iron_worker modifies data which is owned by a variable in main.

Data Race

A data race is likely to happen when

  • Two or more pointers refer to the same data at the same time AND
  • One of the pointers is used to modify the data

Since Rust implements data borrowing/references using pointers, data race in a Rust program can potentially occur when

  • Two or more references bound to the same data within the same scope AND
  • One of these references is a mutable reference

To prevent data race, Rust allows only one mutable reference within a given scope.

rs
fn main() {
    let mut a = String::from("Hello");
    let mut b = String::from("World");
    cold_forge(&a, &mut b); // Ok
    cold_forge(&a, &mut a); // Not allowed
}

fn cold_forge(one: &mut String, two: &mut String) {
    if one.len() < two.len() {
        one.push_str("***");
    } else {
        two.push_str("#####");
    }
}

In the above snippet, the first call to cold_forge compiles with no error, but the second call triggers an error because we have two references to the same data value within the same use scope and one of them is mutable. The error details generated by the compiler are shown below:

rs
cold_forge(&a, &mut a);
---------- --  ^^^^^^ mutable borrow occurs here
|          |
|          mutable borrow occurs here
immutable borrow later used by call

Use Scope of Multiple Mutable References

To understand how "use" scope is checked by the Rust compiler, let's look at the following code:

rs
fn main() {
    let mut a = String::from("Hello");
    let mut b = String::from("World");
    let r1 = &a;
    let r2 = &mut a;
    cold_forge(r1, &mut b);
    cold_forge(&b, r2);
}

fn cold_forge(one: &String, two: &mut String) {
    if one.len() < two.len() {
        two.push_str(" <<<");
    } else if one.len() > two.len() {
        two.push_str(" >>>");
    }
}

which produces the following error:

rs
let r1 = &a;
         -- immutable borrow occurs here
let r2 = &mut a;
         ^^^^^^ mutable borrow occurs here
cold_forge(r1, &mut b);
           -- immutable borrow later used here

Among the important phrases in the error messages is "used here". When the function calls are commented out, the code compiles without error. The cause of the error is NOT with the declaration of r1 and r2, but how they are used.

Explanation: while the immutable reference (r1) to a is being used/consumed by the first call to cold_forge, there is also an active binding of mutable reference (r2) to a's data value. This implies that during that particular execution of cold_forge, there is a chance that the data value passed into one may get modified, a potential of data race.

Apparently, reordering some of the statements in main eliminates the error.

rs
fn main() {
    let mut a = String::from("Hello");
    let mut b = String::from("World");
    let r1 = &a;
    cold_forge(r1, &mut b);
    let r2 = &mut a;
    cold_forge(&b, r2);
}

fn cold_forge(one: &String, two: &mut String) {
    // same code above
}

So what's going on here?

  • Since binding of r2 as a mutable reference to data value of a takes place after the first call of cold_forge, then r1 is the only reference bound to data value of a when the first call of cold_forge executes
  • Similarly during the second call of cold_forge, r2 is the only (mutable) reference bound to the data value of a.

TIP

It is important to under that in this context the "scope" of the reference refers to duration at which the references are used (as opposed to the lexical scope in which the references are declared)

Slices of Contiguous Data

In addition to common data types such as (char, bool, u8 (8-bit unsigned integer), u32, i32 (32-bit signed integer), f32 (single-precision floating-point), etc.) another data type not commonly supported by other languages, but by Rust is the slice.

TIP

A slice of pizza is a (smaller) portion of a round pizza. A Rust slice is a reference to a portion of data which are stored contiguously.

You have seen how references (of a String) are used for the purpose of borrowing the text data of a string. Instead referencing the entire block of text , a slice allows you to borrow only part of the text data. The concept can be extended to any data which are stored contiguously in memory (such as arrays and vectors).

In the snippet below, a, b, and c are a pointer to the contiguous text data "Hello World".

TIP

Pay attention to the last statement: it is possible to build a slice from another slice.

rs
let s = String::from("Hello World");
let a = &s[3..5];  // a reference to "lo"
let b = &s[6..];   // a reference to "World"
let c = &b[1..3];  // a reference to "or" build from a slice

Since slices can be built from another slice, the following snippet works as well with the following differences:

  • In the above snippet the type of s is String which owns the text "Hello World" allocated on the heap
  • In the below snippet the type of s is &str and the string text "Hello World" is allocated on the stack. Moreover s does not own the data.
rs
let s = "Hello World";
let a = &s[3..5];  // a reference to "lo"
let b = &s[6..];   // a reference to "World"
let c = &b[1..3];  // a reference to "or"

TIP

  1. Readers with strong C background may be tempted to read the statements as "address of". However, using this (mis)interpretation, you would think that that c is a "double pointer", but semantically it is not.
  2. Interpreting a slice as a mechanism to borrow data helps you understand that a slice does not own the data.

Vec<T> vs. Array Slices

Once you understand Rust slice applied to a string, it should be easy to apply the idea to a vector (Vec<T>).

TIP

Both String and Vec<T> own the data which can change size. But string slices (&str) and vector slices (&[]) do not own the data.

rs
let s = "Smelter";                  // allocated on stack
let t = String::from("Smelter");    // allocated on heap, data may shrink/expand
let a = [3, 11, 71, 19];            // allocated on stack
let mut b:Vec<u32> = Vec::new();    // allocated on heap
// let mut b = Vec::<u32>::new();   // alternative syntax
b.push(3);
b.push(11);

For convenience, new() and push() calls above can be replaced with the vec! macro:

rs
let mut b = vec![3,11];

Slices of vectors are similar to slices of strings:

rs
let alloy_mix = vec![30, 21, 17, 22, 5, 13];
let mix1 = &alloy_mix[1..3];        // a reference to [21, 17]
let mix2 = &alloy_mix[3..];         // a reference to [22, 5, 13]
let mix2 = &mix2[0..2];             // a reference to [22, 5]

Rust Online Documentation

Two main hubs for online Rust documentations:

  1. The documentation of the Rust Standard Library
  2. The documentation of 3rd party libraries. In Rust parlance, libraries are known as crates.

In you have a poor internet connection, the standard library can be browsed locally on your computer by running the following command:

bash
rustup doc

The Standard Library

If you come from a Java background, the closest analogy the std crate is perhaps the Java java.lang package where you can find the foundation classes essential to the Java language itself (such as Boolean, Character, Float, Double, Class, Thread, etc.)

Important Types

Readers with Java or C++ background are familiar with the exception mechanism used by these languages to indicate "abnormal" return from a function call. Rust does not provide such exception handling mechanism. Instead, many Rust functions communicate their return value via two enum types: Result and Option.

Let's first understand some details about Rust enums.

Unlike C or C++ that defines enums as a collection of pure constants. Rust enums are closer to Java's where you can associate each enum member with extra data payload. When extra data provided, Java requires each enum have the same number and type of data. On the contrary, Rust enums are more flexible and allows each enum member (called variant) to have different payload.

In the following snippet the Polygon variant is associated with an unsigned integer representing the number of sides of the polygon. To extract the payload, you can either use if let or match:

rs
enum Shape {
    Circle,
    Polygon(u32)    // number of sides
}

fn main() {
    let s1 = Shape::Circle;
    // let s1 = Shape::Polygon(6);

    if let Shape::Polygon(n_sides) = s1 {
        println("It is a polygon with {} sides", n_sides)
    } else {
        println("It is a circle")
    }

    // behave like a switch statement but with more powerful pattern matching
    match s1 {
        Shape::Circle => {
            println!("It is a circle");
        }
        Shape::Polygon(n) => {
            println!("A polygon with {} sides", n)
        }
    }
}

The Result Enum

Instead of provide an explicit exception handling mechanism for function calls, Rust "wraps" function return values as an enum (Result) with two variants: Ok and Err. The C/Java enum that mimics Rust Result would look like:

java
enum Result { Ok, Err };

However, each of the enum variant above is associated with additional "payload".

  • The Ok variant includes the return result of the function
  • The Err variant includes the "thrown exception" (if it were in Java). But remember that Rust throws no exceptions!

In the Rust standard library, the Result enum is a generic enum parameterized by two types: the success type (T) and and the "exception"/error type (E):

rs
enum Result<T,E> {
    Ok(T),
    Err(E)
}

A Java method call with a try-catch block:

java
try {
    result = methodCall(....);
   // Snippet A: use result
}
catch (SomeException oops) {
  // Snippet B: use oops
}

would probably translate to the following Rust idiom:

rust
match (methodCall(...)) {
    Ok(result) => {
        // Snippet 1: use result
    }
    Err(oops) => {
        // Snippet 2: use oops
    }
}

TIP

Between the two generic arguments T and E, the error type E is usually defaulted to Error and for convenience Rust provides the following alias:

rs
type Result<T> == Result<T,Error>

Using the type alias, for instance, Result<u32> is equivalent to Result<u32,Error>.

The Option Enum

The other important type used by many Rust functions is the Option type, which is an elegant way to express some arbitrary value or absence of it. Similar to Result, this enum is also generic:

rust
enum Option<T> {
    Some(T),
    None
}

Readers with Python background may see the resemblance of Rust None to Python None. A Java method call that may return either some object or null:

java
out = methodCall();
if (out != null) {
    // Snippet 3: use out
} else {
    // Snippet 4: do other work
}

would probably translate to the following Rust idiom:

rust
match (methodCall()) {
    Some(out) => {
        // Snippet 3: use out
    }
    None => {
        // Snippet 4: do other work
    }
}

Using the std Crate

Readers with Java background understand how Java libraries are organized into packages. Some of the well-known packages include:

  • java.util that provides classes like: ArrayList, Date, Random, etc.
  • java.net that provides classes like: Socket, Proxy, etc.

Rust organizes its libraries using a similar hierarchical structure. However, it uses a slightly different terms: crates and modules. A crate with a large number of structs, functions, enums, is usually organized into of one or more module (and submodules). For instance, the std crate is organized into the following modules: alloc, array, collections, io, and many more.

TIP

Use the search box at the top of the documentation page to locate a specific function, struct, or enum

Typing stdout in the search box should give you three tabs: In Names, In Parameters, and In Return Types. Under the In Names tab you will find

  • std::io::Stdout: the documentation of the Stdout structure
  • std::io::stdout: the documentation of the stdout() function

Unlike Java where fully qualified classnames are delimited by the dot character (.), Rust uses double colon (::) as delimiters.

Let's use std::io to print some text instead of using println! macro. Out of the two names from the search result, we are interested in using the stdout() function which returns a new handle to the system stdout (described by the Stdout structure).

Another function of interest is write_all with the following signature:

rs
fn write_all(&mut self, buf: &[u8]) -> Result<()>
  • The self argument is similar to Python self which indicates that the function must be invoked from an object of type (struct) Stdout and we must supply an array of unsigned integer.
  • The return value is a Result enum with additional payload () (which is similar to Java/C void type)

Crates are brought into the scope of your Rust program via the use statement. Out first (failed) attempt would be to write the following snippet:

rs
use std::io;

fn main() {
    std::io::stdout().write_all("Hello World");
}

and the compiler will flag the error:

rs
io::stdout().write_all("Hello World");
             ^^^^^^^^^ method not found in `Stdout`

and also it gives suggestion how to fix by inserting use std::io::Write.

rs
use std::io;
use std::io::Write;

fn main() {
    std::io::stdout().write_all("Hello World");
}

The above code is not error free, this time the compiler suggest you replace "Hello World" with b"Hello World" (to change a string literal into a byte string).

rs
use std::io;
use std::io::Write;

fn main() {
    std::io::stdout().write_all(b"Hello World");
}

This time the code compiles with a warning: the return value Result was ignored and must be handled.

rs
use std::io;
use std::io::Write;

fn main() {
    let out = std::io::stdout().write_all(b"Hello World");
    if out.is_ok() {
        // Successfully written to stdout
    } else {
            // Unable to write to stdout

    }
}

more more idiomatic Rust style using pattern matching:

rs
use std::io;
use std::io::Write;

fn main() {
    match std::io::stdout().write_all(b"Hello World") {
        Ok(()) => {
            // Successfully written to stdout
        }
        Err(_error) => {
            // Unable to write to stdout
        }
    }

}

Using Third-Party Libraries

Other third-party libraries follow a similar organization structure and the main documentation hub for these libraries is https://docs.rs.

When browsing through the online documentation of a library, most of the time we want to look for a function to a specific task. The detailed documentation of a particular function usually leads you to relevant struct or enum used by the function.

Additional Resources

  1. Rust Tutorial for Beginners
  2. The official book: The Rust Programming Language