Skip to content

Rust

Rust Logo

Table of Contents

Part 1: Getting Started with Rust

1. Installation-(rustup)

Install Rust with rustup it's the official toolchain installer. You'll use it to manage Rust versions, components, and cross compilation targets.

Visit rustup.rs and follow the instructions. A typical installation on Linux or macOS is:

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

This will install rustc (the compiler), cargo (the build tool), rustup, and the standard library.

2. Cargo: The Rust Build Tool and Package Manager

cargo runs the Rust ecosystem. It creates projects, manages dependencies, builds, tests, and does pretty much everything else.

  • cargo new <project_name>: Creates a new binary (executable) project.
  • cargo new <project_name> --lib: Creates a new library project.
  • cargo build: Compiles the project in debug mode.
  • cargo build --release: Compiles the project with optimizations for production.
  • cargo run: Compiles and runs the project.
  • cargo check: Checks the code for errors without producing an executable. It's much faster than a full build and is great for quick feedback while you code.
  • cargo test: Runs the project's tests.

3. Hello, World! Anatomy of a Rust Program

Create a new project with Cargo:

cargo new hello_rust
cd hello_rust

This creates a src/main.rs file with the following content:

src/main.rs

// `fn` declares a new function.
// `main` is the special function where program execution begins.
fn main() {
    // `println!` is a macro that prints text to the console.
    // The `!` indicates that you're calling a macro, not a normal function.
    println!("Hello, world!");
}

Now, run it with Cargo:

cargo run

Output:

Hello, world!

Part 2: Rust Fundamentals

4. Variables, Mutability, and Shadowing

Variables in Rust are immutable by default. This is a safety feature. To make a variable mutable, you must use the mut keyword.

let x = 5; // Immutable
// x = 6; // This would be a compile time error!

let mut y = 10; // Mutable
y = 11; // This is fine.

// Shadowing: You can declare a new variable with the same name as a previous one.
let z = 5;
let z = z + 1; // This new `z` shadows the old one.
let z = z * 2;
println!("The value of z is: {}", z); // Prints 12

5. Data Types (Scalar and-Compound)

Rust is a statically typed language, but the compiler can usually infer the type you want to use.

  • Scalar Types:

    • Integers: i8, i32, i64, i128 (signed), u8, u32, u64, u128 (unsigned). isize and usize depend on the architecture (64-bit on a 64-bit system).
    • Floating Point: f32, f64.
    • Boolean: bool (true or false).
    • Character: char. Represents a single Unicode Scalar Value.
  • Compound Types:

    • Tuple: A fixed size collection of values of different types.
    • Array: A fixed size collection of values of the same type.
let an_integer: u32 = 42;
let a_float: f64 = 3.14;
let is_rust_fun = true;

// Tuple
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // Destructuring
println!("The value of y is: {}", y); // 6.4

// Array (fixed size, on the stack)
let a = [1, 2, 3, 4, 5];
let first = a[0];

6. Functions

Functions are declared with fn. Type annotations for parameters and return values are mandatory.

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

// Functions with a return value
fn five() -> i32 {
    5 // Note: No semicolon. This is an expression, and its value is returned.
}

fn plus_one(x: i32) -> i32 {
    x + 1 // This expression returns a value
}

7. Control Flow (if, loop, while,-for)

let number = 6;

// if expressions
if number % 4 == 0 {
    println!("number is divisible by 4");
} else if number % 3 == 0 {
    println!("number is divisible by 3");
} else {
    println!("number is not divisible by 4 or 3");
}

// loop: An infinite loop, broken with `break`
let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2; // `break` can return a value
    }
};
println!("The result is {}", result); // 20

// for loop: The best way to iterate over a collection
let a = [10, 20, 30, 40, 50];
for element in a {
    println!("the value is: {}", element);
}

