NonNull<T> and null-pointer optimization
In FFI code you often know that a pointer isn’t null. NonNull<T> puts that
knowledge in the type. Wrapped in an Option, it also gives you a pointer that
may be null, but that you can’t use without checking first.
NonNull<T>
NonNull<T> behaves like a *mut T with one extra guarantee: it is never null.
You create one from a raw pointer with NonNull::new, which returns None for
null, or from a reference, which is never null to begin with:
use std::ptr::NonNull;
let mut x = 5_u32;
let p: NonNull<u32> = NonNull::from(&mut x);
// SAFETY: `p` comes from `&mut x`, which is alive and not borrowed elsewhere.
unsafe { *p.as_ptr() += 1 };
assert_eq!(x, 6);
let from_c: *mut u32 = std::ptr::null_mut();
assert!(NonNull::new(from_c).is_none());
A few more methods are worth knowing:
NonNull::new_uncheckedskips the null check. It’sunsafe, because passing a null pointer is undefined behavior.as_ptrturns it back into a*mut T, for example to hand it to C.as_refandas_mutturn it into a reference. They’reunsafefor the same reasons as with raw pointers: non-null doesn’t mean aligned, alive, or valid.NonNull::danglingcreates a non-null, aligned pointer that doesn’t point to anything. The standard library uses it for empty collections, which have nothing to point at but still have to hold a pointer.1
Null-pointer optimization
A NonNull<T> can never be all zeros (0 being a forbidden value is literally
the type’s only job), and the compiler takes advantage of that. A bit pattern
that a type can never hold is called a niche, and the compiler looks for one
whenever it lays out an enum. For pointer-like types the trick has a name of its
own: null-pointer optimization (NPO).
use std::mem::size_of;
use std::ptr::NonNull;
const _: () = {
assert!(size_of::<Option<NonNull<u8>>>() == size_of::<NonNull<u8>>());
assert!(size_of::<Option<&u8>>() == size_of::<&u8>());
assert!(size_of::<Option<Box<u8>>>() == size_of::<Box<u8>>());
assert!(size_of::<Option<extern "C" fn()>>() == size_of::<extern "C" fn()>());
};
A raw pointer has none of these guarantees. Every combination of bits can be a
valid *const T or *mut T, so there’s no niche left to mark None with. The
compiler has to store that separately, and pad the result back out to the
pointer’s alignment. On a 64-bit target that doubles the size:
use std::mem::size_of;
const _: () = {
assert!(size_of::<*const u8>() == 8);
assert!(size_of::<Option<*const u8>>() == 16);
};
That size is a symptom rather than the rule: Option<*const T> has no specified
layout, so it wouldn’t be a C pointer even if it happened to be eight bytes
wide. That is the practical reason NonNull exists: Option<NonNull<T>> lets
you say “may be null” in Rust’s type system and get a value that is one pointer
wide, with a layout you’re allowed to rely on.
What’s guaranteed
Niches are an optimization, and in general the compiler is free to decide how to
lay out an Option. For a short list of types, though, the standard library
guarantees
that Option<T> has the same size, alignment, and function-call ABI as T,
with None represented as all zeros:
&Tand&mut TBox<T>NonNull<T>NonZero<u8>,NonZero<u32>, and the otherNonZerointegers- function pointers, including
extern "C" fn #[repr(transparent)]structs around one of the above
For the pointer types, the all-zeros part only holds when T is sized. A
pointer to an unsized type, such as &[u8] or &str, is a pointer plus a
length, and can’t cross the FFI boundary as a single C pointer.
This guarantee is what makes these types usable in FFI signatures. For anything
else, such as Option<bool> or Option<u32>, the layout is a compiler decision
and can’t be used in FFI signatures.
Head to the exercise
The exercise is one function: take a raw pointer the way C would hand it over,
read a field out of what it points to, and return None when there is nothing
to read. Let the type carry the null check rather than writing one by hand.
Exercise
The exercise for this section is located in 01_foundations/03_nonnull