ExamplesBy LevelBy TopicLearning Paths
700 Advanced

Unsafe Blocks

Functional Programming

Tutorial

The Problem

unsafe blocks are Rust's explicit opt-out from the borrow checker for a specific scope. The fundamental principle: minimize the unsafe footprint. Only the code that genuinely requires unsafe operations should be inside unsafe { }. Safe code (error handling, logging, computation) belongs outside. This discipline makes auditing easier — reviewers can focus on the narrow unsafe region rather than an entire function. It is a core practice in systems programming with Rust.

🎯 Learning Outcomes

  • • How unsafe { } scopes the suspension of safety guarantees to a minimal region
  • • Why safe code before and after the unsafe block is still fully checked
  • • How static mut requires unsafe access and why AtomicU64 is usually better
  • • How the size of the unsafe region affects auditability
  • • The Rust convention: document every unsafe block with a // SAFETY: comment
  • Code Example

    #![allow(clippy::all)]
    //! 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.");
    }
    
    #[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);
        }
    }

    Key Differences

  • Explicit opt-out: Rust makes unsafety visible at the call site with unsafe { }; OCaml's safety violations (Obj.magic) are equally visible but less common.
  • Audit surface: Minimizing unsafe blocks makes security audits tractable — tools like cargo geiger count unsafe lines; OCaml has no equivalent metric.
  • **static mut**: Rust's static mut is inherently unsafe; OCaml's global ref is always safe (GC-managed, no data races in single-threaded mode).
  • Documentation requirement: The Rust community expects SAFETY comments in all unsafe blocks; this is a code review standard, not a compiler requirement.
  • OCaml Approach

    OCaml has no unsafe blocks — all code is uniformly safe (with the exception of Obj.magic and direct C FFI):

    let global_counter = ref 0
    let increment () = incr global_counter
    let get () = !global_counter
    (* For thread safety: use Mutex.t or Atomic.t *)
    

    Full Source

    #![allow(clippy::all)]
    //! 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.");
    }
    
    #[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);
        }
    }
    ✓ Tests Rust test suite
    #[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);
        }
    }

    Exercises

  • Minimize unsafe: Take a function that wraps raw pointer access and refactor it to move all safe operations (validation, error creation, logging) outside the unsafe block.
  • AtomicU64 replacement: Rewrite the global counter example using std::sync::atomic::AtomicU64 — compare the code size and verify thread safety.
  • SAFETY documentation: For each unsafe block in the source, write the complete SAFETY comment explaining: what invariant is required, why it holds here, and what would break if it violated.
  • Open Source Repos