Part 3: The Ownership System (Rust's-Superpower)

This is the most unique and important feature of Rust. It enables memory safety without a garbage collector.

8. Ownership: The Core Rules

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped (memory is freed).
{
    let s1 = String::from("hello"); // s1 is the owner of the string data

    // When we assign s1 to s2, the ownership is MOVED.
    // s1 is no longer valid after this line.
    let s2 = s1;

    // println!("{}", s1); // This would be a COMPILE-TIME ERROR!
    println!("{}", s2); // This is fine.

} // s2 goes out of scope here, and the string data is dropped.

This prevents double free errors. For simple types like integers that live on the stack, the data is copied instead of moved.

9. References and Borrowing

What if we want to use a value without taking ownership? We can borrow it using a reference.

  • &T: An immutable reference. You can have multiple immutable references to the same data.
  • &mut T: A mutable reference. You can only have one mutable reference to a particular piece of data in a particular scope.

The Rules of References: 1. At any given time, you can have either one mutable reference or any number of immutable references. 2. References must always be valid.

This system is how Rust prevents data races at compile time.

fn main() {
    let mut s = String::from("hello");

    // We can have multiple immutable borrows
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // We can have one mutable borrow
    let r3 = &mut s;
    change(r3);
    println!("{}", r3);
}

// This function takes a mutable reference to a String
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

10. Slices: A View Into Data

A slice lets you reference a contiguous sequence of elements in a collection rather than the whole collection.

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Part 4: Structuring Data

11. Structs: Custom Data Types

A struct is a custom data type that lets you package together and name multiple related values.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

// You can also associate functions with structs using an `impl` block.
impl User {
    // This is a method
    fn display_info(&self) {
        println!("User: {}, Email: {}", self.username, self.email);
    }

    // This is an associated function (like a static method)
    fn new(username: String, email: String) -> User {
        User {
            username,
            email,
            sign_in_count: 1,
            active: true,
        }
    }
}

let user1 = User::new(String::from("alice"), String::from("alice@example.com"));
user1.display_info();

12. Enums and the match Expression

Enums (enumerations) allow you to define a type by enumerating its possible variants. Rust enums are incredibly powerful because they can hold data.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

The match control flow operator is like a switch statement on steroids. It must be exhaustive, meaning you have to handle every possible variant of the enum.

fn route(ip: IpAddr) {
    match ip {
        IpAddr::V4(a, b, c, d) => {
            println!("Routing IPv4 address: {}.{}.{}.{}", a, b, c, d);
        }
        IpAddr::V6(addr) => {
            println!("Routing IPv6 address: {}", addr);
        }
    }
}

Part 5: Robust Error Handling

Rust does not have exceptions. Instead, it has two primary mechanisms for handling errors.

13. The Option Enum for Nullable Values

Rust does not have null. Instead, it has the Option<T> enum, which forces you to handle the case where a value might be absent. This prevents null pointer dereferencing bugs.

enum Option<T> {
    Some(T), // A value of type T is present
    None,    // No value is present
}

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

// You must handle both cases
match find_user(1) {
    Some(name) => println!("Found user: {}", name),
    None => println!("User not found."),
}

14. The Result Enum for Recoverable Errors

For operations that can fail (like file I/O or network requests), Rust uses the Result<T, E> enum.

enum Result<T, E> {
    Ok(T),  // Contains a success value of type T
    Err(E), // Contains an error value of type E
}

The ? operator is a convenient shortcut for propagating errors. If the result of an expression is Ok(value), the value is returned. If it's Err(e), the error e is returned from the entire function.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("username.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

15. panic!: For When Things Go Terribly Wrong

For unrecoverable errors, the panic! macro will stop execution, unwind the stack, and print an error message. This should be used rarely, for cases that indicate a bug in the program itself.

Part 6: The Rust Ecosystem

16. Crates and Cargo.toml

A crate is a package of Rust code. The Rust community shares open source crates on crates.io, the official registry.

You manage your project's dependencies in the Cargo.toml file.

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
# Add a crate by specifying its name and version
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

Running cargo build will automatically download and compile the specified dependencies.

17. Essential Crates

  • serde: The de facto standard for serializing and deserializing Rust data structures efficiently and safely (e.g., for JSON).
  • tokio: The most popular asynchronous runtime for writing fast, non blocking network applications.
  • reqwest: A convenient, high level HTTP client.
  • clap: A powerful and feature rich command line argument parser.
  • regex: The official regular expression library.
  • anyhow and thiserror: Crates that make error handling even more ergonomic.

Part 7: Security with Rust

18. Memory Safety by Default

This is Rust's headline feature. The ownership and borrowing rules are enforced by the compiler. This means that entire classes of common, severe security vulnerabilities are prevented at compile time:

  • Use After Free: The compiler knows when data is dropped and will not let you use a reference to it afterwards.
  • Double Free: The single owner rule means only one variable is responsible for freeing the data, preventing this bug entirely.
  • Buffer Overflows: Standard library collections like Vec and String are bounds checked. Accessing an invalid index will cause a controlled panic instead of leading to memory corruption.
  • Null Pointer Dereferencing: The Option enum forces you to handle the None case, making it impossible to dereference a null pointer by accident.

19. Fearless Concurrency

The same ownership system that provides memory safety also prevents data races. A data race occurs when multiple threads access the same memory location concurrently, and at least one of the accesses is a write.

Rust's Send and Sync traits, combined with the borrow checker, ensure that you cannot accidentally share state between threads in an unsafe way. If your code compiles, it is free of data races.

20. The unsafe Keyword: Opting Out of Guarantees

Rust provides the unsafe keyword as an escape hatch for situations where you need to perform operations that the compiler cannot verify as safe. This is most common when interfacing with C libraries or doing very low level systems programming.

let mut num = 5;

// Creating raw pointers is an unsafe operation
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

// Dereferencing raw pointers must be done in an unsafe block
unsafe {
    println!("r1 is: {}", *r1);
    *r2 = 6;
    println!("r2 is now: {}", *r2);
}

Security Implication: Any unsafe block in a Rust codebase is a signal to developers and auditors that this is a critical section where memory safety bugs could be introduced. It dramatically narrows the surface area that needs to be manually audited.

21. Safe Deserialization with serde

Unlike pickle in Python or ObjectInputStream in Java, Rust's primary deserialization library, serde, is safe by design. It deserializes data into strongly typed, pre defined structs. It does not have the ability to instantiate arbitrary types or execute code, which eliminates the risk of deserialization based RCE.

Part 8: Cookbook

22. Cookbook: A Simple Command Line grep Clone

This tool searches for a query string in a file. It demonstrates argument parsing, file I/O, and error handling.

Cargo.toml

[dependencies]
anyhow = "1.0"

src/main.rs

use std::env;
use std::fs;
use std::process;

fn main() -> anyhow::Result<()> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: minigrep <query> <filename>");
        process::exit(1);
    }

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for '{}' in file {}", query, filename);

    let contents = fs::read_to_string(filename)?;

    for line in contents.lines() {
        if line.contains(query) {
            println!("{}", line);
        }
    }

    Ok(())
}

