What is Unsafe Rust?
unsafe is a keyword that unlocks five additional capabilities that the compiler
cannot verify for safety. It doesn't disable the borrow checker — it gives you
extra powers that you promise to use correctly. Well-written Rust code
contains small, audited unsafe blocks wrapped in safe abstractions.
Five Unsafe Superpowers
- Dereference raw pointers (
*const T,*mut T) - Call unsafe functions or methods
- Access or modify mutable static variables
- Implement unsafe traits
- Access fields of unions
Raw Pointers
fn main() {
let mut value = 42;
// Creating raw pointers is safe
let r1 = &value as *const i32;
let r2 = &mut value as *mut i32;
// Dereferencing raw pointers is unsafe
unsafe {
println!("r1 = {}", *r1);
*r2 = 100;
println!("r2 = {}", *r2);
}
// Raw pointers can point to arbitrary memory
let address = 0x012345usize;
let _r = address as *const i32;
// Dereferencing this would be undefined behavior!
// Practical use: splitting a slice
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut v, 3);
println!("Left: {:?}, Right: {:?}", left, right);
}
// Safe abstraction over unsafe code
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Calling C Code (FFI)
// Foreign Function Interface — call C from Rust
extern "C" {
fn abs(input: i32) -> i32;
fn sqrt(input: f64) -> f64;
fn strlen(s: *const i8) -> usize;
}
fn main() {
// All extern functions are unsafe
unsafe {
println!("abs(-42) = {}", abs(-42));
println!("sqrt(144) = {}", sqrt(144.0));
}
// Safe wrapper around C function
fn safe_abs(n: i32) -> i32 {
unsafe { abs(n) }
}
println!("safe_abs(-10) = {}", safe_abs(-10));
}
// Expose Rust functions to C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
// Wrapping a C library safely
mod ffi {
extern "C" {
fn c_library_init() -> i32;
fn c_library_process(data: *const u8, len: usize) -> i32;
fn c_library_cleanup();
}
pub struct Library;
impl Library {
pub fn new() -> Result {
let result = unsafe { c_library_init() };
if result == 0 {
Ok(Library)
} else {
Err(format!("Init failed: {result}"))
}
}
pub fn process(&self, data: &[u8]) -> i32 {
unsafe { c_library_process(data.as_ptr(), data.len()) }
}
}
impl Drop for Library {
fn drop(&mut self) {
unsafe { c_library_cleanup(); }
}
}
}
Unsafe Traits
// Unsafe traits have invariants the compiler can't verify
// Send and Sync are the most common unsafe traits
// Implementing Send means: this type is safe to transfer between threads
// Implementing Sync means: this type is safe to reference from multiple threads
struct MyType {
data: *mut i32, // Raw pointer — not Send or Sync by default
}
// We assert that MyType is safe to send between threads
// This is our PROMISE to the compiler
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
// Custom unsafe trait
unsafe trait TrustedSource {
fn get_data(&self) -> &[u8];
}
// Implementor promises the data is always valid
unsafe impl TrustedSource for Vec {
fn get_data(&self) -> &[u8] {
self.as_slice()
}
}
Best Practices for Unsafe Code
// 1. Minimize unsafe scope
fn good_example(data: &[u8]) -> u32 {
// Safe code here...
let result = unsafe {
// ONLY the unsafe operation
*(data.as_ptr() as *const u32)
};
// More safe code...
result
}
// 2. Document safety invariants
/// Reads a u32 from the start of the byte slice.
///
/// # Safety
/// - data must have at least 4 bytes
/// - data must be aligned to 4 bytes
unsafe fn read_u32_unchecked(data: &[u8]) -> u32 {
*(data.as_ptr() as *const u32)
}
// 3. Create safe wrappers
fn read_u32(data: &[u8]) -> Option {
if data.len() >= 4 {
Some(unsafe { read_u32_unchecked(data) })
} else {
None
}
}
// 4. Use MIRI to detect undefined behavior
// cargo +nightly miri test
When NOT to Use Unsafe
Most Rust programs need zero unsafe code. Don't use unsafe to "work around" the borrow checker — the compiler is usually right. Reach for unsafe only when interfacing with C, building data structures that can't be expressed safely, or for verified performance optimizations.
Key Takeaways
- ✅
unsafeunlocks five extra powers — it doesn't disable the borrow checker - ✅ Wrap unsafe code in safe abstractions with clear invariant documentation
- ✅ FFI with C requires unsafe — use RAII wrappers for safe resource management
- ✅ Minimize unsafe scope to the smallest possible block
- ✅ Use MIRI (
cargo miri) to detect undefined behavior in unsafe code