Unsafe Rust

Encapsulating Unsafe Code

Encapsulating unsafe code means isolating raw pointers, unchecked invariants, and unsafe blocks inside a small, private module boundary that exposes only safe functions and methods to the rest of the codebase.

Why Module Privacy Is the Unit of Encapsulation

In Rust, visibility (pub vs. private) operates at the module level, not the struct level: every function inside the same module can directly read and modify private fields of a struct defined in that module.

Therefore, if a struct FixedVec<T, const N: usize> has a safety invariant ("elements 0..self.len are initialized and self.len <= N"), the safety boundary is the entire module that has direct write access to self.len:

+--------------------------- Crate Root / Application ---------------------------+
|  Can only call safe public methods: `push()`, `pop()`, `as_slice()`           |
|                                                                                |
|  +----------------------- `mod fixed_vec` (Trust Boundary) -----------------+  |
|  |  pub struct FixedVec<T, const N: usize> {                                |  |
|  |      storage: [MaybeUninit<T>; N],  // Private!                          |  |
|  |      len: usize,                    // Private!                          |  |
|  |  }                                                                       |  |
|  |  // Only code inside this small module can touch `len` or `storage`.     |  |
|  +--------------------------------------------------------------------------+  |
+--------------------------------------------------------------------------------+

Complete Example: Encapsulating a Stack-Allocated FixedVec<T, N>

By keeping storage and len private inside mod fixed_vec, the module guarantees that 0..self.len is always initialized and self.len <= N, allowing .as_slice(), .push(), .pop(), and Drop to use MaybeUninit and raw pointers soundly:

mod fixed_vec {
    use std::mem::MaybeUninit;
    use std::ptr;
    use std::slice;

    pub struct FixedVec<T, const N: usize> {
        storage: [MaybeUninit<T>; N],
        // Safety invariant: `self.len <= N`, and `self.storage[0..self.len]`
        // are always initialized.
        len: usize,
    }

    impl<T, const N: usize> FixedVec<T, N> {
        pub const fn new() -> Self {
            Self {
                storage: [const { MaybeUninit::uninit() }; N],
                len: 0,
            }
        }

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

        pub fn pop(&mut self) -> Option<T> {
            if self.len == 0 {
                return None;
            }
            self.len -= 1;
            // SAFETY: By our invariant, `self.storage[self.len]` was initialized,
            // and decrementing `self.len` logically uninitializes that slot.
            Some(unsafe { self.storage[self.len].assume_init_read() })
        }

        pub fn as_slice(&self) -> &[T] {
            // SAFETY: `self.storage[0..self.len]` are initialized and `MaybeUninit<T>`
            // has the exact same layout as `T`.
            unsafe {
                slice::from_raw_parts(self.storage.as_ptr().cast::<T>(), self.len)
            }
        }
    }

    impl<T, const N: usize> Drop for FixedVec<T, N> {
        fn drop(&mut self) {
            // SAFETY: Drop all `0..self.len` initialized elements in place.
            unsafe {
                let initialized_slice = ptr::slice_from_raw_parts_mut(
                    self.storage.as_mut_ptr().cast::<T>(),
                    self.len,
                );
                ptr::drop_in_place(initialized_slice);
            }
        }
    }
}

use fixed_vec::FixedVec;

fn main() {
    let mut vec: FixedVec<String, 3> = FixedVec::new();
    assert!(vec.push(String::from("alpha")).is_ok());
    assert!(vec.push(String::from("beta")).is_ok());
    assert!(vec.push(String::from("gamma")).is_ok());
    assert!(vec.push(String::from("overflow")).is_err());

    println!("slice:  {:?}", vec.as_slice());
    println!("popped: {:?}", vec.pop());
    println!("after:  {:?}", vec.as_slice());
}
slice:  ["alpha", "beta", "gamma"]
popped: Some("gamma")
after:  ["alpha", "beta"]

Checklist for Sound Encapsulation

  1. Place the type in its own small submodule so unrelated functions in the parent module cannot accidentally mutate private invariant-bearing fields.
  2. Enforce panic/unwind safety: Update length or state fields before running user code or destructors that could unwind (notice how self.len -= 1 runs before reading the popped value above).
  3. Never expose raw pointers or unvalidated &mut references to invariant-bearing fields.