ExamplesBy LevelBy TopicLearning Paths
701 Advanced

Unsafe Functions

Functional Programming

Tutorial

The Problem

An unsafe fn declaration means "calling this function has preconditions that the compiler cannot verify — the caller is responsible for upholding them." This is different from an unsafe {} block inside a function body. unsafe fn shifts the burden of proof to the caller and makes the unsafety visible at every call site. The safe-wrapper idiom pairs unsafe fn with a public safe function that validates the preconditions before calling the unsafe version.

🎯 Learning Outcomes

  • • How unsafe fn declares that a function has unchecked preconditions
  • • How # Safety documentation in doc comments specifies the preconditions
  • • How the safe-wrapper idiom hides unsafe fn behind a validated public API
  • • Why unsafe fn is different from a regular function with an unsafe {} block
  • • Where unsafe fn appears: std library internals, allocator APIs, SIMD intrinsics
  • Code Example

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

    Key Differences

  • Compile-time enforcement: Rust unsafe fn makes the caller's opt-in explicit at the call site; OCaml preconditions are purely documentary with no enforcement.
  • Audit tool: cargo audit and cargo geiger can count unsafe fn call sites; there is no OCaml equivalent.
  • API design: Rust APIs prefer safe wrappers hiding unsafe fn; OCaml APIs expose functions with documented preconditions.
  • Standard library: Rust std has many unsafe fn with safe wrappers (e.g., String::from_utf8_unchecked vs from_utf8); OCaml stdlib validates inputs in safe functions.
  • OCaml Approach

    OCaml has no unsafe fn concept — all functions are safe by default. Preconditions are documented via comments and enforced by convention:

    (** [raw_copy src dst n]
        Precondition: src and dst have at least n bytes; they must not overlap. *)
    let raw_copy src dst n = Bytes.blit src 0 dst 0 n
    

    The compiler does not enforce these preconditions — they are purely documentary.

    Full Source

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

    Exercises

  • Write an unsafe fn: Implement unsafe fn get_unchecked(slice: &[i32], idx: usize) -> i32 and document its precondition — then write a safe wrapper that validates the index.
  • # Safety audit: For std::slice::from_raw_parts, read the documentation and list all preconditions — then write a safe wrapper that validates as many as possible.
  • Precondition encoding: Redesign raw_copy to accept NonNull<u8> instead of *const u8/*mut u8 — how does this change the preconditions and safety documentation?
  • Open Source Repos