Course contentsShow
← Rust
Lesson 725 of 1,9106. Generics and Static DispatchFree lesson

Unsafe Blocks in Generic Contexts

Properly encapsulate unsafe memory manipulation within generic containers using trait safety guarantees.

In this lesson, you will learn how to safely wrap low-level memory operations inside generic data structures by leveraging trait bounds to enforce invariant correctness.

When building generic containers (like a custom Vec<T>), you often reach a point where the compiler cannot guarantee memory safety, such as when manually allocating uninitialized memory. Because T is generic, you don't know if T requires drop glue, if it is safe to move, or if it satisfies specific memory requirements. This is where unsafe blocks become necessary, but they must be carefully constrained.

The Strategy: Trait-Bound Enforcement

To keep your unsafe code sound, you must demand that the type T fulfills specific criteria before allowing it into your container. If your unsafe block relies on the assumption that a type can be safely copied via ptr::copy, you should enforce this with a trait bound.

Consider this minimal example:

use std::ptr;

struct MyBox<T> {
    ptr: *mut T,
}

impl<T> MyBox<T> {
    pub fn new(value: T) -> Self {
        // We use unsafe to allocate, but we wrap it in a safe API
        let layout = std::alloc::Layout::new::<T>();
        let ptr = unsafe { std::alloc::alloc(layout) as *mut T };
        unsafe { ptr::write(ptr, value) };
        MyBox { ptr }
    }

    pub fn take(self) -> T {
        // We assume T is safe to read out of raw memory
        unsafe { ptr::read(self.ptr) }
    }
}

Encapsulation Rules

The key to writing this safely is twofold:

  1. Keep it Private: Never expose raw pointers or the internal unsafe memory layout to the user of your generic type. The unsafe logic should be entirely contained within the implementation of your struct, while the public methods remain entirely safe.
  2. Trait Bounds as Guards: If your unsafe block requires T to be Copy or Send, add those bounds to your impl block. This prevents users from passing types that would violate the memory assumptions made inside your unsafe code. By forcing the user to satisfy these bounds, you effectively move the "unsafe" burden from the container logic to the type signature, where the compiler can verify it.

Key Takeaway: Always encapsulate unsafe logic behind a safe, generic API and use trait bounds to explicitly define the invariants that T must satisfy to keep that memory operation sound.