Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust FAQ: Memory Management

PART08 -- Memory Management

Q32: Does drop destroy the reference or the referenced data?

In Rust, the drop function (from the Drop trait) is used to clean up the referenced data (the actual value a variable owns), not the reference itself. When a value goes out of scope, Rust automatically calls its drop method (if implemented) to free resources associated with the value, like memory or file handles. References (&T or &mut T) don’t own data, so they don’t get “destroyed” by drop; they just stop being valid when their scope ends.

For example:

struct Resource {
    name: String,
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("Dropping resource: {}", self.name);
    }
}

fn main() {
    let resource = Resource { name: String::from("Data") };
    let reference = &resource; // Reference to resource
    println!("Using reference: {}", reference.name);
    // When `resource` goes out of scope, `drop` is called on `resource`, not `reference`
} // Prints: Dropping resource: Data
  • Key Points:
    • drop runs on the owning value (e.g., resource), not references to it.
    • References are just temporary “views” and don’t have their own Drop implementation.
    • Rust ensures the referenced data is only dropped when all owners and borrows are out of scope, thanks to its ownership rules.

This keeps memory management safe and predictable, avoiding issues like double-free errors.

Q33: Can I use C's free() on pointers allocated with Rust's Box?

No, you cannot safely use C’s free() on memory allocated with Rust’s Box. Here’s why:

  • Box is Rust’s way of allocating memory on the heap, managed by Rust’s memory allocator (usually the system allocator, like jemalloc or malloc). When a Box goes out of scope, Rust automatically deallocates the memory using the same allocator.
  • C’s free() expects memory allocated by C’s malloc(). Using free() on a Box pointer could cause undefined behavior, like crashes or memory corruption, because the allocators might be different or have incompatible bookkeeping.
  • Rust’s Box also ensures memory safety through ownership rules, which C’s free() bypasses, potentially breaking Rust’s guarantees.

What to do instead:

  • Let Box handle deallocation automatically when it goes out of scope:
    fn main() {
        let b = Box::new(42); // Allocates on heap
        // No need to free; `b` is dropped automatically when scope ends
    }
  • If you need to pass a Box to C code, convert it to a raw pointer with Box::into_raw, but you must manage it carefully and use Rust’s Box::from_raw to reclaim it, not free().

Example with FFI:

use std::ptr;

fn main() {
    let b = Box::new(42);
    let raw_ptr = Box::into_raw(b); // Get raw pointer
    // Pass `raw_ptr` to C code (hypothetical)
    // Later, reclaim in Rust instead of using C's free()
    let reclaimed = unsafe { Box::from_raw(raw_ptr) };
    // `reclaimed` drops automatically
}

Using C’s free() on Box is unsafe and unnecessary since Rust handles deallocation for you.

Q34: Why should I use Box or Rc instead of C's malloc()?

Rust’s Box and Rc (Reference Counted) are safer, more idiomatic alternatives to C’s malloc() for heap allocation. Here’s why you should use them:

  • Safety:

    • Box ensures memory is automatically freed when it goes out of scope, thanks to Rust’s ownership model. With malloc(), you must manually call free(), risking memory leaks or double-free errors.
    • Rc tracks how many references exist to shared data and frees it only when the last reference is gone, preventing dangling pointers.
    • Both integrate with Rust’s borrow checker, ensuring safe access to memory.
  • Ease of Use:

    • Box::new(value) is simpler than malloc(size) plus manual pointer management.
    • Rc provides shared ownership without manual reference counting, unlike C where you’d track pointers yourself.
  • Type Safety:

    • Box<T> and Rc<T> are typed, so you know exactly what data they hold (e.g., Box<i32>). malloc() returns a generic void*, which can lead to type errors.
    • Rust’s compiler checks ensure you don’t misuse pointers.
  • Example:

    use std::rc::Rc;
    
    fn main() {
        let boxed = Box::new(42); // Heap-allocated integer
        println!("Boxed value: {}", boxed); // Auto-freed at scope end
    
        let shared = Rc::new(42); // Shared ownership
        let shared2 = Rc::clone(&shared); // Increase reference count
        println!("Shared value: {}", shared); // Freed when last Rc is dropped
    }

    In C, you’d use malloc(sizeof(int)), cast the pointer, and manually call free(), which is error-prone.

  • Performance: Box and Rc use Rust’s allocator (often the same as C’s), so they’re just as fast but safer.

  • When to use:

    • Use Box for single-owner heap data (e.g., large structs or recursive types).
    • Use Rc for multiple owners in single-threaded code (use Arc for multi-threaded).

