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
unsafe fn declares that a function has unchecked preconditions# Safety documentation in doc comments specifies the preconditionsunsafe fn is different from a regular function with an unsafe {} blockunsafe fn appears: std library internals, allocator APIs, SIMD intrinsicsCode 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
unsafe fn makes the caller's opt-in explicit at the call site; OCaml preconditions are purely documentary with no enforcement.cargo audit and cargo geiger can count unsafe fn call sites; there is no OCaml equivalent.unsafe fn; OCaml APIs expose functions with documented preconditions.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
unsafe fn get_unchecked(slice: &[i32], idx: usize) -> i32 and document its precondition — then write a safe wrapper that validates the index.std::slice::from_raw_parts, read the documentation and list all preconditions — then write a safe wrapper that validates as many as possible.raw_copy to accept NonNull<u8> instead of *const u8/*mut u8 — how does this change the preconditions and safety documentation?