Introduction
Unsafe Rust allows you to disable certain safety checks when necessary. It’s not about writing dangerous codeโit’s about writing code whose safety Rust’s compiler cannot verify automatically.
This guide covers when, where, and how to use unsafe Rust correctly.
The Purpose of Unsafe
What Unsafe Does NOT Do
// โ MYTH: unsafe disables safety checks entirely
// โ
TRUTH: unsafe lets you assert safety Rust can't prove
unsafe {
let x = 5;
let y = x + 1; // Still safe - no bounds checking needed
}
// โ MYTH: unsafe code is faster
// โ
TRUTH: unsafe only removes checks Rust couldn't optimize away
What Unsafe Enables
Safe Rust prevents:
โ
Data races
โ
Buffer overflows
โ
Use-after-free
โ
Null pointer dereferences
โ
Type confusion
Unsafe allows accessing:
โ ๏ธ Raw pointers
โ ๏ธ Calling unsafe functions
โ ๏ธ Mutating statics
โ ๏ธ Implementing unsafe traits
Raw Pointers
Creating Raw Pointers
let x = 5;
let raw_ptr: *const i32 = &x as *const i32;
let mut y = 10;
let mutable_raw: *mut i32 = &mut y as *mut i32;
// Raw pointers can be null
let null_ptr: *const i32 = std::ptr::null();
let null_mut: *mut i32 = std::ptr::null_mut();
Dereferencing Raw Pointers
let x = 5;
let raw_ptr: *const i32 = &x as *const i32;
// Dereferencing requires unsafe
unsafe {
println!("Value: {}", *raw_ptr); // OK
}
// This is undefined behavior:
let dangling = unsafe {
let local = 5;
&local as *const i32 // Pointer to stack variable
};
unsafe {
println!("{}", *dangling); // UB: stack memory freed
}
Pointer Arithmetic
let arr = [1, 2, 3, 4, 5];
let ptr = arr.as_ptr();
unsafe {
println!("{}", *ptr); // 1
println!("{}", *ptr.add(2)); // 3
println!("{}", *ptr.add(4)); // 5
// println!("{}", *ptr.add(5)); // Out of bounds!
}
Unsafe Functions
Defining Unsafe Functions
// Function that requires careful preconditions
unsafe fn dangerous_function(ptr: *const i32) -> i32 {
*ptr // Assumes ptr is valid and aligned
}
fn main() {
let x = 5;
let result = unsafe {
dangerous_function(&x as *const i32)
};
println!("{}", result);
}
When to Use Unsafe Functions
// Good: Document the safety preconditions
/// Splits a slice at an index without bounds checking.
///
/// # Safety
/// The caller must ensure `index <= len(slice)`
/// Otherwise undefined behavior will occur.
unsafe fn split_at_unchecked<T>(
slice: &[T],
index: usize
) -> (&[T], &[T]) {
let ptr = slice.as_ptr();
(
std::slice::from_raw_parts(ptr, index),
std::slice::from_raw_parts(ptr.add(index), slice.len() - index),
)
}
// Safe wrapper
fn split_at_safe<T>(slice: &[T], index: usize) -> (&[T], &[T]) {
assert!(index <= slice.len(), "index out of bounds");
unsafe { split_at_unchecked(slice, index) }
}
Foreign Function Interface (FFI)
Calling C from Rust
// Declare C function
extern "C" {
fn strlen(s: *const u8) -> usize;
fn printf(fmt: *const u8, ...) -> i32;
}
fn main() {
unsafe {
let c_string = b"Hello\0";
let len = strlen(c_string.as_ptr());
println!("Length: {}", len);
}
}
Calling Rust from C
// In Rust library code
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
// Callable from C:
// int add_numbers(int a, int b);
Safe FFI Wrapper
use std::ffi::CStr;
extern "C" {
fn strlen(s: *const u8) -> usize;
}
// Safe Rust wrapper
fn rust_strlen(s: &CStr) -> usize {
unsafe { strlen(s.as_ptr() as *const u8) }
}
fn main() {
let c_str = CStr::from_bytes_with_nul(b"Hello\0").unwrap();
let len = rust_strlen(c_str);
println!("Length: {}", len);
}
Mutable Statics
Interior Mutability with Statics
static COUNTER: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(0);
fn increment_counter() {
COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
fn main() {
increment_counter();
let value = COUNTER.load(std::sync::atomic::Ordering::SeqCst);
println!("Counter: {}", value);
}
Mutable Static (Dangerous)
static mut GLOBAL_STATE: i32 = 0;
fn set_global(value: i32) {
unsafe {
GLOBAL_STATE = value;
}
}
fn get_global() -> i32 {
unsafe {
GLOBAL_STATE
}
}
fn main() {
set_global(42);
println!("Global: {}", get_global());
}
// โ ๏ธ Data races possible with threads!
Unsafe Traits
Implementing Unsafe Traits
// A trait that can be unsafe to implement
unsafe trait Send {}
// Only implement if truly safe
struct MyType;
unsafe impl Send for MyType {}
// This won't compile without unsafe:
// impl Send for MyType {} // ERROR
Common Unsafe Traits
// Types that are safe to send between threads
unsafe trait Send {}
// Types safe to share references across threads
unsafe trait Sync {}
// Usually auto-implemented by the compiler
// But can implement manually for custom types
Real-World Example: Memory Pool
/// Thread-safe memory pool using unsafe
pub struct MemoryPool {
buffer: Vec<u8>,
allocations: std::sync::Mutex<Vec<(usize, usize)>>,
}
impl MemoryPool {
pub fn new(size: usize) -> Self {
MemoryPool {
buffer: vec![0; size],
allocations: std::sync::Mutex::new(Vec::new()),
}
}
pub fn allocate(&self, size: usize) -> Option<*mut [u8]> {
let mut allocs = self.allocations.lock().unwrap();
let mut offset = 0;
for (alloc_offset, alloc_size) in allocs.iter() {
if *alloc_offset - offset >= size {
// Found gap
allocs.push((offset, size));
unsafe {
let ptr = self.buffer.as_ptr().add(offset) as *mut u8;
return Some(std::slice::from_raw_parts_mut(ptr, size));
}
}
offset = alloc_offset + alloc_size;
}
None
}
pub fn deallocate(&self, offset: usize) {
let mut allocs = self.allocations.lock().unwrap();
allocs.retain(|(o, _)| *o != offset);
}
}
Checking Unsafe Code
Common Mistakes
// โ WRONG: Undefined behavior
fn bad_example() {
let x = 5;
let ptr = &x as *const i32;
// x is dropped here
unsafe {
println!("{}", *ptr); // UB: use-after-free
}
}
// โ
CORRECT: Verify validity
fn good_example() {
let x = 5;
let ptr = &x as *const i32;
unsafe {
println!("{}", *ptr); // OK: x still in scope
}
}
Safety Documentation Template
/// # Safety
/// The caller must ensure:
/// - `ptr` is valid and properly aligned
/// - `ptr` points to initialized data
/// - No other mutable references to this data exist
/// - The operation won't cause integer overflow
unsafe fn unsafe_operation(ptr: *const i32) -> i32 {
*ptr
}
Performance Impact
Unsafe Optimization Example
// Safe version (bounds checked)
fn safe_sum(arr: &[i32]) -> i32 {
arr.iter().sum()
}
// Unsafe version (no bounds check)
fn unsafe_sum(arr: &[i32]) -> i32 {
let mut sum = 0;
let mut ptr = arr.as_ptr();
let end = unsafe { ptr.add(arr.len()) };
unsafe {
while ptr != end {
sum += *ptr;
ptr = ptr.add(1);
}
}
sum
}
// Benchmark: 99% identical performance after optimization
// Rust's optimizer can often remove redundant bounds checks
Best Practices
Do’s โ
โ
Use unsafe for FFI (C interop)
โ
Encapsulate unsafe in safe abstractions
โ
Document all safety requirements
โ
Use `#[cfg(test)]` to test unsafe code
โ
Consider using existing safe crates instead
โ
Minimize unsafe blocks
Don’ts โ
โ Don't use unsafe to bypass type system
โ Don't ignore compiler warnings
โ Don't assume performance without benchmarking
โ Don't write unsafe without documentation
โ Don't leak abstractions (expose raw pointers)
โ Don't use unsafe for style
Testing Unsafe Code
Unit Tests for Unsafe
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_wrapper() {
let arr = [1, 2, 3, 4, 5];
let (left, right) = split_at_safe(&arr, 2);
assert_eq!(left, &[1, 2]);
assert_eq!(right, &[3, 4, 5]);
}
#[test]
#[should_panic]
fn test_bounds_check() {
let arr = [1, 2, 3];
split_at_safe(&arr, 10); // Should panic
}
}
Glossary
- Unsafe: Code block that disables some safety checks
- Raw Pointer: Direct memory address without ownership
- FFI: Foreign Function Interface (calling C/other languages)
- Undefined Behavior: Violation of safety guarantees
- Precondition: Requirement caller must guarantee
Comments