Files
Sprimo/skills/unsafe-checker/examples/safe-abstraction.md
2026-02-12 22:58:33 +08:00

6.9 KiB

Safe Abstraction Examples

Examples of building safe APIs on top of unsafe code.

Example 1: Simple Wrapper with Bounds Check

/// A slice wrapper that provides unchecked access internally
/// but safe access externally.
pub struct SafeSlice<'a, T> {
    ptr: *const T,
    len: usize,
    _marker: std::marker::PhantomData<&'a T>,
}

impl<'a, T> SafeSlice<'a, T> {
    /// Creates a SafeSlice from a regular slice.
    pub fn new(slice: &'a [T]) -> Self {
        Self {
            ptr: slice.as_ptr(),
            len: slice.len(),
            _marker: std::marker::PhantomData,
        }
    }

    /// Safe get - returns Option.
    pub fn get(&self, index: usize) -> Option<&T> {
        if index < self.len {
            // SAFETY: We just verified index < len
            Some(unsafe { &*self.ptr.add(index) })
        } else {
            None
        }
    }

    /// Unsafe get - caller must ensure bounds.
    ///
    /// # Safety
    /// `index` must be less than `self.len()`.
    pub unsafe fn get_unchecked(&self, index: usize) -> &T {
        debug_assert!(index < self.len);
        &*self.ptr.add(index)
    }

    pub fn len(&self) -> usize {
        self.len
    }
}

Example 2: Resource Wrapper with Drop

use std::ptr::NonNull;

/// Safe wrapper around a C-allocated buffer.
pub struct CBuffer {
    ptr: NonNull<u8>,
    len: usize,
}

extern "C" {
    fn c_alloc(size: usize) -> *mut u8;
    fn c_free(ptr: *mut u8);
}

impl CBuffer {
    /// Creates a new buffer. Returns None if allocation fails.
    pub fn new(size: usize) -> Option<Self> {
        let ptr = unsafe { c_alloc(size) };
        NonNull::new(ptr).map(|ptr| Self { ptr, len: size })
    }

    /// Returns a slice view of the buffer.
    pub fn as_slice(&self) -> &[u8] {
        // SAFETY: ptr is valid for len bytes (from c_alloc contract)
        unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
    }

    /// Returns a mutable slice view.
    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        // SAFETY: We have &mut self, so exclusive access
        unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
    }
}

impl Drop for CBuffer {
    fn drop(&mut self) {
        // SAFETY: ptr was allocated by c_alloc and not yet freed
        unsafe { c_free(self.ptr.as_ptr()); }
    }
}

// Prevent double-free
impl !Clone for CBuffer {}

// Safe to send between threads (assuming c_alloc is thread-safe)
unsafe impl Send for CBuffer {}

Example 3: Interior Mutability with UnsafeCell

use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};

/// A simple spinlock demonstrating safe abstraction over UnsafeCell.
pub struct SpinLock<T> {
    locked: AtomicBool,
    data: UnsafeCell<T>,
}

pub struct SpinLockGuard<'a, T> {
    lock: &'a SpinLock<T>,
}

impl<T> SpinLock<T> {
    pub const fn new(data: T) -> Self {
        Self {
            locked: AtomicBool::new(false),
            data: UnsafeCell::new(data),
        }
    }

    pub fn lock(&self) -> SpinLockGuard<'_, T> {
        // Spin until we acquire the lock
        while self.locked.compare_exchange_weak(
            false,
            true,
            Ordering::Acquire,
            Ordering::Relaxed,
        ).is_err() {
            std::hint::spin_loop();
        }
        SpinLockGuard { lock: self }
    }
}

impl<T> std::ops::Deref for SpinLockGuard<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        // SAFETY: We hold the lock, so we have exclusive access
        unsafe { &*self.lock.data.get() }
    }
}

impl<T> std::ops::DerefMut for SpinLockGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        // SAFETY: We hold the lock, so we have exclusive access
        unsafe { &mut *self.lock.data.get() }
    }
}

impl<T> Drop for SpinLockGuard<'_, T> {
    fn drop(&mut self) {
        self.lock.locked.store(false, Ordering::Release);
    }
}

// SAFETY: The lock ensures only one thread accesses data at a time
unsafe impl<T: Send> Sync for SpinLock<T> {}
unsafe impl<T: Send> Send for SpinLock<T> {}

Example 4: Iterator with Lifetime Tracking

use std::marker::PhantomData;

/// An iterator over raw pointer range with proper lifetime tracking.
pub struct PtrIter<'a, T> {
    current: *const T,
    end: *const T,
    _marker: PhantomData<&'a T>,
}

impl<'a, T> PtrIter<'a, T> {
    /// Creates an iterator from a slice.
    pub fn new(slice: &'a [T]) -> Self {
        let ptr = slice.as_ptr();
        Self {
            current: ptr,
            // SAFETY: Adding len to slice pointer is always valid
            end: unsafe { ptr.add(slice.len()) },
            _marker: PhantomData,
        }
    }
}

impl<'a, T> Iterator for PtrIter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current == self.end {
            None
        } else {
            // SAFETY:
            // - current < end (checked above)
            // - PhantomData<&'a T> ensures the data lives for 'a
            let item = unsafe { &*self.current };
            self.current = unsafe { self.current.add(1) };
            Some(item)
        }
    }
}

Example 5: Builder Pattern with Delayed Initialization

use std::mem::MaybeUninit;

/// A builder that collects exactly N items, then produces an array.
pub struct ArrayBuilder<T, const N: usize> {
    data: [MaybeUninit<T>; N],
    count: usize,
}

impl<T, const N: usize> ArrayBuilder<T, N> {
    pub fn new() -> Self {
        Self {
            // SAFETY: MaybeUninit doesn't require initialization
            data: unsafe { MaybeUninit::uninit().assume_init() },
            count: 0,
        }
    }

    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.count >= N {
            return Err(value);
        }
        self.data[self.count].write(value);
        self.count += 1;
        Ok(())
    }

    pub fn build(self) -> Option<[T; N]> {
        if self.count != N {
            return None;
        }

        // SAFETY: All N elements have been initialized
        let result = unsafe {
            // Prevent drop of self.data (we're moving out)
            let data = std::ptr::read(&self.data);
            std::mem::forget(self);
            // Transmute MaybeUninit array to initialized array
            std::mem::transmute_copy::<[MaybeUninit<T>; N], [T; N]>(&data)
        };
        Some(result)
    }
}

impl<T, const N: usize> Drop for ArrayBuilder<T, N> {
    fn drop(&mut self) {
        // Drop only initialized elements
        for i in 0..self.count {
            // SAFETY: Elements 0..count are initialized
            unsafe { self.data[i].assume_init_drop(); }
        }
    }
}

Key Patterns

  1. Encapsulation: Hide unsafe behind safe public API
  2. Invariant maintenance: Use private fields to maintain invariants
  3. PhantomData: Track lifetimes and ownership for pointers
  4. RAII: Use Drop for cleanup
  5. Type state: Use types to encode valid states