๐Ÿฆ€ Functional Rust

701: Unsafe Functions

Difficulty: 4 Level: Expert Declare `unsafe fn` to push safety contracts to the caller, then wrap them in safe APIs.

The Problem This Solves

Some operations have preconditions the compiler cannot check โ€” "this index is in bounds", "these memory regions don't overlap", "this pointer has been initialised". You could document them in a comment and hope callers comply, but Rust provides a better mechanism: `unsafe fn`. Marking a function `unsafe` makes the contract visible in the type system โ€” callers must write an `unsafe` block to invoke it, forcing them to acknowledge the preconditions exist. The safe-wrapper idiom builds on top of this. You write a private `unsafe fn` that is fast and unchecked, then a public safe function that validates the preconditions first. Callers interact only with the safe API; the `unsafe fn` is an implementation detail. This is exactly how `slice::get_unchecked` and `ptr::copy_nonoverlapping` work in the standard library. unsafe is a tool, not a crutch โ€” use only when safe Rust genuinely can't express the pattern.

The Intuition

Think of an `unsafe fn` as a professional power tool: it does the job faster than the consumer version, but it ships without the guard rail. The `// # Safety` doc section is the user manual โ€” it lists every condition that must be true before you press the trigger. A safe wrapper is the guard rail: it checks those conditions before handing control to the dangerous function. The compiler does not verify `// # Safety` comments. It only verifies that the caller acknowledged the contract by writing `unsafe { }`. The human โ€” you, the reviewer, the auditor โ€” reads the comment and decides whether the acknowledgement is justified.

How It Works in Rust

/// Copy `n` bytes from `src` to `dst`.
///
/// # Safety
/// - `src` must be valid for `n` bytes of reads.
/// - `dst` must be valid for `n` bytes of writes.
/// - The two regions must not overlap.
unsafe fn raw_copy(src: *const u8, dst: *mut u8, n: usize) {
 for i in 0..n {
     // SAFETY: Caller guarantees validity, non-overlap, and correct size.
     *dst.add(i) = *src.add(i);
 }
}

/// Safe wrapper: validates slice lengths, then delegates to raw_copy.
pub fn safe_copy(src: &[u8], dst: &mut [u8]) -> Result<(), String> {
 if src.len() != dst.len() {
     return Err(format!("length mismatch: {} vs {}", src.len(), dst.len()));
 }
 unsafe {
     // SAFETY: Both slices are valid for their full length.
     // Rust's borrow checker guarantees &[u8] and &mut [u8] cannot alias.
     raw_copy(src.as_ptr(), dst.as_mut_ptr(), src.len());
 }
 Ok(())
}
The structure is always: (1) document preconditions in `# Safety`, (2) implement the fast path, (3) write a safe wrapper that enforces those preconditions before calling through.

What This Unlocks

Key Differences

ConceptOCamlRust
Precondition declaration`[@warning "-8"]` or doc comment`unsafe fn` + `# Safety` doc section
Caller acknowledgementNone requiredMust write `unsafe { }` to call
Unchecked array access`Array.unsafe_get``slice::get_unchecked` (unsafe fn)
Safe wrapper patternChecked wrapper calls unchecked`pub fn` validates, then calls `unsafe fn`
Compiler enforcementNoYes โ€” calling `unsafe fn` without `unsafe` is a compile error
//! 701 โ€” Unsafe Functions
//! unsafe fn declarations and the safe-wrapper idiom.

/// Copy `n` bytes from `src` to `dst` without bounds checking.
///
/// # Safety
/// - `src` must be valid for `n` bytes of reads.
/// - `dst` must be valid for `n` bytes of writes.
/// - The two regions must not overlap.
/// - Both pointers must be aligned for `u8` (alignment 1 โ€” always satisfied).
unsafe fn raw_copy(src: *const u8, dst: *mut u8, n: usize) {
    for i in 0..n {
        // SAFETY: Caller guarantees src and dst are valid for n bytes,
        // non-overlapping, and aligned.
        *dst.add(i) = *src.add(i);
    }
}

/// Safe wrapper: validates slice lengths, then calls `raw_copy`.
pub fn safe_copy(src: &[u8], dst: &mut [u8]) -> Result<(), String> {
    if src.len() != dst.len() {
        return Err(format!(
            "length mismatch: src={} dst={}",
            src.len(), dst.len()
        ));
    }
    unsafe {
        // SAFETY: Both slices are valid for their full length.
        // Rust's borrow rules guarantee &[u8] and &mut [u8] cannot alias.
        raw_copy(src.as_ptr(), dst.as_mut_ptr(), src.len());
    }
    Ok(())
}

/// Get the element at index without bounds check.
///
/// # Safety
/// `idx` must be less than `slice.len()`.
unsafe fn get_unchecked<T: Copy>(slice: &[T], idx: usize) -> T {
    // SAFETY: Caller guarantees idx < slice.len().
    *slice.as_ptr().add(idx)
}

/// Safe wrapper: bounds-checks before calling get_unchecked.
pub fn safe_get<T: Copy>(slice: &[T], idx: usize) -> Option<T> {
    if idx < slice.len() {
        Some(unsafe {
            // SAFETY: idx < slice.len() confirmed above.
            get_unchecked(slice, idx)
        })
    } else {
        None
    }
}

fn main() {
    let src = b"Hello, Rust!";
    let mut dst = vec![0u8; src.len()];
    safe_copy(src, &mut dst).expect("copy failed");
    println!("Copied: {}", std::str::from_utf8(&dst).unwrap());

    match safe_copy(src, &mut vec![0u8; 3]) {
        Ok(_) => println!("unexpected ok"),
        Err(e) => println!("Expected error: {e}"),
    }

    let nums = [10, 20, 30, 40];
    println!("safe_get(2) = {:?}", safe_get(&nums, 2));
    println!("safe_get(9) = {:?}", safe_get(&nums, 9));
}

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

    #[test]
    fn test_safe_copy() {
        let src = b"abcde";
        let mut dst = vec![0u8; 5];
        assert!(safe_copy(src, &mut dst).is_ok());
        assert_eq!(&dst, b"abcde");
    }

    #[test]
    fn test_safe_copy_mismatch() {
        assert!(safe_copy(b"abc", &mut vec![0u8; 5]).is_err());
    }

    #[test]
    fn test_safe_get() {
        let v = [1i32, 2, 3];
        assert_eq!(safe_get(&v, 0), Some(1));
        assert_eq!(safe_get(&v, 2), Some(3));
        assert_eq!(safe_get(&v, 3), None);
    }
}
(* OCaml: no unsafe functions โ€” safety is guaranteed by the type system.
   We model "low-level" access with validation wrappers. *)

(** Array.unsafe_get skips bounds check โ€” the OCaml unsafe equivalent. *)
let unchecked_get (arr : 'a array) (i : int) : 'a = Array.unsafe_get arr i

(** Safe wrapper validates before delegating to unchecked_get. *)
let safe_get (arr : 'a array) (i : int) : 'a option =
  if i >= 0 && i < Array.length arr then Some (unchecked_get arr i)
  else None

let () =
  let data = [| 100; 200; 300 |] in
  (match safe_get data 1 with
   | Some v -> Printf.printf "safe_get(1) = %d\n" v
   | None   -> print_endline "out of bounds");
  (match safe_get data 5 with
   | Some _ -> print_endline "Should not happen"
   | None   -> print_endline "safe_get(5) = out of bounds (caught safely)")