Building on Box and RawVec


title: “Using NonNull in Box and Vec Implementations” meta_description: “Refactor Box and RawVec to use NonNull for clearer invariants and better safety in Rust.” keywords: [“rust box nonnull”, “rawvec nonnull”, “vec internals”, “smart pointers rust”]

🏗️ Reinventing from Scratch: NonNull<T>

Chapter 6 — “Building on Box and RawVec”

“Replace *mut T with NonNull<T> and watch your invariants become clearer.”


🧭 6.1. Before and After

Box with Raw Pointer

pub struct MyBox<T> {
    ptr: *mut T,  // Could be null!
}

impl<T> MyBox<T> {
    pub fn new(value: T) -> Self {
        // What if allocation returns null?
        let ptr = unsafe { alloc(...) as *mut T };
        // Need to check!
        if ptr.is_null() {
            panic!("Allocation failed");
        }
        // ...
    }
}

Box with NonNull

pub struct MyBox<T> {
    ptr: NonNull<T>,  // Never null!
}

impl<T> MyBox<T> {
    pub fn new(value: T) -> Self {
        let ptr = unsafe {
            let p = alloc(...) as *mut T;
            NonNull::new(p).expect("Allocation failed");
            // Or use new_unchecked if alloc never returns null
        };
        // Clearer: ptr is guaranteed non-null
        // ...
    }
}

Benefits:

  • Invariant is type-level (not just documentation)
  • No need to check null in every method
  • Optimizer can make better assumptions

🎯 6.2. RawVec with NonNull

pub struct RawVec<T> {
    ptr: NonNull<T>,
    cap: usize,
}

impl<T> RawVec<T> {
    pub fn new() -> Self {
        Self {
            ptr: NonNull::dangling(),  // Zero-capacity!
            cap: 0,
        }
    }
    
    pub fn with_capacity(cap: usize) -> Self {
        if cap == 0 {
            return Self::new();
        }
        
        let layout = Layout::array::<T>(cap).unwrap();
        
        let ptr = unsafe {
            let p = alloc(layout) as *mut T;
            NonNull::new(p).expect("Allocation failed")
        };
        
        Self { ptr, cap }
    }
    
    pub fn ptr(&self) -> *mut T {
        self.ptr.as_ptr()
    }
}

📦 6.3. Vec with NonNull

pub struct MyVec<T> {
    buf: RawVec<T>,  // Uses NonNull internally
    len: usize,
}

impl<T> MyVec<T> {
    pub fn new() -> Self {
        Self {
            buf: RawVec::new(),  // ptr is dangling (non-null!)
            len: 0,
        }
    }
    
    pub fn push(&mut self, value: T) {
        if self.len == self.buf.cap {
            self.buf.grow();
        }
        
        unsafe {
            // NonNull makes this clearer
            let ptr = self.buf.ptr().add(self.len);
            ptr.write(value);
        }
        
        self.len += 1;
    }
    
    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }
        
        unsafe {
            // Safe: index < len, ptr is valid
            Some(&*self.buf.ptr().add(index))
        }
    }
}

✨ 6.4. Benefits of NonNull

Before: Defensive Null Checks

pub fn get(&self, index: usize) -> Option<&T> {
    if self.ptr.is_null() {  // Redundant if well-designed!
        return None;
    }
    // ...
}

After: Clearer Intent

pub fn get(&self, index: usize) -> Option<&T> {
    // No null check needed — NonNull guarantees it!
    // Just check bounds
    if index >= self.len {
        return None;
    }
    // ...
}

🎨 6.5. Dangling vs Null

// Zero-capacity Vec
pub struct MyVec<T> {
    ptr: NonNull<T>,  // Dangling when cap == 0
    len: usize,
    cap: usize,
}

// When empty:
// ptr = NonNull::dangling()  ← Non-null but invalid to deref
// len = 0
// cap = 0

// Key insight: We never deref when len == 0!
// So dangling pointer is safe

🧠 6.6. Chapter Takeaways

Refactoring to NonNull gives us:

  • Type-level invariants (can’t be null)
  • Clearer code (no defensive null checks)
  • Better optimizations (compiler knows pointer is valid)
  • Safer APIs (intent is explicit)

🚀 6.7. What’s Next

Next: Chapter 7 — Slices, Fat Pointers, and Struct Embedding

Learn how NonNull works with:

  • NonNull<[T]> (fat pointers with length)
  • NonNull<dyn Trait> (fat pointers with vtable)
  • Embedded pointers in structs

💡 Exercises

  1. Refactor Box to use NonNull
  2. Refactor Vec/RawVec to use NonNull
  3. Compare codegen of *mut T vs NonNull (use cargo-show-asm)
  4. Measure performance difference (should be zero!)
  5. Build a linked list with Option<NonNull>

Next: Chapter 7 — Slices, Fat Pointers, and Struct Embedding 📦