Files
Sprimo/skills/unsafe-checker/rules/ptr-02-prefer-nonnull.md
2026-02-12 22:58:33 +08:00

2.7 KiB

id, original_id, level, impact
id original_id level impact
ptr-02 P.UNS.PTR.02 P MEDIUM

Prefer NonNull Over *mut T

Summary

Use NonNull<T> instead of *mut T when the pointer should never be null. This enables null pointer optimization and makes the intent clear.

Rationale

  • NonNull<T> guarantees non-null at the type level
  • Enables niche optimization: Option<NonNull<T>> is the same size as *mut T
  • Makes invariants explicit in the type system
  • Covariant over T (like &T), which is usually what you want

Bad Example

// DON'T: Use *mut when pointer is always non-null
struct MyBox<T> {
    ptr: *mut T,  // Invariant: never null, but not enforced
}

impl<T> MyBox<T> {
    pub fn new(value: T) -> Self {
        let ptr = Box::into_raw(Box::new(value));
        // ptr is guaranteed non-null, but type doesn't show it
        Self { ptr }
    }

    pub fn get(&self) -> &T {
        // Must add null check or document the invariant
        unsafe { &*self.ptr }
    }
}

Good Example

use std::ptr::NonNull;

// DO: Use NonNull when pointer is never null
struct MyBox<T> {
    ptr: NonNull<T>,  // Type guarantees non-null
}

impl<T> MyBox<T> {
    pub fn new(value: T) -> Self {
        let ptr = Box::into_raw(Box::new(value));
        // SAFETY: Box::into_raw never returns null
        let ptr = unsafe { NonNull::new_unchecked(ptr) };
        Self { ptr }
    }

    pub fn get(&self) -> &T {
        // SAFETY: NonNull guarantees ptr is valid
        unsafe { self.ptr.as_ref() }
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        // SAFETY: ptr was created from Box::into_raw
        unsafe { drop(Box::from_raw(self.ptr.as_ptr())); }
    }
}

// DO: Niche optimization with Option
struct OptionalBox<T> {
    ptr: Option<NonNull<T>>,  // Same size as *mut T!
}

NonNull API

use std::ptr::NonNull;

// Creating NonNull
let ptr: NonNull<i32> = NonNull::new(raw_ptr).expect("null pointer");
let ptr: NonNull<i32> = unsafe { NonNull::new_unchecked(raw_ptr) };
let ptr: NonNull<i32> = NonNull::dangling();  // For ZSTs or uninitialized

// Using NonNull
let raw: *mut i32 = ptr.as_ptr();
let reference: &i32 = unsafe { ptr.as_ref() };
let mut_ref: &mut i32 = unsafe { ptr.as_mut() };

// Casting
let ptr: NonNull<u8> = ptr.cast::<u8>();

When to Use *mut T Instead

  • When null is a valid/expected value
  • FFI with C code that may return null
  • When variance matters (NonNull is covariant, sometimes you need invariance)

Checklist

  • Is my pointer ever null? If no, use NonNull
  • Do I need null pointer optimization?
  • Is the variance correct for my use case?
  • ptr-03: Use PhantomData for variance and ownership
  • safety-06: Don't expose raw pointers in public APIs