Using malloc() in Rust requires unsafe code and loses Rust’s safety guarantees, so stick with Box or Rc unless you’re interfacing with C.

Q35: Why doesn't Rust have a realloc() equivalent?

Rust doesn’t have a direct equivalent to C’s realloc() (which resizes a previously allocated memory block) because Rust’s memory management prioritizes safety and abstraction, and realloc()’s behavior doesn’t fit neatly into Rust’s ownership model. Here’s why:

  • Safety Concerns: realloc() can move memory, invalidating pointers, which is risky in Rust’s strict ownership and borrowing system. Rust prefers explicit, safe operations over low-level memory manipulation.
  • Higher-Level Abstractions: Rust’s standard library provides types like Vec and String for dynamic resizing, which handle reallocation internally. These types are safer and more convenient than manual realloc().
  • Allocator API: Rust’s allocator API (in std::alloc) allows custom memory management, but it’s low-level and unsafe. Most Rust code doesn’t need realloc() because Vec and similar types cover common use cases.

What to use instead:

  • Use Vec for resizable arrays:
    #![allow(unused)]
    fn main() {
    let mut vec = vec![1, 2, 3];
    vec.push(4); // Internally resizes if needed
    vec.resize(10, 0); // Resizes to length 10, filling with 0
    }
    Vec automatically handles reallocation, growing or shrinking as needed.
  • For custom needs, you can use std::alloc::realloc in unsafe code, but it’s rarely necessary:
    #![allow(unused)]
    fn main() {
    use std::alloc::{alloc, dealloc, Layout, realloc};
    
    unsafe {
        let layout = Layout::new::<i32>();
        let ptr = alloc(layout); // Allocate
        let ptr = realloc(ptr, layout, 2 * layout.size()); // Reallocate
        dealloc(ptr, layout); // Free
    }
    }

Why no direct realloc()?

  • Rust’s design favors safe, high-level abstractions like Vec over low-level, error-prone functions.
  • realloc()’s behavior (e.g., copying data or returning null) doesn’t align with Rust’s ownership rules.
  • Most resizing needs are covered by Vec, String, or other collections, making a standalone realloc() less useful.

Q36: How do I allocate/unallocate an array in Rust?

In Rust, the idiomatic way to allocate and deallocate an array (a dynamic sequence of elements) is to use Vec<T>, which manages a resizable array on the heap. Here’s how it works:

Allocation:

  • Create a Vec using vec![], Vec::new(), or Vec::with_capacity():
    #![allow(unused)]
    fn main() {
    let mut numbers = vec![1, 2, 3]; // Allocate array with 3 elements
    let mut empty = Vec::new(); // Empty array, grows as needed
    let mut preallocated = Vec::with_capacity(10); // Pre-allocate space for 10 elements
    }

Adding Elements:

  • Use push to add elements, which may trigger reallocation if the capacity is exceeded:
    #![allow(unused)]
    fn main() {
    numbers.push(4); // Adds 4, may resize internally
    }

Deallocation:

  • Rust automatically deallocates a Vec when it goes out of scope, thanks to the Drop trait:
    fn main() {
        let numbers = vec![1, 2, 3];
        // Use numbers...
    } // `numbers` is automatically deallocated here
  • You can also explicitly drop a Vec early with drop():
    #![allow(unused)]
    fn main() {
    let mut numbers = vec![1, 2, 3];
    drop(numbers); // Explicitly deallocate
    }

Fixed-size arrays: For fixed-size arrays (not resizable), use [T; N]:

#![allow(unused)]
fn main() {
let fixed: [i32; 3] = [1, 2, 3]; // Stack-allocated, no manual deallocation needed
}

These are deallocated automatically when out of scope, but they’re not dynamic like Vec.

Why use Vec?

  • Safe: Handles memory allocation and deallocation for you.
  • Dynamic: Can grow or shrink as needed.
  • Type-safe: Ensures all elements are of type T.

