705: Null Pointer Handling with NonNull<T>
Difficulty: 4 Level: Expert Encode the non-null invariant into the type system using `NonNull<T>` and get the null-pointer optimisation for free.The Problem This Solves
When writing custom collections in Rust โ linked lists, trees, arena allocators โ you often need optional pointers: "this node has a next node, or it doesn't." The obvious choice is `Option<mut Node<T>>`, but that carries two problems. First, it gives up nothing: the compiler doesn't help you avoid null dereferences any more than a raw pointer would. Second, it wastes memory โ `Option<mut T>` is two words wide on most platforms because the compiler can't prove the pointer is non-null. C FFI brings the same problem from the other direction. A C function returning `*T` uses NULL to signal absence or error. Rust needs to model that contract precisely โ not just "here's a raw pointer" but "this might be null, and you must check before using it." `NonNull<T>` solves both sides. It's a `*mut T` that is statically guaranteed to be non-null. The compiler knows this guarantee, so `Option<NonNull<T>>` compresses to pointer size โ exactly like C's nullable pointer, but type-safe. Null handling moves to `NonNull::new()`, which returns `Option<NonNull<T>>`, forcing explicit handling at construction time rather than silently at dereference time.The Intuition
Think of `NonNull<T>` as a `mut T` with a pinky-swear to the compiler: "this pointer is never null." The compiler takes that promise seriously enough to let `Option<NonNull<T>>` use the null bit pattern for `None` โ the same trick C++ `std::optional<T>` uses, but guaranteed at the type level. You pay for the guarantee once, at `NonNull::new(ptr)` โ which returns `Option<NonNull<T>>` and forces you to handle the null case. After that, every use of the `NonNull` is null-free with no runtime cost.How It Works in Rust
use std::ptr::NonNull;
// Construct from a raw pointer โ null check happens here, once.
fn from_raw<T>(ptr: *mut T) -> Option<NonNull<T>> {
NonNull::new(ptr) // None if ptr is null
}
// Dereference โ still unsafe, but null is ruled out by the type.
unsafe fn read_nonnull<T: Copy>(p: NonNull<T>) -> T {
// SAFETY: NonNull guarantees non-null.
// Caller guarantees: aligned, initialized, no exclusive alias.
*p.as_ptr()
}
// The null-pointer optimisation in action:
assert_eq!(
std::mem::size_of::<Option<NonNull<u8>>>(),
std::mem::size_of::<*mut u8>() // same size โ no overhead!
);
For a linked list node:
struct Node<T> {
value: T,
next: Option<NonNull<Node<T>>>, // None = end of list
}
`next` is pointer-sized. `None` is represented as a null pointer. Traversal: `while let Some(p) = cursor { let node = unsafe { p.as_ref() }; ... cursor = node.next; }`.
What This Unlocks
- Zero-overhead optional pointers: `Option<NonNull<T>>` is pointer-sized โ use it anywhere you'd use a nullable C pointer, without any size penalty.
- Safer FFI: Wrap C's nullable return values in `NonNull::new(ptr).ok_or(MyError::Null)` at the boundary โ null handling is explicit and typed.
- Custom collections: Build intrusive linked lists, tree nodes, and arenas where the "no next" state is expressed in the type, not a sentinel value.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Absence of value | `None` (option type) | `Option<T>` or null raw pointer |
| Null-optimised option | `option` always pointer-sized | `Option<NonNull<T>>` = pointer size |
| Guaranteed non-null | Every value is non-null | `NonNull<T>` wrapper |
| Null check location | Not needed | `NonNull::new(ptr)` at construction |
| FFI null handling | Not applicable | `NonNull::new(ptr).ok_or(err)` |
| Dereference safety | GC-managed | Still `unsafe`, but null is ruled out |
//! 705 โ Null Pointer Handling: NonNull<T>
//! NonNull<T> = non-null *mut T; Option<NonNull<T>> has pointer size.
use std::ptr::NonNull;
use std::mem::size_of;
/// A simple bump-allocator-style linked list using NonNull<Node<T>>.
struct Node<T> {
value: T,
next: Option<NonNull<Node<T>>>,
}
/// Traverse a NonNull-linked list and collect values.
fn collect_list<T: Copy>(head: Option<NonNull<Node<T>>>) -> Vec<T> {
let mut result = Vec::new();
let mut cursor = head;
while let Some(ptr) = cursor {
let node = unsafe {
// SAFETY: NonNull guarantees non-null; Node was allocated via Box
// and is still live; no &mut alias exists during this traversal.
ptr.as_ref()
};
result.push(node.value);
cursor = node.next;
}
result
}
fn main() {
// โโ NonNull::new returns Option<NonNull<T>> โโโโโโโโโโโโโโโโโโโโโโโโโโโ
let mut x: i32 = 42;
let raw: *mut i32 = &mut x;
let nn = NonNull::new(raw);
println!("NonNull::new(valid): {:?}", nn);
let null: *mut i32 = std::ptr::null_mut();
println!("NonNull::new(null): {:?}", NonNull::new(null));
// โโ Null-pointer optimisation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
println!(
"size_of Option<NonNull<i32>> = {} (== *mut i32 = {})",
size_of::<Option<NonNull<i32>>>(),
size_of::<*mut i32>()
);
assert_eq!(
size_of::<Option<NonNull<u128>>>(),
size_of::<*mut u128>()
);
// โโ Build a short linked list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let mut n3 = Box::new(Node::<i32> { value: 30, next: None });
let mut n2 = Box::new(Node::<i32> { value: 20, next: NonNull::new(n3.as_mut()) });
let mut n1 = Box::new(Node::<i32> { value: 10, next: NonNull::new(n2.as_mut()) });
let head = NonNull::new(n1.as_mut());
println!("List: {:?}", collect_list(head));
drop(n1); drop(n2); drop(n3);
// โโ Dangling pointer prevented by type system โโโโโโโโโโโโโโโโโโโโโโโโโ
// NonNull::new does not prevent dangling โ the programmer must ensure
// the pointee outlives the NonNull.
println!("NonNull does not prevent dangling โ only non-null.");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nonnull_some() {
let mut v: u32 = 7;
assert!(NonNull::new(&mut v as *mut u32).is_some());
}
#[test]
fn test_nonnull_none() {
assert!(NonNull::new::<u32>(std::ptr::null_mut()).is_none());
}
#[test]
fn test_size_opt() {
assert_eq!(
size_of::<Option<NonNull<u64>>>(),
size_of::<*mut u64>()
);
}
#[test]
fn test_collect_list() {
let mut n2 = Box::new(Node::<i32> { value: 2, next: None });
let mut n1 = Box::new(Node::<i32> { value: 1, next: NonNull::new(n2.as_mut()) });
let h = NonNull::new(n1.as_mut());
assert_eq!(collect_list(h), vec![1, 2]);
drop(n1); drop(n2);
}
}
(* OCaml: no nulls โ every value is non-null by definition.
We model nullable vs non-nullable using option types. *)
(** NonNull is the default in OCaml โ every int is non-null. *)
let make_nonnull (x : 'a) : 'a = x (* identity: all values are non-null *)
(** Nullable via option. *)
let to_option (x : 'a option) : 'a option = x
let () =
let nn = make_nonnull 42 in
Printf.printf "NonNull value: %d\n" nn;
let some_v = to_option (Some 99) in
let none_v = to_option None in
Printf.printf "Some: %s\n" (match some_v with Some v -> string_of_int v | None -> "null");
Printf.printf "None: %s\n" (match none_v with Some v -> string_of_int v | None -> "null");
(* Option size hint: OCaml options are always pointer-size *)
Printf.printf "Simulating null-ptr optimisation: present in OCaml by default\n"