Step 6 of 6
When (not) to use unsafe at work
Here's the honest picture: most Rust developers write little or no unsafe, and many application teams have none in their own code at all. The unsafe code your programs rely on lives in std, Tokio, hashbrown and similar crates, where specialists write it, review it and test it with Miri and fuzzers. You get the speed without the risk.
When unsafe is (rarely) right
- FFI: calling a C library or an OS API that has no safe crate yet.
- Low-level data structures that can't be expressed with references, when no existing crate fits (a custom arena allocator, a lock-free queue).
- Hardware: memory-mapped registers in embedded code.
- Performance, but only when profiling proves a hot spot and the safe version can't be made fast. This is rarer than it sounds: iterators and an
assert!up front usually let the compiler remove bounds checks on their own (see the performance module).
A checklist before writing unsafe
- Is there a safe API? Most "I need unsafe" moments have one:
| You want to... | Safe tool |
|---|---|
two &mut into one slice |
split_at_mut, chunks_mut, get_disjoint_mut |
swap or move out of a &mut |
std::mem::swap, mem::take, mem::replace |
| mutate through a shared reference | Cell, RefCell, Mutex, atomics |
| reinterpret bytes as numbers | u32::from_le_bytes, f32::from_bits, or the bytemuck / zerocopy crates |
| skip a bounds check | iterators, chunks_exact, an assert! before the loop |
| a global counter | an atomic in a static |
- Is there a well-tested crate that already wraps it?
- Did you measure that it's worth it?
- Can you keep it tiny? One small module, a safe public API,
assert!s that make it sound. - Is every block documented? A
// SAFETY:comment per block, a# Safetysection perunsafe fn. - Is it tested? Unit tests run under Miri in CI, and a second reviewer who knows unsafe Rust.
Tooling that keeps unsafe in check
Put #![forbid(unsafe_code)] at the top of your crate (in main.rs or lib.rs) and the compiler rejects any unsafe in it, permanently:
#![forbid(unsafe_code)]
fn main() {
let x = 5;
let p = &raw const x;
println!("{}", unsafe { *p });
}
error: usage of an `unsafe` block
Many teams set it on every crate except one small, well-reviewed module. Also useful: clippy's undocumented_unsafe_blocks lint, the unsafe_op_in_unsafe_fn lint (on by default since edition 2024), and cargo geiger, which counts the unsafe code in your dependencies.
The safe versions are usually shorter
use std::mem;
struct Player {
name: String,
inventory: Vec<String>,
}
fn main() {
let mut a = 1;
let mut b = 2;
mem::swap(&mut a, &mut b);
println!("{a} {b}");
let mut p = Player { name: "Ferris".to_string(), inventory: vec!["map".to_string()] };
let old = mem::take(&mut p.inventory); // moves the Vec out, leaves an empty one
println!("{} had {:?}, now {:?}", p.name, old, p.inventory);
let bytes = [0x2a, 0x00, 0x00, 0x00];
println!("{}", u32::from_le_bytes(bytes));
}
2 1
Ferris had ["map"], now []
42
mem::take replaces a value with its Default and hands you the old one, which is how you "move out of a &mut" without ptr::read. mem::replace(&mut x, new) does the same with a value you choose.
Your task
A teammate wrote rotate_left_by_one with ptr::read, ptr::copy and ptr::write. It works, but every reader now has to verify the pointer math. Rewrite it with no unsafe, using the slice method rotate_left. Keep the empty-slice case working (rotate_left(1) panics on an empty slice).
Challenge 2
take_name moves the name out of a Player and leaves an empty name behind, using ptr::read and ptr::write. That's correct only because the two lines are in exactly this order with nothing in between that could panic; one careless edit and the String gets freed twice. Rewrite it with std::mem::take and no unsafe.
Challenge 3
read_u32_le reads the first four bytes of a buffer as a little-endian u32 by casting the byte pointer to *const u32. That's two bugs: it never checks the buffer has four bytes, and a u32 must be read from an address divisible by 4, which a byte slice doesn't guarantee (debug builds abort with misaligned pointer dereference). Rewrite it safely: return None if there are fewer than 4 bytes, otherwise Some(u32::from_le_bytes(...)).