How do I work with NonNull in Rust?

Walkthrough

NonNull is a wrapper around a raw pointer *mut T that guarantees the pointer is never null. It's a building block for safe abstractions over raw pointers, commonly used when implementing custom smart pointers, collections, and FFI bindings.

Key characteristics:

  • Never null — Guaranteed to be non-null, enabling compiler optimizations
  • Covariant — NonNull<&'a T> can be used where NonNull<&'static T> is expected
  • *mut T equivalent — Same size and layout as a raw pointer
  • Unsafe construction — Creating requires unsafe or validation
  • Send/Sync opt-out — Like raw pointers, not auto Send/Sync

When to use NonNull:

  • Implementing custom smart pointers (Box, Rc, Arc)
  • Building collection types (Vec, LinkedList)
  • FFI with non-null pointer guarantees
  • Optimizing Option<NonNull> to pointer size

Code Examples

Basic NonNull Usage

use std::ptr::NonNull;
 
fn main() {
    let mut value = 42;
    
    // Create NonNull from a reference (safe because reference is non-null)
    let ptr: NonNull<i32> = NonNull::from(&mut value);
    
    // Access the value
    unsafe {
        println!("Value: {}", *ptr.as_ref());
    }
    
    // Modify the value
    unsafe {
        *ptr.as_mut() = 100;
    }
    
    println!("Modified: {}", value);
}

Creating NonNull Pointers

use std::ptr::NonNull;
 
fn main() {
    let mut value = 42i32;
    
    // From reference (safe)
    let ptr1: NonNull<i32> = NonNull::from(&value);
    println!("From ref: {:?}", ptr1);
    
    // From raw pointer (unsafe - must verify non-null)
    let raw_ptr: *mut i32 = &mut value;
    let ptr2: NonNull<i32> = unsafe { NonNull::new_unchecked(raw_ptr) };
    println!("From raw: {:?}", ptr2);
    
    // From raw pointer (safe - returns Option)
    let ptr3: Option<NonNull<i32>> = NonNull::new(raw_ptr);
    println!("From raw (checked): {:?}", ptr3);
    
    // From dangling (for ZSTs or sentinel values)
    let dangling: NonNull<i32> = NonNull::dangling();
    println!("Dangling: {:?}", dangling);
}

NonNull with Option Optimization

use std::ptr::NonNull;
 
fn main() {
    // Option<NonNull<T>> has the same size as *const T
    // Because None is represented as a null pointer
    
    let mut value = 42;
    let ptr: Option<NonNull<i32>> = Some(NonNull::from(&mut value));
    let none: Option<NonNull<i32>> = None;
    
    println!("Size of Option<NonNull<i32>>: {}", std::mem::size_of::<Option<NonNull<i32>>>());
    println!("Size of *const i32: {}", std::mem::size_of::<*const i32>());
    // Both are 8 bytes on 64-bit!
    
    // Can pattern match safely
    match ptr {
        Some(p) => println!("Got pointer: {:?}", p),
        None => println!("No pointer"),
    }
}

Implementing a Smart Pointer with NonNull

use std::ptr::NonNull;
use std::ops::Deref;
 
struct MyBox<T> {
    ptr: NonNull<T>,
}
 
impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        // Box::into_raw returns *mut T, which is non-null after allocation
        let ptr = NonNull::new(Box::into_raw(Box::new(value)))
            .expect("Box::new should never return null");
        Self { ptr }
    }
    
    fn into_inner(self) -> T {
        let ptr = self.ptr;
        std::mem::forget(self); // Don't run Drop
        unsafe { *Box::from_raw(ptr.as_ptr()) }
    }
}
 
impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        unsafe { self.ptr.as_ref() }
    }
}
 
impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(self.ptr.as_ptr()));
        }
    }
}
 
fn main() {
    let boxed = MyBox::new(String::from("Hello, World!"));
    println!("Value: {}", *boxed);
    
    let inner = boxed.into_inner();
    println!("Extracted: {}", inner);
}

NonNull with Covariance

use std::ptr::NonNull;
 
// NonNull is covariant in T
// This means NonNull<&'a T> can be used where NonNull<&'static T> is expected
 