For low-level control, you can use std::alloc in unsafe code, but Vec is almost always the better choice.

Q37: What happens if I forget to use Vec when deallocating an array?

In Rust, if you’re working with a dynamic array, you typically use Vec for allocation and deallocation. If you “forget” to use Vec and instead work with raw pointers (e.g., via std::alloc or C’s malloc), you risk serious issues because Rust’s safety guarantees don’t apply. Here’s what could happen:

  • Using Raw Pointers Without Vec: If you allocate an array with std::alloc or malloc and forget to deallocate it, you get a memory leak:

    #![allow(unused)]
    fn main() {
    use std::alloc::{alloc, Layout};
    
    unsafe {
        let layout = Layout::array::<i32>(10).unwrap();
        let ptr = alloc(layout); // Allocate array
        // Forgot to call `dealloc(ptr, layout)`!
    } // Memory is leaked
    }
  • If you try to deallocate incorrectly (e.g., using C’s free() on a non-malloc pointer or mismatching Layout), you get undefined behavior, like crashes or corruption.

  • Fixed-size Arrays ([T; N]): If you use a fixed-size array like [i32; 10], Rust handles deallocation automatically when the array goes out of scope. Forgetting to “use Vec” isn’t an issue here since these arrays are stack-allocated and don’t need manual deallocation.

  • Forgetting Vec for Dynamic Arrays: If you meant to use Vec but instead used raw pointers or another method, you lose:

    • Automatic deallocation (Rust won’t clean up raw pointers).
    • Safety checks (e.g., bounds checking for array access).
    • Resizing capabilities (raw pointers don’t grow like Vec).

Example of Correct Use with Vec:

#![allow(unused)]
fn main() {
let mut vec = vec![1, 2, 3]; // Safe allocation
// Use vec...
// No need to deallocate; `vec` is dropped automatically
}

What to do:

  • Always use Vec for dynamic arrays unless you have a specific reason (e.g., FFI with C).
  • If using raw pointers, ensure proper deallocation with std::alloc::dealloc in unsafe code, but this is rare.
  • Rust’s compiler will catch most mistakes if you stick to Vec.

Forgetting Vec and using raw pointers incorrectly can lead to leaks or crashes, so Vec is the safe, idiomatic choice.

Q38: What's the best way to define a NULL-like constant in Rust?

Rust doesn’t use NULL like C because it avoids null pointer errors through its type system. Instead, the idiomatic way to represent “no value” is to use the Option<T> enum, which has two variants: Some(T) (a value) and None (no value). Defining a NULL-like constant is rarely needed, but here’s how to handle it:

Using Option<T>:

#![allow(unused)]
fn main() {
let no_value: Option<i32> = None; // `None` is Rust’s "NULL"-like concept
let some_value: Option<i32> = Some(42);

match no_value {
    Some(value) => println!("Got: {}", value),
    None => println!("No value!"), // Like NULL
}
}

Defining a Constant: If you need a NULL-like constant for a specific type (e.g., for FFI or a specific use case), you can define a constant using Option:

const NO_VALUE: Option<i32> = None;

fn main() {
    let value = NO_VALUE;
    if value.is_none() {
        println!("No value present!"); // Prints: No value present!
    }
}

For Raw Pointers (FFI): If you’re working with C code and need a NULL-like constant for raw pointers, use std::ptr::null or std::ptr::null_mut:

const NULL_PTR: *const i32 = std::ptr::null();

fn main() {
    if NULL_PTR.is_null() {
        println!("This is a null pointer!");
    }
}

Why Option is best:

  • Safety: Option<T> forces you to handle the None case explicitly, avoiding null pointer dereference errors.
  • Clarity: None clearly indicates “no value” in a type-safe way.
  • Idiomatic: Rust codebases use Option universally, making your code easier to understand.

Avoid:

  • Defining a custom NULL-like constant (e.g., const NULL: i32 = 0) unless absolutely necessary for C interop, as it bypasses Rust’s safety.
  • Using raw pointers (*const T or *mut T) unless required for FFI, as they’re unsafe and error-prone.

Best Practice: Use Option::None for most cases. For raw pointers in FFI, use std::ptr::null() or std::ptr::null_mut(). This keeps your code safe and idiomatic.