๐Ÿฆ€ Functional Rust

700: Unsafe Block

Difficulty: 4 Level: Expert Minimise the `unsafe` footprint โ€” do only what must be unsafe, keep everything else safe.

The Problem This Solves

The Rust compiler enforces memory safety by default, but some operations genuinely cannot be verified statically: dereferencing a raw pointer, calling a C function, mutating a global variable, implementing certain marker traits, or accessing a union field. For exactly these five operations, Rust provides the `unsafe` block โ€” a small, explicitly labelled region where you take over the compiler's job. The key discipline is containment. An `unsafe` block is not a license to write arbitrary dangerous code throughout a function โ€” it is a precise surgical incision. Everything that can be expressed safely should remain outside the `unsafe` block. The smaller the unsafe region, the smaller the surface area you must reason about and audit. unsafe is a tool, not a crutch โ€” use only when safe Rust genuinely can't express the pattern.

The Intuition

An `unsafe` block is a contract between you and the compiler. You are saying: "I've checked this manually. The invariants hold. Trust me here." The compiler records the boundary in the source code so auditors, future maintainers, and tools like `cargo geiger` can find exactly what needs human review. The five operations that only `unsafe` enables: 1. Dereference a raw pointer 2. Call an `unsafe fn` 3. Implement an `unsafe trait` 4. Mutate a `static mut` variable 5. Access a union field Every other Rust operation โ€” iterators, closures, arithmetic, string formatting โ€” is always safe and belongs outside the block.

How It Works in Rust

static mut GLOBAL_COUNTER: u64 = 0;

fn increment() {
 unsafe {
     // SAFETY: Single-threaded; no concurrent access to GLOBAL_COUNTER.
     // In multi-threaded code, replace with AtomicU64.
     GLOBAL_COUNTER += 1;
 }
 // Safe side-effects live OUTSIDE the unsafe block.
 // Logging, formatting, error handling โ€” all stay here.
}

fn reset() {
 unsafe {
     // SAFETY: Same single-threaded guarantee.
     GLOBAL_COUNTER = 0;
 }
 // โ† println! is safe; it goes here, not inside unsafe.
 println!("Counter reset to 0.");
}
The pattern to internalise: shrink the `unsafe` block to the minimum number of lines that genuinely require it. Bounds-check before the block, format strings after the block, validate return values after the block.

What This Unlocks

Key Differences

ConceptOCamlRust
Unsafe regionNo explicit marker; `Obj.magic` is always "trusted"`unsafe { }` block โ€” compiler-enforced boundary
Mutable global`let x = ref 0` (always fine)`static mut` requires `unsafe` to read or write
AuditabilitySearch for known unsafe patterns by convention`cargo geiger` counts `unsafe` blocks automatically
Scope of trustEntire modulePrecisely the `unsafe { }` block
Safe defaultType system doesn't express safetySafe code is the default; unsafe is the opt-in exception
//! 700 โ€” Unsafe Blocks
//! Keep unsafe footprint minimal: only what truly needs it.

static mut GLOBAL_COUNTER: u64 = 0;

/// Increment the global counter โ€” smallest possible unsafe block.
fn increment() {
    unsafe {
        // SAFETY: Single-threaded; no concurrent access to GLOBAL_COUNTER.
        // In multi-threaded code, use AtomicU64 instead.
        GLOBAL_COUNTER += 1;
    }
    // โ† Safe code (logging, side-effects) lives OUTSIDE the unsafe block.
}

fn get() -> u64 {
    unsafe {
        // SAFETY: Same single-threaded guarantee.
        GLOBAL_COUNTER
    }
}

fn reset() {
    unsafe {
        // SAFETY: Same single-threaded guarantee.
        GLOBAL_COUNTER = 0;
    }
    // Safe operations after the minimal unsafe block
    println!("Counter reset to 0.");
}

fn main() {
    for _ in 0..5 { increment(); }
    println!("After 5 increments: {}", get());
    reset();
    println!("After reset: {}", get());

    // Show that safe code stays completely outside unsafe blocks
    let values: Vec<u32> = (1..=10).collect();
    let sum: u32 = values.iter().sum();     // entirely safe
    println!("Safe sum: {sum}");

    // Demonstrate accessing mutable static directly (the unsafe part)
    let snapshot = unsafe {
        // SAFETY: Single-threaded; reading a u64 is atomic on all supported targets.
        GLOBAL_COUNTER
    };
    println!("Snapshot: {snapshot}");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_counter_lifecycle() {
        reset();
        assert_eq!(get(), 0);
        increment();
        increment();
        assert_eq!(get(), 2);
        reset();
        assert_eq!(get(), 0);
    }

    #[test]
    fn test_safe_code_outside_unsafe() {
        // Demonstrate safe code compiles and works without unsafe
        let v = vec![1u32, 2, 3];
        assert_eq!(v.iter().sum::<u32>(), 6);
    }
}
(* OCaml: all memory access is safe by construction.
   Module boundaries and the type system enforce invariants automatically. *)

(** A simple mutable counter without any unsafe code. *)
let counter = ref 0

let increment () = incr counter
let get_count () = !counter
let reset ()     = counter := 0

let () =
  for _ = 1 to 5 do increment () done;
  Printf.printf "Count after 5 increments: %d\n" (get_count ());
  reset ();
  Printf.printf "Count after reset: %d\n" (get_count ())