Unsafe Abstractions
Unsafe abstractions are reusable data structures, smart pointers, and concurrency primitives that internally use raw pointers, PhantomData, NonNull<T>, and manual memory management to provide capabilities beyond safe built-in references while enforcing lifetime, variance, and drop-check rules through the type system.
Four Pillars of Building a Sound Unsafe Abstraction
When designing a custom smart pointer or collection around raw pointers (*mut T or NonNull<T>), four type-system properties must be configured explicitly because raw pointers do not carry them automatically:
| Pillar | Problem with Raw *mut T / NonNull<T> |
Type-System Solution |
|---|---|---|
| 1. Lifetimes | Raw pointers have no lifetime 'a, allowing borrowed references to outlive their owner |
Add PhantomData<&'a T> (shared) or PhantomData<&'a mut T> (exclusive) |
| 2. Variance | *mut T is invariant over T; NonNull<T> is covariant over T even if the container mutates T |
Add PhantomData<T> or PhantomData<Cell<T>> if invariance is required |
| 3. Send & Sync | Raw pointers implement neither Send nor Sync |
Provide audited unsafe impl<T: Send> Send and unsafe impl<T: Sync> Sync |
| 4. Ownership & Drop Check | NonNull<T> does not tell the compiler that dropping the container drops T |
Add PhantomData<T> so the borrow checker knows T is owned and dropped |
Building a Custom Owned Heap Box (MiniBox<T>)
Below is a complete, sound implementation of a single-owner heap smart pointer MiniBox<T> that handles zero-sized types (size_of::<T>() == 0), allocation failures (handle_alloc_error), Deref/DerefMut, Drop, and thread safety (Send/Sync):
use std::alloc::{Layout, alloc, dealloc, handle_alloc_error};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::ptr::{self, NonNull};
pub struct MiniBox<T> {
ptr: NonNull<T>,
// Tells the compiler that `MiniBox<T>` logically owns a value of type `T`:
_marker: PhantomData<T>,
}
// SAFETY: `MiniBox<T>` uniquely owns its heap-allocated `T`, so transferring
// or sharing `MiniBox<T>` across threads has the exact same safety as `T`.
unsafe impl<T: Send> Send for MiniBox<T> {}
unsafe impl<T: Sync> Sync for MiniBox<T> {}
impl<T> MiniBox<T> {
pub fn new(value: T) -> Self {
let ptr = if size_of::<T>() == 0 {
NonNull::dangling()
} else {
let layout = Layout::new::<T>();
// SAFETY: `size_of::<T>() > 0` was checked above.
let raw = unsafe { alloc(layout).cast::<T>() };
let Some(non_null) = NonNull::new(raw) else {
handle_alloc_error(layout);
};
// SAFETY: `non_null` points to freshly allocated aligned memory for `T`.
unsafe {
non_null.as_ptr().write(value);
}
non_null
};
Self {
ptr,
_marker: PhantomData,
}
}
}
impl<T> Deref for MiniBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// SAFETY: `self.ptr` is always aligned, initialized, and valid while `self` lives.
unsafe { self.ptr.as_ref() }
}
}
impl<T> DerefMut for MiniBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
// SAFETY: `&mut self` guarantees exclusive access to the owned `T`.
unsafe { self.ptr.as_mut() }
}
}
impl<T> Drop for MiniBox<T> {
fn drop(&mut self) {
// SAFETY: Drop the inner `T` in place first; then deallocate if non-ZST.
unsafe {
ptr::drop_in_place(self.ptr.as_ptr());
if size_of::<T>() != 0 {
dealloc(self.ptr.as_ptr().cast::<u8>(), Layout::new::<T>());
}
}
}
}
fn main() {
let mut boxed = MiniBox::new(String::from("custom heap box"));
boxed.push_str(" works!");
let zst_box = MiniBox::new(());
println!("boxed: {}", *boxed);
println!("zst_box: {:?}", *zst_box);
}
boxed: custom heap box works!
zst_box: ()