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 pointerlenelements 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
Thas destructors (e.g.String,Vec, etc.), reallocation must preserve initialized values — otherwise, you’ll double-free or leak memory. Rust’s realVechas deep logic to ensure that noDropruns 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?