fn main() {
    fn accept_static(_: NonNull<&'static str>) {}
    
    let mut value = "hello";
    let ptr: NonNull<&str> = NonNull::from(&mut value);
    
    // This works because NonNull is covariant
    // (In practice, this example is simplified - lifetimes are checked at compile time)
    println!("Covariance demonstrated");
}

Building a Linked List

use std::ptr::NonNull;
 
struct Node<T> {
    value: T,
    next: Option<NonNull<Node<T>>>,
}
 
struct LinkedList<T> {
    head: Option<NonNull<Node<T>>>,
    len: usize,
}
 
impl<T> LinkedList<T> {
    fn new() -> Self {
        Self { head: None, len: 0 }
    }
    
    fn push(&mut self, value: T) {
        let node = Box::into_raw(Box::new(Node {
            value,
            next: self.head,
        }));
        
        self.head = NonNull::new(node);
        self.len += 1;
    }
    
    fn pop(&mut self) -> Option<T> {
        self.head.map(|node| {
            let node = unsafe { Box::from_raw(node.as_ptr()) };
            self.head = node.next;
            self.len -= 1;
            node.value
        })
    }
    
    fn len(&self) -> usize {
        self.len
    }
    
    fn iter(&self) -> Iter<T> {
        Iter {
            next: self.head.map(|n| n.as_ptr()),
        }
    }
}
 
struct Iter<'a, T> {
    next: *const Node<T>,
}
 
impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.next.is_null() {
            None
        } else {
            unsafe {
                let node = &*self.next;
                self.next = node.next.map(|n| n.as_ptr()).unwrap_or(std::ptr::null());
                Some(&node.value)
            }
        }
    }
}
 
impl<T> Drop for LinkedList<T> {
    fn drop(&mut self) {
        while self.pop().is_some() {}
    }
}
 
fn main() {
    let mut list = LinkedList::new();
    
    list.push(1);
    list.push(2);
    list.push(3);
    
    println!("Length: {}", list.len());
    
    for val in list.iter() {
        println!("Value: {}", val);
    }
    
    while let Some(val) = list.pop() {
        println!("Popped: {}", val);
    }
}

NonNull for FFI

use std::ptr::NonNull;
use std::ffi::c_void;
 
// Many C APIs guarantee non-null pointers
// NonNull helps encode this in the type system
 
// C function: void process_data(void* data) - data must not be null
fn process_data_ffi(data: NonNull<c_void>) {
    // Safe: C function expects non-null pointer
    unsafe {
        // simulate_c_process_data(data.as_ptr());
        println!("Processing data at {:?}", data);
    }
}
 
fn main() {
    let mut data = vec![1u8, 2, 3, 4];
    let ptr = NonNull::new(data.as_mut_ptr() as *mut c_void).unwrap();
    
    process_data_ffi(ptr);
}

NonNull Methods

use std::ptr::NonNull;
 
fn main() {
    let mut value = 42i32;
    let ptr: NonNull<i32> = NonNull::from(&mut value);
    
    // as_ptr: Get *mut T
    let raw: *mut i32 = ptr.as_ptr();
    println!("Raw pointer: {:?}", raw);
    
    // as_ref: Get &T (unsafe)
    let reference: &i32 = unsafe { ptr.as_ref() };
    println!("Reference: {}", reference);
    
    // as_mut: Get &mut T (unsafe)
    let mut_ref: &mut i32 = unsafe { ptr.as_mut() };
    *mut_ref = 100;
    println!("Modified: {}", value);
    
    // cast: Convert NonNull<T> to NonNull<U>
    let bytes: NonNull<u8> = ptr.cast();
    println!("As bytes: {:?}", bytes);
}

NonNull with Arrays

use std::ptr::NonNull;
 
fn main() {
    let mut arr = [1, 2, 3, 4, 5];
    
    // Get NonNull to array
    let ptr: NonNull<[i32; 5]> = NonNull::from(&mut arr);
    
    // Get NonNull to first element
    let first: NonNull<i32> = unsafe {
        NonNull::new_unchecked(arr.as_mut_ptr())
    };
    
    // Iterate through array using NonNull
    let mut current = first;
    for i in 0..5 {
        unsafe {
            println!("Element {}: {}", i, *current.as_ref());
            current = NonNull::new_unchecked(current.as_ptr().add(1));
        }
    }
}

NonNull in Option-like Structures

use std::ptr::NonNull;
 
struct MaybePresent<T> {
    ptr: Option<NonNull<T>>,
}
 
impl<T> MaybePresent<T> {
    fn new() -> Self {
        Self { ptr: None }
    }
    
    fn set(&mut self, value: T) {
        let ptr = NonNull::new(Box::into_raw(Box::new(value)))
            .expect("Allocation should not fail");
        self.ptr = Some(ptr);
    }
    
    fn get(&self) -> Option<&T> {
        self.ptr.map(|p| unsafe { p.as_ref() })
    }
    
    fn take(&mut self) -> Option<T> {
        self.ptr.take().map(|p| unsafe {
            *Box::from_raw(p.as_ptr())
        })
    }
}
 
