Skip to content
Go back

Rust Basics - Memory

Published:  at  08:22 PM

Welcome to the first post in my Rust Basics series! This collection explores the Rust programming language through explanations and practical examples. I’m documenting my learning journey while creating a resource that might help others understand Rust’s powerful features. While I’ve used AI to expand some examples and clarify technical details, all content has been carefully reviewed for accuracy.

Memory model

Rust follows a compile-time memory management model with ownership, borrowing, and lifetimes. It enforces strict rules at compile time to prevent memory errors (like use-after-free, double-free, data races).

Ownership: Each value has a single owner, and ownership can be transferred.

Borrowing: References (&T, &mut T) allow temporary access without ownership.

Lifetimes: Ensure references are always valid.

Threading

Rust is multi-threaded and enforces thread safety through ownership, borrowing rules, and Send/Sync traits.

Heap vs Stack vs Where Else?

Stack

fn example_stack() {
    let x = 42;                 // Integer stored on stack
    let y = true;               // Boolean stored on stack
    let z = [1, 2, 3, 4, 5];    // Fixed-size array stored on stack
}  // All values automatically deallocated when they go out of scope

Heap

fn example_heap() {
    let s = String::from("hello");  // String data stored on heap
    let v = vec![1, 2, 3];          // Vector contents stored on heap
    let b = Box::new(42);           // Boxed integer stored on heap
}  // automatically frees heap memory when owners go out of scope

Where else?

Static/Global Memory

// Compile-time constant
const MAX_USERS: usize = 100;

// Static variable with 'static lifetime
static PROGRAM_NAME: &str = "My Rust App";

// Mutable static (requires unsafe)
static mut COUNTER: u32 = 0;

fn use_static() {
    // Using const and immutable static is safe
    println!("Max users: {}, Program: {}", MAX_USERS, PROGRAM_NAME);
    
    // Modifying mutable static requires unsafe block
    unsafe {
        COUNTER += 1;
        println!("Counter: {}", COUNTER);
    }
}

Thread-Local Storage (TLS)

use std::cell::Cell;
use std::thread;

thread_local! {
    static THREAD_COUNTER: Cell<u32> = Cell::new(0);
}

fn thread_local_example() {
    THREAD_COUNTER.with(|counter| {
        counter.set(counter.get() + 1);
        println!("Thread counter: {}", counter.get());
    });
    
    // Each thread has its own independent copy
    thread::spawn(|| {
        THREAD_COUNTER.with(|counter| {
            println!("New thread counter: {}", counter.get()); // Will be 0
            counter.set(100);
        });
    }).join().unwrap();
}

Memory-Mapped Files/Regions

Usage of mmap files

  1. Performance: Memory mapping can be more efficient than traditional file I/O because:

    • It reduces copying between kernel and user space
    • It enables OS-level page caching
    • It allows accessing only the parts of a file that you need
  2. Convenience: Access file data using simple memory operations rather than read/write calls

  3. Shared memory: Multiple processes can share the same mapped region for efficient inter-process communication

Real-world use cases:

use memmap2::MmapOptions;
use std::fs::File;
use std::io::Result;

fn memory_map_example() -> Result<()> {
    // Open a file
    let file = File::open("data.bin")?;
    
    // Create a read-only memory map
    let mmap = unsafe { MmapOptions::new().map(&file)? };
    
    // Access it like normal memory - the OS loads pages on demand
    if mmap.len() > 20 {
        println!("First 20 bytes: {:?}", &mmap[0..20]);
    }
    
    // Random access is efficient - this might be gigabytes into the file
    // but only the needed pages are loaded
    if mmap.len() > 1_000_000 {
        println!("Byte at position 1M: {}", mmap[1_000_000]);
    }
    
    // The memory map is automatically unmapped when dropped
    Ok(())
}

Example: Shared memory between processes

use memmap2::{MmapMut, MmapOptions};
use std::fs::{File, OpenOptions};
use std::io::Result;

fn shared_memory_example() -> Result<()> {
    // Create or open a file to back the shared memory
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open("shared.bin")?;
    
    // Set the file size
    file.set_len(1024)?;
    
    // Create a writable memory map
    let mut mmap = unsafe { MmapOptions::new().map_mut(&file)? };
    
    // Write to the shared memory
    mmap[0] = 42;
    
    // Another process could open the same file and see these changes
    // Or we can create another map to the same file:
    let reader_map = unsafe { MmapOptions::new().map(&file)? };
    assert_eq!(reader_map[0], 42);
    
    Ok(())
}

Kernel-Space Memory

