Rust
Rust
Rust
Part - I
- by Engr. Anjum Nazir
Table of Contents
How to Install Rust .......................................................................................................................... 3
How to Update Rust ........................................................................................................................ 3
How to Uninstall Rust ..................................................................................................................... 3
Cargo ............................................................................................................................................... 3
Creating a Project with Cargo ......................................................................................................... 3
What is TOML.............................................................................................................................. 4
Crates .............................................................................................................................................. 4
How to Build and Run a Project with Cargo.................................................................................... 5
Build a Project with Cargo........................................................................................................... 5
Run Project with Cargo ............................................................................................................... 5
Common Build Options ............................................................................................................... 6
Cargo.lock File ............................................................................................................................. 6
Goal of Rust: Safety......................................................................................................................... 8
Dangling pointers ........................................................................................................................ 8
Attempting to Create Dangling Pointer .................................................................................... 10
Rust preventing a race condition .............................................................................................. 13
Rust Attempting to Block Buffer Overflow ............................................................................... 16
Rust Attempting to modify an iterator while iterating over it ................................................. 19
Goal of Rust: Control..................................................................................................................... 20
Rust: Multiple ways to create integer values ........................................................................... 20
Pointer vs Smart Pointer ............................................................................................................... 23
Pointer....................................................................................................................................... 23
Smart Pointer ............................................................................................................................ 23
Acknowledgement ........................................................................................................................ 24
2
How to Install Rust
$ sudo ./rust_setup.sh
Cargo
• Cargo is Rust programming language build system and package manager.
• It can be used to:
o Builds your code.
o Download and build the libraries and relevant dependencies.
Cargo will generate two files and one directory for us:
3
$ tree hello_world/
hello_world/
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
What is TOML
TOML stands for Tom’s Obvious, Minimum Language. It is generally used to store configuration
data like JSON file format, however it is designed to be simple and human readable. It
comprises of (i) sections or tables and (ii) key-value pairs use to store information.
Cargo.toml file is used to store project’s metadata, such as the
1. Project’s name
2. Its version, and
3. Its dependencies.
$ cd hello_world
$ cat Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
[dependencies]
• The first line, [package], is a section heading that indicates that the following statements
are configuring a package.
Crates
In Rust, packages are referred to as crates.
4
How to Build and Run a Project with Cargo
$ cargo build
Compiling hello_world v0.1.0 (/root/rust-work/hello_world)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
The above command generated unoptimized binary file with debugging information.
1. Unoptimized
• Unoptimized means that the Rust compiler (via Cargo) does not apply optimizations that
would make the code run faster or use fewer resources. In the development phase, the
focus is on quick compilation times, not on the performance of the compiled code.
• Optimizations typically occur in the release build (triggered by cargo build --release),
which can take more time to compile but produces faster, more efficient binaries. In
development mode, optimizations are skipped to allow for faster feedback during iterative
development.
2. Debuginfo
• Debuginfo refers to the additional metadata added to the compiled binary to help with
debugging. This metadata includes information like variable names, line numbers, and
function call stacks, making it easier to debug the code using tools such as a debugger
(e.g., gdb or Rust's built-in debugging tools).
• The presence of debug information can make the binary larger, but it is essential for
debugging purposes during development.
$ cargo run
5
Common Build Options
$ cargo --help
Commands:
build, b Compile the current package
check, c Analyze the current package and report errors, but don't
build object files
clean Remove the target directory
doc, d Build this package's and its dependencies' documentation
new Create a new cargo package
init Create a new cargo package in an existing directory
add Add dependencies to a manifest file
remove Remove dependencies from a manifest file
run, r Run a binary or example of the local package
test, t Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this package to the registry
install Install a Rust binary
uninstall Uninstall a Rust binary
... See all commands with --list
Cargo.lock File
After compilation you will find Cargo.lock file in the project root. Cargo.lock is a file that
specifies the exact version numbers of all the dependencies so that future builds are reliably
built the same way until Cargo.toml is modified.
6
Listing 1.1
1 fn greet_world() {
2 println!("Hello, world!"); // <1>
3
4 let southern_germany = "Grüß Gott!"; // <2>
5 let japan = "ハロー・ワールド"; // <3>
6
7 let regions = [southern_germany, japan]; // <4>
8
9 for region in regions.iter() { // <5>
10 println!("{}", ®ion); // <6>
11 }
12 }
13
14 fn main() {
15 greet_world(); // <7>
16 }
Rust is able to include a wide range of characters. Strings are guaranteed to be encoded as UTF-
8
Rust also brings contemporary developer tools to the systems programming world:
• Cargo, the included dependency / package manager and build tool, that makes adding,
compiling, and managing dependencies painless and in a consistent way across the Rust
ecosystem.
• The Rust Language Server powers Integrated Development Environment (IDE) integration
for code completion and inline error messages.
If you want to stick to a standard style across Rust projects, you can use an automatic formatter
tool called rustfmt to format your code in a particular style. The Rust team has included this
7
tool with the standard Rust distribution, like rustc, so it should already be installed on your
computer!
Control vs Safety
Rust is a systems programming language that runs blazingly fast, prevents segfaults, and
guarantees thread safety. Rust’s distinguishing feature as a programming language is its ability
to prevent invalid data access at compile time.
Dangling pointers
Dangling pointers are pointers in programming that point to a memory location that has
already been freed or deallocated. In other words, the pointer refers to a memory address that
is no longer valid or accessible, but the pointer itself still exists and holds that now-invalid
memory reference.
8
How Dangling Pointers Occur:
1. Memory Deallocation:
o A dangling pointer can occur when memory is freed or deallocated (e.g., using
free() in C/C++ or dropping a resource in Rust), but a pointer to that memory still
exists and hasn’t been updated to reflect that the memory is no longer valid.
o Example in C:
int *ptr = (int *)malloc(sizeof(int)); // Allocate memory
*ptr = 42; // Use the memory
free(ptr); // Deallocate memory
*ptr = 50; // Dangling pointer! Memory is freed, but ptr still
points to it
2. Out-of-Scope Pointers:
• A dangling pointer can also occur when a local variable goes out of scope, but a pointer
or reference to that variable is still retained elsewhere in the program.
• Example in C:
int* danglingPointer() {
int x = 10;
return &x; // Returning pointer to local variable
}
9
free(ptr);
ptr = NULL;
• Rust’s Ownership Model: In Rust, the ownership and borrowing system is designed to
prevent dangling pointers by ensuring that memory is always valid when accessed.
Summary
A dangling pointer occurs when a pointer still references memory that has been deallocated or
is otherwise invalid, leading to potential crashes, data corruption, or security issues. Proper
memory management is crucial to avoid such problems.
fn main() {
let mut grains: Vec<Cereal> = vec![];
grains.push(Cereal::Rye);
drop(grains);
println!("{:?}", grains);
}
Code Explanation
#[derive(Debug)]
This attribute #[derive(Debug)] is used to automatically implement the Debug trait for the
Cereal enum.
The Debug trait allows the values of the enum to be printed using the {:?} format
specifier in println!.
10
enum Cereal {
Barley, Millet, Rice,
Rye, Spelt, Wheat,
}
Here, an enum called Cereal is defined with six variants: Barley, Millet, Rice, Rye, Spelt, and
Wheat.
Enums in Rust are used to represent a type that can hold one of several defined values (in this
case, different types of cereals).
fn main() {
let mut grains: Vec<Cereal> = vec![];
grains.push(Cereal::Rye);
The push method adds Cereal::Rye (one of the enum variants) to the grains vector.
drop(grains);
The drop function is called to explicitly drop the grains vector. In Rust, drop is used to manually
deallocate or clean up resources before the variable goes out of scope. After this line, grains is
no longer valid, and its memory has been released.
println!("{:?}", grains);
This line attempts to print the grains vector using the {:?} format specifier, which relies on the
Debug trait that was automatically derived for the Cereal enum . However, this line will result
in a compile-time error because grains was explicitly dropped, making it invalid to use here.
{:?}
This line attempts to print the grains vector using the {:?} format specifier, which relies on the
Debug trait that was automatically derived for the Cereal enum.
In Rust, when you use println! to print a value, you often need to specify how the value should
be formatted. The {:?} format specifier is used for "debug formatting," which is a way to output
a value in a developer-friendly format (useful for debugging purposes). To use {:?}, the type of
the value being printed must implement the Debug trait.
11
The Debug Trait:
The Debug trait in Rust allows types (such as enums, structs, or other custom types) to be
printed in a way that is suitable for debugging. By default, custom types like enums (e.g.,
Cereal) don’t implement the Debug trait, so you can't directly print them using {:?}.
However, by using the #[derive(Debug)] attribute, Rust automatically generates the
necessary implementation of the Debug trait for your type. This means that your type can now
be printed with the {:?} format specifier.
This line attempts to print the grains vector, which is a Vec<Cereal>. Since Vec<T> implements
the Debug trait (when T also implements Debug), Rust can print the entire vector in a readable
format.
Thanks to the #[derive(Debug)] for Cereal, each variant of the Cereal enum (e.g., Rye) can be
printed using {:?}. For instance, if grains contains Cereal::Rye, the output would look like:
[Rye]
If the Debug trait hadn't been derived for Cereal, you would have encountered a compilation
error saying that Cereal does not implement Debug
Summary
12
• #[derive(Debug)] automatically provides the Debug trait for a type, allowing it to be
printed using {:?}.
• In this case, the Cereal enum has #[derive(Debug)], allowing the grains vector (which
holds Cereal values) to be printed in a readable format.
ch1/ch1-race/src/main.rs file.
fn main() {
let mut data = 100;
println!("{}", data);
}
Code Explanation
Closure
• In Rust, a closure is an anonymous function that can capture variables from its surrounding
environment.
• Closures are similar to functions, but with some important distinctions, such as their ability
to capture and use variables from the scope in which they are defined.
• They are often used for short-lived tasks like passing as arguments to higher-order
functions, performing operations in a thread, or working with iterators.
For Example
let closure_name = |parameter1, parameter2| {
// closure body
};
13
let x = 5;
let closure = |y| x + y; // The closure captures `x` by borrowing.
println!("{}", closure(3)); // Output: 8
use std::thread;
fn main() {
let x = 10;
handle.join().unwrap();
}
handle.join().unwrap(); in Rust is used to wait for a spawned thread to finish and handle
any errors.
• handle.join():
• .unwrap():
o Extracts the Ok value from the Result. If the thread panicked (i.e., the Result is
Err), unwrap() will cause the program to panic and terminate.
In short, this line waits for the thread to finish and ensures that any thread failure causes a
panic in the main program.
Issues
1. Closure Capture Rules (Thread Safety): The variable data is declared in the main thread,
but the closures inside the thread::spawn calls attempt to modify data. In Rust,
accessing or modifying variables across threads requires special care due to potential
data races.
14
o By default, closures capture variables by reference if possible (i.e., borrowing),
but Rust does not allow shared access to mutable data across threads without
synchronization.
o To safely share and modify data between threads, Rust requires you to use
thread-safe types, such as Arc (atomic reference counting) and Mutex (mutual
exclusion lock).
o These ensure that the data is accessed in a controlled, synchronized manner.
2. Ownership and Borrowing: The spawned threads run in parallel to the main thread, but
data is owned by the main thread. Once a thread attempts to access or modify data, it
would require either borrowing (with a reference) or moving the ownership of data into
the closure. Since you can't safely share mutable state across threads without
synchronization, the compiler prevents this code from compiling.
3. Non-deterministic Execution Order: Even if the threading issues were solved, the order
in which threads execute is non-deterministic. So even if both threads could modify
data, it would be impossible to predict whether the value would end up as 500 or 1000.
To allow safe mutable access to data from multiple threads, you can use an Arc<Mutex<T>> for
thread-safe access. Here's an example of how to fix this code:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(100));
handle1.join().unwrap();
handle2.join().unwrap();
println!("{}", *data.lock().unwrap());
}
15
Explanation of the Fix
1. Arc (Atomic Reference Counting): Arc (Atomic Reference Counting) is used to allow
multiple ownership of the data across threads. Each thread gets a clone of the Arc,
which keeps track of the number of references.
2. Mutex (Mutual Exclusion): Mutex ensures that only one thread can access or modify
the data at a time, preventing race conditions. When a thread wants to modify data, it
locks the Mutex. Other threads will wait until the lock is released.
3. Thread Join: Each thread is joined using handle.join().unwrap() to ensure that the main
thread waits for the spawned threads to finish before printing the result.
With this fix, the program compiles and runs safely, allowing shared mutable access to data
across threads.
fn main() {
let fruit = vec![' ', ' ', ' '];
This Rust code is designed to demonstrate a runtime error known as a "buffer overflow" (or
more specifically, an index out of bounds error). Let’s break it down:
<1> This line attempts to access the element at index 4 of the fruit vector. However, the vector
only has 3 elements (0, 1, and 2), so index 4 does not exist. This will cause a runtime error
called an "index out of bounds" error.
In Rust, when you try to access an invalid index in a vector, the program will panic and
terminate the execution to prevent undefined behavior.
assert_eq!(buffer_overflow, ' ') // <2>
<2> This line uses an assertion to check whether the variable buffer_overflow is equal to
' '. However, this line will never be reached because the program would have already
panicked due to the out-of-bounds access on the previous line (fruit[4]).
16
SOLUTION
fn main() {
let fruit = vec![' ', ' ', ' '];
-----------------------------------------------------------------------------
This is an example of pattern matching with the if let construct, and it works as follows:
• fruit.get(4):
o The get() method on a vector (fruit) tries to access an element at a
specific index (in this case, 4).
o Unlike directly indexing with fruit[4], which would panic if the index is out of
bounds, get() returns an Option type:
▪ If the index is valid (within the vector's bounds), it returns Some(&T),
where T is the type of the element.
▪ If the index is out of bounds, it returns None.
• if let Some(buffer_overflow):
This line essentially checks if the vector contains an element at index 4. If it does, the block is
executed. If not, it is skipped.
17
Line <2>: assert_eq!(*buffer_overflow, ' ');
This line performs an assertion to check whether the value at the index (if it exists) is equal to
' '.
• assert_eq!:
o This is a macro that compares two values and ensures they are equal. If they are
not equal, it will cause the program to panic with an error message.
o assert_eq!(a, b) checks if a == b, and if this is false, the program panics
with a message showing the two values.
• *buffer_overflow:
o The buffer_overflow variable contains a reference to the element in the
vector because fruit.get(4) returns an Option<&T>, where &T is a
reference to the value.
o The * operator is used to dereference buffer_overflow, retrieving the actual
value stored at that index (in this case, a character, such as ' ').
o So, *buffer_overflow gives the value of the element, which is then compared
to ' '.
Summary
1. The if let checks if there is an element at index 4 in the fruit vector.
2. If the element exists (Some(buffer_overflow)), the code inside the block is executed.
3. Inside the block, assert_eq! checks if the value at index 4 is equal to ' '.
o If they are equal, the program continues normally.
o If they are not equal, the program panics with an error message.
o If the index 4 does not exist, the block is skipped entirely, and the program does not
panic.
Some
• Some is a variant of the Option enum and represents the presence of a value.
• It is used to wrap a value that might or might not exist, alongside None, which represents
the absence of a value.
• Option and its variants (Some and None) help handle situations where a value may or may
not be available without panicking.
18
Rust Attempting to modify an iterator while iterating over it
ch1/ch1-letters/src/main.rs.
fn main() {
let mut letters = vec![ // <1>
"a", "b", "c"
];
The above code will fail to compile because Rust does not allow the variable to be letters
modified within the iteration block
Pushing Inside the Loop:
• The code tries to push letter.clone() back into the letters vector.
• The .clone() method creates a copy of letter, which the code attempts to push into the
vector.
• However, this will cause a runtime panic because you are modifying (mutating) the
vector letters while iterating over it, which is not allowed in Rust for safety reasons.
• In Rust, you cannot have both mutable and immutable borrows of the same data at
the same time. This leads to a conflict, and the program will panic to prevent undefined
behavior.
19
Solution:
If you need to modify the vector while iterating over it, you can either:
1. Iterate over a copy of the vector, so that the original vector can be mutated without a
conflict.
2. Use an index-based loop, which doesn't require borrowing the entire vector and allows
you to modify it as you loop through it.
fn main() {
let mut letters = vec!["a", "b", "c"];
let mut i = 0;
while i < letters.len() {
println!("{}", letters[i]);
letters.push(letters[i].clone()); // Safely push a clone of the
current letter
i += 1; // Increment index
}
}
fn main() {
let a = 10;
let b = Box::new(20);
let c = Rc::new(Box::new(30));
let d = Arc::new(Mutex::new(40));
println!("a: {:?}, b: {:?}, c: {:?}, d: {:?}", a, b, c, d);
}
Let's go through the Rust code step by step, explaining the concepts used such as Box, Rc, Arc,
and Mutex, and how they work together.
20
Code Breakdown:
use std::rc::Rc;
use std::sync::{Arc, Mutex};
• Arc: Short for "Atomic Reference Counted," Arc is similar to Rc but designed for multi-
threaded contexts. It is thread-safe and uses atomic operations to manage the
reference count.
• Mutex: A "Mutual Exclusion" lock that ensures only one thread can access the data at a
time, making it safe to mutate data in a multi-threaded environment.
Atomic Operation
• An atomic operation is a low-level, indivisible operation that is performed in a single step
without the possibility of interference from other operations or threads.
• It is guaranteed to be executed fully or not at all, ensuring that no other thread can observe
the operation in a partially completed state.
• In multi-threaded programs, atomic operations are crucial for preventing race conditions
when multiple threads access or modify shared data.
• Atomic operations are often used in synchronization primitives like mutexes and atomic
reference counting (e.g., Arc in Rust).
Variables Explanation:
1. let a = 10;:
o a is a simple integer variable. It's stored on the stack, as primitive types like i32
are typically stack-allocated in Rust.
2. let b = Box::new(20);:
o Box is a smart pointer that allocates data on the heap instead of the stack.
o Box::new(20) creates a new box that stores the integer 20 on the heap. The
value of b is the Box pointer, while the actual data (20) is stored on the heap.
21
o Purpose of Box: Used to heap-allocate values, especially when working with
types that must be stored on the heap (e.g., recursive data structures).
3. let c = Rc::new(Box::new(30));:
o Rc stands for reference counting, and it enables multiple ownership of the same
data. Here, c is a reference-counted smart pointer to a Box that holds the integer
30.
o Rc::new(Box::new(30)) creates a reference-counted pointer (Rc) that manages a
heap-allocated Box containing the value 30.
o Purpose of Rc: Useful when multiple parts of a program need to share ownership
of the same data, but only within a single thread (as Rc is not thread-safe).
4. let d = Arc::new(Mutex::new(40));:
The println! macro prints the values of a, b, c, and d using the {:?} format specifier,
which is used for debug printing.
• a is printed as the simple value 10.
• b is a Box that contains the value 20. It prints as Box(20).
• c is an Rc that wraps a Box. The output shows the reference count (strong=1 because
there's only one reference) and the inner value (Box(30)).
• d is an Arc that contains a Mutex. It shows the reference count (strong=1), and inside
the Mutex is the value 40.
22
Example Output:
a: 10, b: 20, c: Rc(Box(30)), d: Arc(Mutex { data: 40 })
Summary:
• a: A simple integer value, stored on the stack.
• b: A Box smart pointer, heap-allocating the integer 20.
• c: An Rc pointer with a reference to a Box, allowing shared ownership of the Box
(containing 30) within a single thread.
• d: An Arc pointer wrapping a Mutex, allowing multiple threads to share and mutate a
protected integer (40) in a thread-safe manner.
Pointer
A pointer is a variable that holds the memory address of another value. In languages like C or
C++, pointers are used to reference data stored in memory. In Rust, raw pointers (*const T and
*mut T) are used, but they are unsafe and require manual management.
• Where it's stored: The pointer itself is stored on the stack (because it’s just an address),
but the data it points to could be on the heap or stack depending on how the memory is
allocated.
Smart Pointer
A smart pointer is a data structure that, in addition to holding a memory address, it manages
the lifetime, ownership, and access to the data it points to. In Rust, examples of smart pointers
are Box, Rc, and Arc.
• Box: Allocates data on the heap.
• Rc/Arc: Provide reference counting and share data across multiple owners. The Rc/Arc
pointer is stored on the stack, while the data they reference is stored on the heap.
• Mutex: Can wrap data for thread-safe access, often combined with Arc for shared
ownership across threads.
23
Storage:
• Pointer: The pointer is on the stack, but the data it references can be on either the stack
or the heap.
• Smart Pointer: The smart pointer itself is on the stack, but the data it manages is
typically on the heap.
Acknowledgement
The code samples are taken from book ‘Rust in Action’.
24