25.2 Unsafe Blocks and Functions

Operations designated as unsafe can only be performed within contexts explicitly marked by the unsafe keyword.

25.2.1 Unsafe Blocks

An unsafe { ... } block isolates a segment of code containing one or more unsafe operations. This is the most common way to introduce unsafety. It signals that the code within the block might perform actions requiring manual safety verification.

A frequent use case is dereferencing raw pointers. While creating, passing, or comparing raw pointers is safe, reading from or writing to the memory they point to (*ptr) requires an unsafe block. This is because the compiler cannot guarantee that the pointer is valid (i.e., not null, dangling, properly aligned, or pointing to initialized memory of the correct type).

fn main() {
    let mut num: i32 = 42;
    // Creating a raw pointer from a valid reference is safe.
    let r_ptr: *mut i32 = &mut num;

    // Dereferencing the raw pointer requires an unsafe block.
    unsafe {
        println!("Value before: {}", *r_ptr);
        // Modify the value through the raw pointer.
        *r_ptr = 99;
        println!("Value after: {}", *r_ptr);
    }
    // The original variable reflects the change.
    println!("Final value of num: {}", num); // num is now 99
}

In this example, the operation is safe because r_ptr originates from a valid mutable reference &mut num. The unsafe block serves as an annotation that the programmer, not the compiler, is responsible for ensuring this validity.

25.2.2 Unsafe Functions

A function can be declared as unsafe fn if calling it requires the caller to satisfy certain preconditions (invariants) that the compiler cannot enforce through the type system or borrow checker alone. Such functions can perform unsafe operations internally without needing additional unsafe blocks for those specific operations.

However, calling an unsafe fn is itself an unsafe operation and must occur within an unsafe block or another unsafe fn.

// This function is unsafe because dereferencing `ptr` is only valid
// if the caller guarantees `ptr` points to valid, initialized memory.
unsafe fn read_from_pointer(ptr: *const i32) -> i32 {
    *ptr // Unsafe operation permitted directly within `unsafe fn`.
}

fn main() {
    let x = 42;
    let ptr = &x as *const i32;

    // Calling an unsafe function requires an unsafe block.
    let value = unsafe {
        read_from_pointer(ptr)
    };
    println!("Value read via unsafe fn: {}", value);
}

The unsafe keyword on the function signature acts as a contract: “Warning: This function relies on preconditions not checked by the compiler. Incorrect usage can lead to undefined behavior. Ensure you meet its documented requirements before calling.”

25.2.3 unsafe fn vs. unsafe Block

Choosing between an unsafe fn and an unsafe block inside a safe function depends on where the responsibility for safety lies:

  • Use unsafe fn when the function has preconditions that the caller must fulfill to ensure safety. Violating these preconditions, even if the function call type-checks, could lead to UB. Safety depends on the caller’s context.
  • Use an unsafe block inside a safe function (fn) when the function itself can guarantee that its internal unsafe operations are performed correctly, provided the function is called with arguments valid according to its safe signature. Safety is maintained by the function’s implementation.

Best Practice: Encapsulate unsafe operations within unsafe blocks inside safe functions whenever feasible. This minimizes the surface area of unsafety and presents a safe interface to the rest of the codebase. Reserve unsafe fn for interfaces where safety fundamentally depends on guarantees provided by the caller, often seen in FFI or low-level abstractions.