impl<T> Drop for MaybePresent<T> {
    fn drop(&mut self) {
        if let Some(ptr) = self.ptr {
            unsafe {
                drop(Box::from_raw(ptr.as_ptr()));
            }
        }
    }
}
 
fn main() {
    let mut maybe = MaybePresent::new();
    
    println!("Has value: {}", maybe.get().is_some());
    
    maybe.set(String::from("Hello"));
    println!("Has value: {}", maybe.get().is_some());
    println!("Value: {}", maybe.get().unwrap());
    
    let value = maybe.take();
    println!("Took: {:?}", value);
}

NonNull for Zero-Sized Types

use std::ptr::NonNull;
 
// ZSTs have no data, but we still need a "pointer" for them
 
struct Empty;
 
fn main() {
    // dangling() provides a non-null but invalid pointer
    // Safe because ZSTs don't actually dereference memory
    let zst_ptr: NonNull<Empty> = NonNull::dangling();
    
    println!("ZST pointer: {:?}", zst_ptr);
    
    // Size of pointer to ZST is still just a pointer
    println!("Size of NonNull<Empty>: {}", std::mem::size_of::<NonNull<Empty>>());
}

NonNull with unsafe Send/Sync

use std::ptr::NonNull;
 
// NonNull<T> is !Send and !Sync (like raw pointers)
// But if T is Send/Sync, you can implement it manually
 
struct ThreadSafeBox<T> {
    ptr: NonNull<T>,
}
 
// SAFETY: We guarantee exclusive access to the data
// In real code, you'd need proper synchronization!
unsafe impl<T: Send> Send for ThreadSafeBox<T> {}
unsafe impl<T: Sync> Sync for ThreadSafeBox<T> {}
 
fn main() {
    fn check_send<T: Send>() {}
    fn check_sync<T: Sync>() {}
    
    check_send::<ThreadSafeBox<i32>>();
    check_sync::<ThreadSafeBox<i32>>();
    
    println!("ThreadSafeBox is Send + Sync");
}

Common Patterns with NonNull

use std::ptr::NonNull;
 
fn main() {
    // Pattern 1: Store allocated pointer
    struct Owned<T> {
        ptr: NonNull<T>,
    }
    
    // Pattern 2: Optional pointer without Option overhead
    struct MaybeOwned<T> {
        ptr: Option<NonNull<T>>,  // Same size as *const T
    }
    
    // Pattern 3: Self-referential structure
    struct Node {
        value: i32,
        prev: Option<NonNull<Node>>,
        next: Option<NonNull<Node>>,
    }
    
    // Pattern 4: FFI non-null guarantee
    fn ffi_wrapper(ptr: *mut i32) -> Option<i32> {
        NonNull::new(ptr).map(|p| unsafe { *p.as_ref() })
    }
    
    println!("Patterns demonstrated");
}

Summary

NonNull Methods:

Method Description Safety
new(ptr) Create from *mut T (returns Option) Safe
new_unchecked(ptr) Create from *mut T (assumes non-null) Unsafe
dangling() Create dangling pointer (for ZSTs) Safe
from(ref) Create from reference Safe
as_ptr() Get *mut T Safe
as_ref() Get &T Unsafe
as_mut() Get &mut T Unsafe
cast<U>() Convert to NonNull<U> Safe

Comparison with Related Types:

Type Null? Size Send/Sync
*mut T Can be null Pointer size !Send, !Sync
NonNull<T> Never null Pointer size !Send, !Sync
&T Never null Pointer size Send if T: Sync, Sync if T: Sync
&mut T Never null Pointer size Send if T: Send, !Sync
Box<T> Never null Pointer size Send if T: Send, Sync if T: Sync
Option<NonNull<T>> None is null Pointer size !Send, !Sync

When to Use NonNull:

Scenario Appropriate?
Custom smart pointers āœ… Yes
Collections (Vec, LinkedList) āœ… Yes
FFI non-null pointers āœ… Yes
Option pointer optimization āœ… Yes
Self-referential structures āœ… Yes
Normal code āŒ Use references
Nullable FFI pointers āŒ Use *mut T directly

Key Points:

  • NonNull<T> guarantees the pointer is never null
  • Enables compiler optimizations (null pointer optimization)
  • Option<NonNull<T>> has the same size as a raw pointer
  • Covariant in T (unlike *mut T which is invariant)
  • Essential for implementing custom smart pointers
  • Like raw pointers, not automatically Send or Sync
  • Use dangling() for ZSTs or sentinel values
  • Combine with MaybeUninit for uninitialized pointer wrappers