23. Cookbook: A JSON API Client

This program uses reqwest and serde to fetch a post from the JSONPlaceholder API and deserialize it into a Rust struct.

Cargo.toml

[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] } # reqwest needs tokio

src/main.rs

use serde::Deserialize;

#[derive(Deserialize,-Debug)]
struct Post {
    #[serde(rename =-"userId")]
    user_id: i32,
    id: i32,
    title: String,
    body: String,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let url = "https://jsonplaceholder.typicode.com/posts/1";

    let post = reqwest::get(url)
        .await?
        .json::<Post>()
        .await?;

    println!("Fetched Post:");
    println!("Title: {}", post.title);
    println!("Body: {}", post.body);

    Ok(())
}

24. Cookbook: A Basic TCP Echo Server with Tokio

This example uses the tokio runtime to build a simple, asynchronous TCP server that echoes back any data it receives.

Cargo.toml

[dependencies]
tokio = { version = "1", features = ["full"] }

src/main.rs

use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Echo server listening on 127.0.0.1:8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        println!("Accepted connection from: {}", addr);

        tokio::spawn(async move {
            let mut buf = [0; 1024];

            loop {
                match socket.read(&mut buf).await {
                    Ok(0) => return, // Connection closed
                    Ok(n) => {
                        // Echo the data back to the client
                        if socket.write_all(&buf[0..n]).await.is_err() {
                            return; // Connection error
                        }
                    }
                    Err(_) => return, // Connection error
                }
            }
        });
    }
}