ExamplesBy LevelBy TopicLearning Paths
705 Fundamental

null pointer check

Functional Programming

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

  • • The specific unsafe feature demonstrated: null pointer check
  • • When this feature is necessary vs when safe alternatives exist
  • • How to use it correctly with appropriate SAFETY documentation
  • • The invariants that must be maintained for the operation to be sound
  • • Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • 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

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • • C FFI via the ctypes library for external function calls
  • Bigarray for controlled raw memory access
  • • The GC for memory management (no manual allocators needed)
  • Bytes.t for mutable byte sequences
  • OCaml 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Non-null valueevery 'a value (no nulls exist)NonNull<T>*mut T guaranteed non-null
    Nullable pointer'a optionOption<NonNull<T>>
    Null checkpattern match on optionNonNull::new returns Option<NonNull<T>>
    Memory size of nullable ptralways pointer-size (boxed rep)size_of::<Option<NonNull<T>>>() == size_of::<*mut T>()
    Dereferencedirect (GC-managed)unsafe { nn.as_ref() } — explicit unsafe block

    Key Insights

  • No nulls in OCaml: every OCaml value is non-null by definition; the language achieves the same safety guarantee that NonNull<T> adds back to Rust's unsafe pointer world.
  • Null-pointer optimisation: 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.
  • Safety boundary at construction: 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.
  • Manual memory discipline: OCaml's GC reclaims nodes automatically; the Rust version requires an explicit 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

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos