Unsafe Rust Safely
Need to use unsafe Rust? This guide covers when to use unsafe, raw pointers, safety invariants, and minimizing unsafe code.
Problem: Understanding When Unsafe is Needed
Scenario
You’re unsure if unsafe is necessary.
Solution: Know the Five Unsafe Powers
Unsafe allows you to:
- Dereference raw pointers
- Call unsafe functions
- Access mutable static variables
- Implement unsafe traits
- Access fields of unions
Use unsafe only when necessary:
- FFI (calling C code)
- Performance-critical optimizations
- Low-level system programming
- Implementing safe abstractions
Problem: Raw Pointers
Scenario
You need manual memory management or FFI.
Solution: Use Raw Pointers Carefully
fn main() {
let mut num = 5;
// Creating raw pointers is safe
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Dereferencing requires unsafe
unsafe {
println!("r1: {}", *r1);
*r2 = 10;
println!("r2: {}", *r2);
}
}Safety invariants:
- Pointer must be valid (non-null, aligned, pointing to allocated memory)
- For
*mut T, no other references can exist - Memory must not be freed while pointer is in use
Problem: Creating Safe Abstraction Over Unsafe Code
Scenario
You need unsafe internally but want safe public API.
Solution: Encapsulate Unsafe in Safe Functions
Unsafe split_at_mut:
use std::slice;
fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
assert!(mid <= slice.len());
let len = slice.len();
let ptr = slice.as_mut_ptr();
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut v, 3);
assert_eq!(left, &mut [1, 2, 3]);
assert_eq!(right, &mut [4, 5, 6]);
}Safety invariants documented:
midmust be <=slice.len()(enforced by assert)- Pointers are valid for the lifetimes
- Slices don’t overlap
Problem: Null Pointer Checks
Scenario
FFI returns potentially null pointers.
Solution: Always Check for Null
use std::ffi::CStr;
use std::os::raw::c_char;
extern "C" {
fn get_string() -> *const c_char;
}
fn safe_get_string() -> Option<String> {
unsafe {
let ptr = get_string();
if ptr.is_null() {
return None;
}
let c_str = CStr::from_ptr(ptr);
c_str.to_str().ok().map(|s| s.to_string())
}
}
fn main() {
match safe_get_string() {
Some(s) => println!("Got: {}", s),
None => println!("Null pointer"),
}
}Problem: Uninitialized Memory
Scenario
You need to work with uninitialized memory for performance.
Solution: Use MaybeUninit
use std::mem::MaybeUninit;
fn main() {
let mut values: [MaybeUninit<i32>; 5] = unsafe {
MaybeUninit::uninit().assume_init()
};
// Initialize values
for (i, value) in values.iter_mut().enumerate() {
value.write(i as i32 * 2);
}
// Convert to initialized array
let values: [i32; 5] = unsafe {
std::mem::transmute(values)
};
println!("{:?}", values); // [0, 2, 4, 6, 8]
}Better with transmute_copy:
use std::mem::{MaybeUninit, transmute_copy};
let values = unsafe {
transmute_copy::<_, [i32; 5]>(&values)
};Problem: Static Mutable State
Scenario
You need global mutable state.
Solution: Use Mutex or Atomic Types
Bad (data race):
static mut COUNTER: i32 = 0;
fn increment() {
unsafe {
COUNTER += 1; // Data race if called from multiple threads!
}
}Good (thread-safe):
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER: AtomicI32 = AtomicI32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst);
}With Mutex:
use std::sync::Mutex;
static COUNTER: Mutex<i32> = Mutex::new(0);
fn increment() {
let mut counter = COUNTER.lock().unwrap();
*counter += 1;
}Problem: Implementing Unsafe Trait
Scenario
You need to implement Send or Sync for your type.
Solution: Understand Safety Requirements
use std::marker::PhantomData;
struct MyType {
ptr: *mut i32,
_marker: PhantomData<i32>, // Not Send or Sync by default
}
// SAFETY: MyType owns the pointed-to data
// and ensures exclusive access
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}Document safety invariants:
- Why it’s safe to send across threads
- Why it’s safe to share references across threads
- What invariants are maintained
Problem: Transmute
Scenario
You need to reinterpret bytes.
Solution: Minimize Transmute Use
use std::mem;
fn main() {
// Transmute allows arbitrary type reinterpretation
let a: f32 = 1.0;
let b: u32 = unsafe { mem::transmute(a) };
println!("f32 {} as u32: {}", a, b);
}Safer alternatives:
// Instead of transmute for numeric conversions
let a: f32 = 1.0;
let b = a.to_bits(); // Safe method
// Instead of transmute for slices
let bytes: &[u8] = unsafe {
std::slice::from_raw_parts(ptr as *const u8, len)
};
// Use bytemuck for safe transmutes
use bytemuck::{Pod, Zeroable};
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct MyStruct {
x: u32,
y: u32,
}
let bytes: &[u8] = bytemuck::bytes_of(&my_struct);Problem: Inline Assembly
Scenario
You need assembly for performance or low-level operations.
Solution: Use asm! Macro (Nightly)
#![feature(asm)]
use std::arch::asm;
fn main() {
let mut x: u64 = 4;
unsafe {
asm!(
"add {0}, {0}", // x = x + x
inout(reg) x,
);
}
assert_eq!(x, 8);
}Document:
- What the assembly does
- Why it’s safe
- Platform requirements
Problem: Safety Invariants Documentation
Scenario
Your unsafe code needs clear safety requirements.
Solution: Document All Invariants
/// Creates a new Vec from raw parts
///
/// # Safety
///
/// This function is unsafe because:
/// - `ptr` must be allocated via `Vec`/`String`
/// - `ptr` must be aligned and non-null
/// - `ptr` must point to `capacity` consecutive properly initialized values of type `T`
/// - The allocated size must be exactly `capacity * size_of::<T>()`
/// - `length` must be less than or equal to `capacity`
/// - The allocated memory must not be freed for lifetime of the returned Vec
/// - Caller must not use the pointer after this call
unsafe fn from_raw_parts<T>(ptr: *mut T, length: usize, capacity: usize) -> Vec<T> {
// Implementation
}Problem: Minimizing Unsafe Blocks
Scenario
You want to reduce amount of unsafe code.
Solution: Minimize Unsafe Scope
Bad (large unsafe block):
unsafe {
let ptr = malloc(100);
if ptr.is_null() {
return Err("allocation failed");
}
memset(ptr, 0, 100);
let data = process_data();
write_to_ptr(ptr, data);
free(ptr);
}Good (minimal unsafe blocks):
let ptr = unsafe { malloc(100) };
if ptr.is_null() {
return Err("allocation failed");
}
unsafe { memset(ptr, 0, 100) };
let data = process_data(); // Safe code
unsafe {
write_to_ptr(ptr, data);
free(ptr);
}Problem: Testing Unsafe Code
Scenario
You need to verify unsafe code is correct.
Solution: Extensive Testing and Miri
Unit tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_at_mut() {
let mut v = vec![1, 2, 3, 4, 5];
let (left, right) = split_at_mut(&mut v, 2);
assert_eq!(left, &[1, 2]);
assert_eq!(right, &[3, 4, 5]);
left[0] = 10;
right[0] = 30;
assert_eq!(v, vec![10, 2, 30, 4, 5]);
}
#[test]
#[should_panic]
fn test_split_at_mut_panic() {
let mut v = vec![1, 2, 3];
split_at_mut(&mut v, 10); // Should panic
}
}Miri (undefined behavior detector):
cargo +nightly miri testCommon Pitfalls
Pitfall 1: Assuming Validity
Problem: Not validating pointer assumptions.
// Bad
unsafe {
*ptr = value; // What if ptr is null or invalid?
}Solution: Always validate.
// Good
assert!(!ptr.is_null());
assert!(ptr as usize % align_of::<T>() == 0);
unsafe {
*ptr = value;
}Pitfall 2: Data Races
Problem: Mutable static without synchronization.
Solution: Use atomics or Mutex.
Pitfall 3: Lifetime Assumptions
Problem: Assuming pointer outlives data.
Solution: Use lifetimes in safe wrapper.
struct Wrapper<'a> {
ptr: *const i32,
_marker: PhantomData<&'a i32>,
}Related Resources
- Tutorials: Advanced - Unsafe Rust coverage
- FFI Interop - Unsafe in FFI context
- Best Practices - Safe unsafe patterns
- Rustonomicon - Unsafe Rust deep dive
Write unsafe Rust code that maintains safety guarantees!