null pointer check
Tutorial
The Problem
This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.
🎯 Learning Outcomes
Code Example
use std::ptr::NonNull;
use std::mem::size_of;
// NonNull::new turns a raw pointer into Option<NonNull<T>>,
// forcing null-check at the boundary rather than at dereference.
pub fn wrap_nullable<T>(ptr: *mut T) -> Option<NonNull<T>> {
NonNull::new(ptr) // None if null, Some(nn) if non-null
}
// Option<NonNull<T>> is exactly pointer-sized (null-pointer optimisation).
pub fn option_nonnull_is_pointer_sized<T>() -> bool {
size_of::<Option<NonNull<T>>>() == size_of::<*mut T>()
}Key Differences
unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.extern "C"; OCaml uses ctypes which wraps C types in OCaml values.#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.OCaml Approach
OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:
ctypes library for external function callsBigarray for controlled raw memory access Bytes.t for mutable byte sequencesOCaml programs rarely need operations equivalent to these Rust unsafe patterns.
Full Source
#![allow(clippy::all)]
//! 705 — Null Pointer Handling: NonNull<T>
//!
//! `NonNull<T>` is a `*mut T` guaranteed non-null at the type level.
//! `Option<NonNull<T>>` compresses to pointer-size (null-pointer optimisation),
//! while still forcing explicit null-checking at construction via `NonNull::new`.
use std::mem::size_of;
use std::ptr::NonNull;
// ---------------------------------------------------------------------------
// Linked list node using NonNull for the "next" pointer
// ---------------------------------------------------------------------------
pub struct Node<T> {
pub value: T,
pub next: Option<NonNull<Node<T>>>,
}
/// Build a singly-linked list on the heap, returning the head as `NonNull`.
///
/// Each node is allocated with `Box::into_raw` so we own the memory and can
/// free it later. `NonNull::new_unchecked` is safe here because `Box` never
/// returns a null pointer.
pub fn build_list<T>(values: &[T]) -> Option<NonNull<Node<T>>>
where
T: Copy,
{
let mut head: Option<NonNull<Node<T>>> = None;
for &v in values.iter().rev() {
let node = Box::new(Node {
value: v,
next: head,
});
// SAFETY: Box::into_raw is never null.
head = Some(unsafe { NonNull::new_unchecked(Box::into_raw(node)) });
}
head
}
/// Traverse the list and collect values.
///
/// # Safety invariant
/// Every `NonNull<Node<T>>` in this list was produced by `Box::into_raw` inside
/// `build_list`, so the pointer is valid, aligned, and not aliased mutably.
pub fn collect_list<T: Copy>(mut cursor: Option<NonNull<Node<T>>>) -> Vec<T> {
let mut out = Vec::new();
while let Some(ptr) = cursor {
// SAFETY: pointer came from Box::into_raw and is still live.
let node = unsafe { ptr.as_ref() };
out.push(node.value);
cursor = node.next;
}
out
}
/// Free the heap-allocated nodes in the list.
///
/// # Safety invariant
/// Same as `collect_list` — every pointer came from `Box::into_raw`.
pub fn free_list<T>(mut cursor: Option<NonNull<Node<T>>>) {
while let Some(ptr) = cursor {
// SAFETY: pointer came from Box::into_raw; we are the sole owner.
let node = unsafe { Box::from_raw(ptr.as_ptr()) };
cursor = node.next;
}
}
// ---------------------------------------------------------------------------
// Null-pointer optimisation: Option<NonNull<T>> == size_of::<*mut T>()
// ---------------------------------------------------------------------------
/// Returns true when Option<NonNull<T>> is the same size as a raw pointer.
///
/// This is the "null-pointer optimisation": the compiler encodes `None` as the
/// null address, so no extra discriminant word is needed.
pub fn option_nonnull_is_pointer_sized<T>() -> bool {
size_of::<Option<NonNull<T>>>() == size_of::<*mut T>()
}
// ---------------------------------------------------------------------------
// Simulating a C FFI nullable pointer pattern
// ---------------------------------------------------------------------------
/// Wraps a raw (potentially null) pointer the way a C FFI boundary would.
///
/// `NonNull::new` returns `None` for null, forcing the caller to handle the
/// absent case explicitly rather than dereferencing blindly.
pub fn wrap_nullable<T>(ptr: *mut T) -> Option<NonNull<T>> {
NonNull::new(ptr)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_null_pointer_optimisation() {
// Option<NonNull<T>> must be pointer-sized — this is the whole point.
assert!(option_nonnull_is_pointer_sized::<i32>());
assert!(option_nonnull_is_pointer_sized::<u8>());
assert!(option_nonnull_is_pointer_sized::<[u8; 64]>());
// Contrast: Option<*mut T> is NOT pointer-sized (needs a discriminant).
assert!(size_of::<Option<*mut i32>>() > size_of::<*mut i32>());
}
#[test]
fn test_wrap_nullable_null() {
let null: *mut i32 = std::ptr::null_mut();
assert!(wrap_nullable(null).is_none());
}
#[test]
fn test_wrap_nullable_nonnull() {
let mut value: i32 = 42;
let nn = wrap_nullable(&mut value);
assert!(nn.is_some());
// SAFETY: pointer is to a live stack variable, no aliasing.
let got = unsafe { *nn.unwrap().as_ptr() };
assert_eq!(got, 42);
}
#[test]
fn test_linked_list_empty() {
let head = build_list::<i32>(&[]);
assert!(head.is_none());
let values = collect_list(head);
assert!(values.is_empty());
}
#[test]
fn test_linked_list_single() {
let head = build_list(&[99_i32]);
let values = collect_list(head);
assert_eq!(values, [99]);
free_list(build_list(&[99_i32]));
}
#[test]
fn test_linked_list_multiple() {
let data = [1_i32, 2, 3, 4, 5];
let head = build_list(&data);
let values = collect_list(head);
assert_eq!(values, data);
free_list(build_list(&data));
}
#[test]
fn test_nonnull_new_unchecked_never_null() {
// NonNull::new_unchecked on a live Box pointer must produce Some equivalent.
let mut x: i32 = 7;
let raw: *mut i32 = &mut x;
// SAFETY: raw is non-null (stack variable).
let nn = unsafe { NonNull::new_unchecked(raw) };
assert_eq!(unsafe { *nn.as_ptr() }, 7);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_null_pointer_optimisation() {
// Option<NonNull<T>> must be pointer-sized — this is the whole point.
assert!(option_nonnull_is_pointer_sized::<i32>());
assert!(option_nonnull_is_pointer_sized::<u8>());
assert!(option_nonnull_is_pointer_sized::<[u8; 64]>());
// Contrast: Option<*mut T> is NOT pointer-sized (needs a discriminant).
assert!(size_of::<Option<*mut i32>>() > size_of::<*mut i32>());
}
#[test]
fn test_wrap_nullable_null() {
let null: *mut i32 = std::ptr::null_mut();
assert!(wrap_nullable(null).is_none());
}
#[test]
fn test_wrap_nullable_nonnull() {
let mut value: i32 = 42;
let nn = wrap_nullable(&mut value);
assert!(nn.is_some());
// SAFETY: pointer is to a live stack variable, no aliasing.
let got = unsafe { *nn.unwrap().as_ptr() };
assert_eq!(got, 42);
}
#[test]
fn test_linked_list_empty() {
let head = build_list::<i32>(&[]);
assert!(head.is_none());
let values = collect_list(head);
assert!(values.is_empty());
}
#[test]
fn test_linked_list_single() {
let head = build_list(&[99_i32]);
let values = collect_list(head);
assert_eq!(values, [99]);
free_list(build_list(&[99_i32]));
}
#[test]
fn test_linked_list_multiple() {
let data = [1_i32, 2, 3, 4, 5];
let head = build_list(&data);
let values = collect_list(head);
assert_eq!(values, data);
free_list(build_list(&data));
}
#[test]
fn test_nonnull_new_unchecked_never_null() {
// NonNull::new_unchecked on a live Box pointer must produce Some equivalent.
let mut x: i32 = 7;
let raw: *mut i32 = &mut x;
// SAFETY: raw is non-null (stack variable).
let nn = unsafe { NonNull::new_unchecked(raw) };
assert_eq!(unsafe { *nn.as_ptr() }, 7);
}
}
Deep Comparison
OCaml vs Rust: Null Pointer Handling with NonNull<T>
Side-by-Side Code
OCaml
(* OCaml has no nulls — every value is non-null by construction.
Absence is modelled with option, not null pointers. *)
let make_nonnull (x : 'a) : 'a = x (* identity: all values are non-null *)
let wrap_nullable (x : 'a option) : 'a option = x
let () =
let nn = make_nonnull 42 in
Printf.printf "NonNull value: %d\n" nn;
Printf.printf "Some: %s\n"
(match wrap_nullable (Some 99) with Some v -> string_of_int v | None -> "null");
Printf.printf "None: %s\n"
(match wrap_nullable None with Some v -> string_of_int v | None -> "null");
assert (make_nonnull 0 = 0);
print_endline "ok"
Rust (idiomatic — Option<NonNull<T>> at pointer size)
use std::ptr::NonNull;
use std::mem::size_of;
// NonNull::new turns a raw pointer into Option<NonNull<T>>,
// forcing null-check at the boundary rather than at dereference.
pub fn wrap_nullable<T>(ptr: *mut T) -> Option<NonNull<T>> {
NonNull::new(ptr) // None if null, Some(nn) if non-null
}
// Option<NonNull<T>> is exactly pointer-sized (null-pointer optimisation).
pub fn option_nonnull_is_pointer_sized<T>() -> bool {
size_of::<Option<NonNull<T>>>() == size_of::<*mut T>()
}
Rust (functional — NonNull-based linked list)
pub fn build_list<T: Copy>(values: &[T]) -> Option<NonNull<Node<T>>> {
let mut head = None;
for &v in values.iter().rev() {
let node = Box::new(Node { value: v, next: head });
// SAFETY: Box::into_raw is never null.
head = Some(unsafe { NonNull::new_unchecked(Box::into_raw(node)) });
}
head
}
pub fn collect_list<T: Copy>(mut cursor: Option<NonNull<Node<T>>>) -> Vec<T> {
let mut out = Vec::new();
while let Some(ptr) = cursor {
// SAFETY: every pointer came from Box::into_raw and is still live.
let node = unsafe { ptr.as_ref() };
out.push(node.value);
cursor = node.next;
}
out
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Non-null value | every 'a value (no nulls exist) | NonNull<T> — *mut T guaranteed non-null |
| Nullable pointer | 'a option | Option<NonNull<T>> |
| Null check | pattern match on option | NonNull::new returns Option<NonNull<T>> |
| Memory size of nullable ptr | always pointer-size (boxed rep) | size_of::<Option<NonNull<T>>>() == size_of::<*mut T>() |
| Dereference | direct (GC-managed) | unsafe { nn.as_ref() } — explicit unsafe block |
Key Insights
NonNull<T> adds back to Rust's unsafe pointer world.Option<NonNull<T>> is encoded as a single machine word — None maps to the null address, Some(nn) to the pointer itself — exactly matching a C nullable pointer in size and ABI.NonNull::new is the only safe entry point; it returns Option<NonNull<T>> so null-checking is forced at the FFI or allocation boundary rather than silently deferred to dereference time.free_list walk using Box::from_raw to avoid leaking heap nodes — the borrow checker doesn't help inside unsafe.unsafe is scoped and documented**: each unsafe block carries a // SAFETY: comment explaining why the invariant holds, making audits tractable even when the code is low-level.When to Use Each Style
**Use NonNull<T> when:** writing custom allocators, intrusive data structures, or wrapping C FFI that returns nullable pointers — anywhere you need raw-pointer performance with a compiler-enforced non-null invariant and zero-cost Option encoding.
**Use Option<Box<T>> or Option<&T> when:** you don't need raw pointers at all; prefer safe references and let the borrow checker handle lifetime and aliasing instead of maintaining manual unsafe invariants.
Exercises
bytemuck for transmute, CString for FFI strings) and implement it.