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:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shor 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:
Download Rustup for your platform (Linux, Windows 32/64-bit, OSX) to install the following development tools:
cargo: package managerrustc: Rust compilerrustfmt: source code formatterrustdoc: document generatorrustup: tool chain manager
Install the tool chain for your OS platform
bashrustup 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
- VSCode with rust-analyzer plugin (Extension ID
rust-lang.rust-analyzer) - CLion with Rust plugin
Setting Up A Project
Run cargo (from a command line) to create a new project directory
bashcargo new my-first-rust cd my-first-rustThe above step creates a new directory
my-first-rustwith the following files/directories:Cargo.toml: the project configuration filesrc/main.rs: a "Hello World" program in Rust
In addition
cargoalso initialized your project as a (local) git repository.Compile and run the program
bashcd my-first-rust # change into the project directory cargo build # compile the code cargo run # runAlthough you may directly use
rustcto compile your code, usingcargois more preferred since as your project becomes more complex and depends on third party libraries,cargowill 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.
let a = 71;
let b = String::from("Seventy one");For instance, in the above snippet:
- Both the variable
aand 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
newto 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()andfree() - Likewise, C++ developers depend on
newanddeletefor 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:
- Each data value (on the heap) has a unique owner any time
- 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
mainfunction goes out of scope - The string object holding the text
"Seventy One"is dropped after"Metal"is printed but before"Oxide"is printed
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
btoy) and invalidatingb's pointer. Each on of these micro operations costs O(1), regardless the size of the string text. Invalidatingb's pointer prevents double de-allocations that may happen in other languages.
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:
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 moveThe cause of the error:
- The ownership of string text "Seventy One" was moved from
btoy. Therefore whenprintlnattempts to printb's data, it is no longer owned byb. - Hence, an attempt to print (the data of)
bfailed
The principle of copy and move also applies to function calls and returns.
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 hereUpon 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
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 scopeWhen 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:
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 hereBorrow 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:
- When declaring the formal parameter in the function header
- When supplying the actual argument in the function invocation
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 hereTIP
Pay attention to the declaration b inside the last two main functions:
- Under "Own & Modify", the
mainis not required to declarebas mutable. This is becausemaindoes not alterb's data, theiron_workerdoes. - Under "Borrow & Modify", the
mainfunction must declarebas mutable. This is becauseiron_workermodifies data which is owned by a variable inmain.
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.
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:
cold_forge(&a, &mut a);
---------- -- ^^^^^^ mutable borrow occurs here
| |
| mutable borrow occurs here
immutable borrow later used by callUse Scope of Multiple Mutable References
To understand how "use" scope is checked by the Rust compiler, let's look at the following code:
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:
let r1 = &a;
-- immutable borrow occurs here
let r2 = &mut a;
^^^^^^ mutable borrow occurs here
cold_forge(r1, &mut b);
-- immutable borrow later used hereAmong 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.
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
r2as a mutable reference to data value ofatakes place after the first call ofcold_forge, thenr1is the only reference bound to data value ofawhen the first call ofcold_forgeexecutes - Similarly during the second call of
cold_forge,r2is the only (mutable) reference bound to the data value ofa.
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.
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 sliceSince slices can be built from another slice, the following snippet works as well with the following differences:
- In the above snippet the type of
sisStringwhich owns the text "Hello World" allocated on the heap - In the below snippet the type of
sis&strand the string text "Hello World" is allocated on the stack. Moreoversdoes not own the data.
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
- Readers with strong
Cbackground may be tempted to read the statements as "address of". However, using this (mis)interpretation, you would think that thatcis a "double pointer", but semantically it is not. - 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.
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:
let mut b = vec![3,11];Slices of vectors are similar to slices of strings:
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:
- The documentation of the Rust Standard Library
- 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:
rustup docThe 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:
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:
enum Result { Ok, Err };However, each of the enum variant above is associated with additional "payload".
- The
Okvariant includes the return result of the function - The
Errvariant 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):
enum Result<T,E> {
Ok(T),
Err(E)
}A Java method call with a try-catch block:
try {
result = methodCall(....);
// Snippet A: use result
}
catch (SomeException oops) {
// Snippet B: use oops
}would probably translate to the following Rust idiom:
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:
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:
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:
out = methodCall();
if (out != null) {
// Snippet 3: use out
} else {
// Snippet 4: do other work
}would probably translate to the following Rust idiom:
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.utilthat provides classes like:ArrayList,Date,Random, etc.java.netthat 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 theStdoutstructurestd::io::stdout: the documentation of thestdout()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:
fn write_all(&mut self, buf: &[u8]) -> Result<()>- The
selfargument is similar toPython selfwhich indicates that the function must be invoked from an object of type (struct)Stdoutand we must supply an array of unsigned integer. - The return value is a
Resultenum with additional payload()(which is similar to Java/Cvoidtype)
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:
use std::io;
fn main() {
std::io::stdout().write_all("Hello World");
}and the compiler will flag the error:
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.
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).
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.
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:
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
- Rust Tutorial for Beginners
- The official book: The Rust Programming Language