title: “NonNull Invariants and Safety Guarantees in Rust”
meta_description: “Understanding NonNull’s safety guarantees, invariants, and what it does NOT promise in Rust unsafe code.”
keywords: [“rust nonnull safety”, “nonnull invariants”, “pointer safety rust”, “nonnull guarantees”]
🛡️ Reinventing from Scratch: NonNull<T>
Chapter 2 — “Invariants and Safety”
“NonNull promises one thing and one thing only: this pointer is not null. Everything else is on you.”
🧭 2.1. The Single Promise
NonNull<T> has exactly one invariant:
// INVARIANT: The wrapped pointer is never null
let ptr: NonNull<T> = ...;
assert!(!ptr.as_ptr().is_null()); // Always true
That’s it. That’s the whole promise.
❌ 2.2. What NonNull Does NOT Guarantee
❌ NOT: The Pointer is Valid
let ptr = NonNull::dangling(); // Valid NonNull!
// But reading from it → UB!
❌ NOT: Memory is Initialized
let layout = Layout::new::<i32>();
let ptr = unsafe {
let raw = alloc(layout) as *mut i32;
NonNull::new_unchecked(raw)
};
// ptr is non-null, but memory is uninitialized!
unsafe {
let x = *ptr.as_ptr(); // ❌ UB! Reading uninit memory
}
❌ NOT: Pointer is Aligned
let bytes = [0u8; 8];
let ptr = NonNull::new(bytes.as_ptr() as *mut u64).unwrap();
// Non-null? Yes!
// Aligned for u64? Maybe not! (depends on byte array alignment)
unsafe {
let x = *ptr.as_ptr(); // Might be UB if misaligned!
}
❌ NOT: You Have Permission to Dereference
fn foo(ptr: NonNull<i32>) {
// Just because you have a NonNull doesn't mean you can read it!
// Need to check lifetime/provenance
unsafe {
let x = *ptr.as_ptr(); // Might be UB!
}
}
✅ 2.3. What NonNull DOES Give You
✅ Optimizer Hints
fn process(ptr: Option<NonNull<T>>) {
match ptr {
Some(p) => { /* use p */ }
None => { /* handle null */ }
}
}
// Compiler knows: if Some, pointer is definitely non-null
// Can optimize null-checks away
✅ Niche Optimization
assert_eq!(
size_of::<Option<NonNull<u8>>>(),
size_of::<*mut u8>()
);
// Option<NonNull<T>> is same size as a raw pointer!
// The None case uses the null bit pattern
✅ Better API Design
// Before: Unclear if null is allowed
fn process(ptr: *mut T) { }
// After: Clear that null is forbidden
fn process(ptr: NonNull<T>) { }
🎯 2.4. The Safety Contract
When you create a NonNull<T>, you promise:
pub fn new(ptr: *mut T) -> Option<NonNull<T>> {
// Checks if null, returns None if so
if ptr.is_null() {
None
} else {
Some(unsafe { NonNull::new_unchecked(ptr) })
}
}
pub unsafe fn new_unchecked(ptr: *mut T) -> NonNull<T> {
// SAFETY: Caller must ensure ptr is not null
NonNull { pointer: ptr }
}
Your responsibility:
- Ensure pointer came from valid allocation
- Ensure alignment is correct
- Ensure you have permission to access the memory
NonNull’s responsibility:
- Ensure pointer is not null
🧩 2.5. Using NonNull Safely
Pattern 1: After Allocation
let layout = Layout::new::<i32>();
let ptr = unsafe { alloc(layout) as *mut i32 };
let ptr = NonNull::new(ptr)
.expect("Allocation failed"); // Handles null case
// Now we know ptr is non-null!
// But still need to initialize before reading
unsafe {
ptr.as_ptr().write(42); // Initialize
let x = ptr.as_ptr().read(); // Now safe to read
}
Pattern 2: From Reference
let value = 42;
let ptr = NonNull::from(&value); // Always succeeds!
// References are never null, so this is safe
unsafe {
assert_eq!(*ptr.as_ptr(), 42);
}
Pattern 3: In Data Structures
struct MyVec<T> {
ptr: NonNull<T>, // Never null!
len: usize,
cap: usize,
}
impl<T> MyVec<T> {
pub fn new() -> Self {
Self {
ptr: NonNull::dangling(), // Non-null but don't use
len: 0,
cap: 0,
}
}
}
⚠️ 2.6. Common Mistakes
Mistake 1: Assuming Valid Memory
let ptr: NonNull<i32> = NonNull::dangling();
// ❌ WRONG: NonNull doesn't mean initialized!
unsafe {
let x = *ptr.as_ptr(); // UB!
}
Mistake 2: Ignoring Alignment
let bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let ptr = NonNull::new(bytes.as_ptr() as *mut u64).unwrap();
// ❌ WRONG: Misaligned!
unsafe {
let x = *ptr.as_ptr(); // UB on most platforms!
}
Mistake 3: Outliving the Allocation
fn bad() -> NonNull<i32> {
let x = 42;
NonNull::from(&x) // ❌ Returns pointer to local variable!
}
let ptr = bad();
// ptr is non-null, but points to freed stack memory!
🧠 2.7. Chapter Takeaways
| Guarantee | NonNull | Raw Pointer |
|---|---|---|
| Non-null | ✅ Yes | ❌ No |
| Valid address | ❌ No | ❌ No |
| Initialized | ❌ No | ❌ No |
| Aligned | ❌ No | ❌ No |
| Permission to access | ❌ No | ❌ No |
| Niche optimization | ✅ Yes | ❌ No |
Key point: NonNull is a better raw pointer, but still requires unsafe code to use!
🚀 2.8. What’s Next
We understand what NonNull promises (and doesn’t promise).
Next: Chapter 3 — API Tour
Learn all the NonNull methods:
- Construction (new, new_unchecked, from)
- Conversion (as_ptr, as_ref, as_mut)
- Arithmetic (add, offset, etc.)
💡 Exercises
- Create NonNull from an allocation and verify it’s non-null
- Test Option<NonNull
> size optimization - Write a function that safely dereferences NonNull
- Implement a Vec-like struct using NonNull instead of *mut
- Use Miri to catch invalid NonNull usage
Next: Chapter 3 — API Tour: Construction and Conversion 🔧