// Example using mmap syscall (simplified)
#[cfg(unix)]
fn kernel_mem_example() {
    use std::ptr;
    use libc::{mmap, PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANONYMOUS, munmap};
    
    unsafe {
        // Request 1024 bytes of memory from kernel
        let size = 1024;
        let addr = mmap(
            ptr::null_mut(),
            size,
            PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS,
            -1,
            0
        );
        
        // Use the memory...
        
        // Free the memory when done
        munmap(addr, size);
    }
}

Memory layouf of Box, Vector and String

Box

Box<T> is a simple heap allocation - it stores a single value of type T on the heap.

fn box_example() {
    // Memory layout:
    // Stack: Pointer (8 bytes) → Heap: i32 (4 bytes)
    let boxed_int = Box::new(42);
    
    // Stack: Pointer (8 bytes) → Heap: Person struct (size depends on fields)
    struct Person { name: String, age: u32 }
    let boxed_person = Box::new(Person { 
        name: "Alice".to_string(), 
        age: 30 
    });
    
    // When boxed_int goes out of scope, both the stack pointer
    // and the heap memory it points to are freed
}

Vec

Vec<T> has a more complex memory layout with three components stored on the stack:

fn vector_example() {
    // Empty vector:
    // Stack: ptr (8 bytes), capacity (8 bytes), length (8 bytes)
    // Heap: Nothing yet (no allocation until elements are added)
    let mut v: Vec<i32> = Vec::new();
    
    // After pushing elements:
    // Stack: Same three fields
    // Heap: Contiguous block with elements [10, 20, 30]
    v.push(10);
    v.push(20);
    v.push(30);
    
    // Capacity management
    println!("Length: {}, Capacity: {}", v.len(), v.capacity());
    
    // When capacity is exceeded, Vec allocates a larger block,
    // copies the elements, and frees the old block
    for i in 0..10 {
        v.push(i);
        println!("Added {}, Capacity: {}", i, v.capacity());
    }
}

String

String is actually a wrapper around Vec<u8> that guarantees UTF-8 encoding:

fn string_example() {
    // Empty string:
    // Stack: ptr (8 bytes), capacity (8 bytes), length (8 bytes)
    // Heap: No allocation yet
    let mut s = String::new();
    
    // After assignment:
    // Stack: Same three fields
    // Heap: UTF-8 bytes for "Hello"
    s = String::from("Hello");
    
    // Adding to the string might cause reallocation
    s.push_str(", world!");
    
    // String slice (&str) vs String:
    let slice: &str = &s[0..5]; // "Hello"
    // slice is just two values on stack: pointer to s's buffer + length (no heap allocation)
    
    // Multi-byte characters
    let multi = String::from("");
    println!("Character: {}, Bytes: {}", multi, multi.len()); // len() returns 3 (bytes), not 1
    
    // When String goes out of scope, the heap memory is freed
}

RAII and how Rust implements it

RAII (Resource Acquisition Is Initialization) is a programming idiom where resource management is tied to object lifetime. Resources are acquired during initialization and released during destruction.

Rust fully embraces RAII through its ownership system:

  1. Memory Safety: Prevents leaks and use-after-free errors
  2. Deterministic Cleanup: Resources are freed at predictable points
  3. Exception Safety: Resources are released even if errors occur
  4. Simpler Code: No manual resource tracking or explicit cleanup calls

Basic RAII with File:

fn read_file() -> std::io::Result<()> {
    // File is automatically opened when created
    let file = std::fs::File::open("example.txt")?;
    
    // Use the file...
    
    // No need to close explicitly - file is automatically closed
    // when it goes out of scope at the end of the function
    Ok(())
}

Custom RAII with Drop trait:

struct DatabaseConnection {
    conn_string: String,
}

impl DatabaseConnection {
    fn new(conn_string: &str) -> Self {
        println!("Opening connection to {}", conn_string);
        // In real code: actual connection would be established here
        DatabaseConnection {
            conn_string: conn_string.to_string(),
        }
    }
}

impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        // Cleanup code runs automatically when object is dropped
        println!("Closing connection to {}", self.conn_string);
        // In real code: actual connection would be closed here
    }
}

fn main() {
    {
        let db = DatabaseConnection::new("localhost:5432");
        // Use the connection...
    } // db goes out of scope here, Drop::drop is automatically called
    
    println!("Connection has been closed");
}

RAII with smart pointers:

fn managed_resources() {
    // Box automatically frees heap memory
    let heap_data = Box::new([1, 2, 3, 4]);
    
    // Mutex automatically unlocks when guard goes out of scope
    let counter = std::sync::Mutex::new(0);
    {
        let mut guard = counter.lock().unwrap();
        *guard += 1;
    } // Lock is automatically released here
    
    // Arc/Rc automatically decrease reference counts and free when zero
    let shared = std::sync::Arc::new(String::from("shared data"));
    
    // All resources are automatically freed at function end
}


Previous Post
Rust Basics - Types and Data Structures
Next Post
Building Web API's With Rust 2