One of Rust’s primary goals is to provide compile-time memory safety guarantees without needing a garbage collector. However, there are situations where the compiler’s rules are too restrictive or when you need to interface with lower-level systems that Rust can’t verify. For these cases, Rust provides an escape hatch: the unsafe keyword.
Using unsafe does not turn off the borrow checker or disable all of Rust’s safety features. Instead, it allows you to access a few extra “superpowers” that the compiler cannot statically prove are safe. When you write an unsafe block, you are telling the compiler, “I have read the documentation, I understand the risks, and I guarantee that the code inside this block is correct.”
The goal is to minimize the amount of unsafe code and encapsulate it within a safe, public API, so that consumers of your code don’t have to worry about the invariants you are manually upholding.
The Five Unsafe Superpowers
There are five things you can do in unsafe Rust that you cannot do in safe Rust:
- Dereference a raw pointer.
- Call an
unsafefunction or method. - Access or modify a
static mutvariable. - Implement an
unsafetrait. - Access fields of a
union.
Let’s look at each one.
1. Dereferencing a Raw Pointer
In safe Rust, you work with references (&T and &mut T), which are guaranteed to be non-null and point to valid memory. unsafe Rust allows you to work with raw pointers (*const T and *mut T).
Raw pointers:
- Can be null.
- Are allowed to ignore the borrowing rules (you can have multiple mutable pointers to the same location).
- Are not guaranteed to point to valid memory.
Creating a raw pointer is safe, but dereferencing it is unsafe because the compiler cannot guarantee its validity.
fn main() {
let mut num = 5;
// Creating raw pointers from references is safe.
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Dereferencing them requires an unsafe block.
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
*r2 = 10; // Write to the memory location.
println!("num is now: {}", num);
}
}
2. Calling an Unsafe Function or Method
Some functions and methods are marked as unsafe because they have invariants that the compiler cannot verify. A common example is std::slice::from_raw_parts, which creates a slice from a raw pointer and a length. It’s unsafe because it trusts the programmer to provide a valid pointer and a correct length.
use std::slice;
fn main() {
let some_memory = [1, 2, 3, 4];
let pointer = some_memory.as_ptr();
let length = some_memory.len();
// Calling an unsafe function requires an unsafe block.
unsafe {
// This is unsafe because we are asserting that the pointer is valid
// for the given length.
let my_slice: &[i32] = slice::from_raw_parts(pointer, length);
assert_eq!(my_slice, &[1, 2, 3, 4]);
}
}
This is fundamental to Foreign Function Interface (FFI), as calling functions in other languages (like C) is inherently unsafe.
3. Accessing or Modifying a static mut Variable
Rust allows for global variables using the static keyword. While immutable static variables are safe, static mut variables are unsafe to access. This is because if multiple threads tried to modify it at the same time, it would cause a data race.
// A mutable static variable.
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
// Modifying a static mut is unsafe.
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
// Reading it is also unsafe.
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
Note: For thread-safe global state, you should always prefer safe abstractions like Mutex or OnceCell over static mut.
4. Implementing an Unsafe Trait
A trait can be declared as unsafe if it contains at least one method with an invariant that the compiler cannot check. When you implement an unsafe trait, you are promising that you have upheld these invariants.
The most common examples are the Send and Sync traits, which are fundamental to Rust’s concurrency model. They are unsafe traits because the compiler cannot automatically verify if a type containing raw pointers is safe to send across threads.
// A custom type that holds a raw pointer.
struct MyWrapper(*mut u8);
// This is us telling the compiler: "Trust me, it's safe to send this type
// to another thread." This is a huge responsibility!
unsafe impl Send for MyWrapper {}
unsafe impl Sync for MyWrapper {}
5. Accessing Fields of a union
A union is like a struct, but all its fields share the same memory location. Accessing union fields is unsafe because Rust cannot guarantee which field’s data is currently stored in that memory.
union IntOrFloat {
i: i32,
f: f32,
}
fn main() {
let mut u = IntOrFloat { i: 1 };
// We wrote an integer, so reading it is okay.
unsafe {
assert_eq!(u.i, 1);
}
// Writing a float overwrites the integer.
u.f = 2.0;
// Reading the integer now would be incorrect and produce garbage data.
// This is unsafe behavior.
unsafe {
println!("u.f is {}", u.f);
}
}
Conclusion
unsafe Rust is a necessary tool for low-level systems programming. It gives you the power to work around the compiler’s limitations when you know more about the code’s invariants than the compiler does. However, with great power comes great responsibility. The best practice is to isolate unsafe code into small, well-documented blocks and wrap them in a safe, high-level API.