Manual Memory Management 101

Perfect. Let’s descend another layer down the stack — where things stop being “Rusty” and start being raw metal. Welcome to…


⚙️ Reinventing from Scratch: Building Vec<T> the Hard Way

Chapter 2 — “Manual Memory Management 101”

“We don’t allocate because we can. We allocate because the stack won’t stretch.”


🧭 2.1. Why We Need to Allocate

Our MyVec<T> is born empty — it has a null pointer, no capacity, no length.

That’s like having a grocery bag handle but no bag.

struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

If we want to push anything into it, we need a real memory region — a heap allocation. That’s what std::alloc is for.


🧩 2.2. Meet the Heap Wizard: std::alloc

Rust’s standard library gives us manual access to the global heap allocator — through four functions:

Function What it does
alloc(layout) Allocate uninitialized memory
dealloc(ptr, layout) Free previously allocated memory
realloc(ptr, old_layout, new_size) Resize an existing allocation
Layout::array::<T>(n) Compute size & alignment for n elements

⚠️ Unsafe Territory: These functions don’t initialize, drop, or track anything for you. They just hand you a raw pointer and expect you to play nice.


🧠 2.3. How Allocation Works in Rust

Let’s visualize what happens when we call alloc():

Before:
(ptr = null, len = 0, cap = 0)

alloc(Layout::array::<T>(4)) -> 0x7ff3...
 
After:
(ptr = 0x7ff3..., len = 0, cap = 4)

At this point:

  • You own a 4-slot memory region on the heap.
  • Each slot is size_of::<T>() bytes.
  • The contents are uninitialized garbage — touching them is UB until you write valid values.

🧰 2.4. Implementing with_capacity

Let’s add a proper constructor for MyVec:

use std::alloc::{alloc, Layout};

impl<T> MyVec<T> {
    pub fn with_capacity(capacity: usize) -> Self {
        // Compute memory layout
        let layout = Layout::array::<T>(capacity).unwrap();

        // Allocate uninitialized memory
        let ptr = unsafe { alloc(layout) as *mut T };

        // Handle allocation failure (rare, but possible)
        if ptr.is_null() {
            panic!("Allocation failed");
        }

        MyVec {
            ptr,
            len: 0,
            cap: capacity,
        }
    }
}

We now have a working “empty bag” — it owns a region of heap memory that can hold capacity elements of type T.


⚠️ Sidenote: Uninitialized ≠ Empty

At this stage, our allocated memory exists, but it’s not safe to read from it yet.

unsafe {
    let x = *myvec.ptr; // ❌ Undefined Behavior!
}

You must initialize it first using pointer writes like:

unsafe {
    *myvec.ptr = 42;
}

Think of it like:

“The room exists, but the furniture is floating in quantum limbo until you move something in.”


🧩 2.5. Writing Elements to the Heap

Let’s make a push() function.

impl<T> MyVec<T> {
    pub fn push(&mut self, value: T) {
        // Need to grow?
        if self.len == self.cap {
            self.grow();
        }

        unsafe {
            // Write the new element at ptr + len
            std::ptr::write(self.ptr.add(self.len), value);
        }

        self.len += 1;
    }
}

Key idea:

  • std::ptr::write() writes without reading (avoids dropping old garbage).
  • ptr.add(self.len) moves the pointer len elements forward.
  • After writing, we safely increase len.

💣 2.6. Growing the Vector

To grow capacity (double it, just like real Vec does):

use std::alloc::{realloc, Layout};

impl<T> MyVec<T> {
    fn grow(&mut self) {
        let new_cap = if self.cap == 0 { 4 } else { self.cap * 2 };
        let new_layout = Layout::array::<T>(new_cap).unwrap();

        unsafe {
            let new_ptr = if self.cap == 0 {
                alloc(new_layout)
            } else {
                let old_layout = Layout::array::<T>(self.cap).unwrap();
                realloc(self.ptr.cast(), old_layout, new_layout.size())
            } as *mut T;

            if new_ptr.is_null() {
                panic!("Reallocation failed");
            }

            self.ptr = new_ptr;
            self.cap = new_cap;
        }
    }
}

🧨 Warning: If T has destructors (e.g. String, Vec, etc.), reallocation must preserve initialized values — otherwise, you’ll double-free or leak memory. Rust’s real Vec has deep logic to ensure that no Drop runs mid-resize.


🧹 2.7. Cleaning Up (Drop)

We must free our heap memory when MyVec goes out of scope.

use std::alloc::dealloc;

impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        // Drop each element manually
        for i in 0..self.len {
            unsafe {
                std::ptr::drop_in_place(self.ptr.add(i));
            }
        }

        // Free the heap memory
        if self.cap != 0 {
            let layout = Layout::array::<T>(self.cap).unwrap();
            unsafe {
                dealloc(self.ptr.cast(), layout);
            }
        }
    }
}

Now our custom vector doesn’t leak memory — it drops each element and then releases the heap.

Congrats! You’ve just built a safe (enough) heap-backed dynamic array in Rust!


🔍 2.8. Visual Recap

Before push:
Stack: [ ptr=null, len=0, cap=0 ]
Heap:  []

After push(42):
Stack: [ ptr=0x7ff3..., len=1, cap=4 ]
Heap:  [42, ?, ?, ?]

After drop:
Heap: (deallocated)

🧠 2.9. Chapter Takeaways

Concept Purpose
alloc, dealloc, realloc Manual heap management
Layout Memory alignment and size descriptor
ptr.add(i) Safe pointer arithmetic (under unsafe)
std::ptr::write() Initializes uninitialized memory
Drop Explicit resource cleanup

⚠️ “Unsafe code is not evil. It’s powerful — and power must be handled with paranoia.”


🧩 Next Up → Chapter 3: Owning Raw Pointers We’ll dive into why Vec doesn’t use Box<T> internally, how ownership works with *mut T, and what it takes to make raw pointers behave safely.


Would you like me to continue to Chapter 3: Owning Raw Pointers — with Rustonomicon-style commentary and unsafe callouts next?