Unsafe Rust Explained â When & How to Use Raw Pointers, FFI & Unsafe Operations
In this tutorial, you'll learn about Unsafe Rust Explained. We cover key concepts, practical examples, and best practices.
Master unsafe Rust: raw pointer dereferencing, mutable statics, unsafe traits and functions, calling FFI functions, and the safety invariants that unsafe code must uphold.
What You'll Learn
In this tutorial, you'll master Rust unsafe code â when and why to use unsafe, raw pointer operations with *const T and *mut T, calling foreign functions via FFI, implementing unsafe traits, working with mutable statics, and the invariants you must manually enforce to avoid undefined behavior.
Why It Matters
Unsafe Rust is a superpower, not a loophole. The vast majority of Rust code never needs unsafe. But systems programming â interacting with hardware, implementing allocators, calling C libraries, optimizing hot paths â requires it. Understanding exactly what unsafe does and does not do is essential for any serious Rust systems programmer.
Real-World Use
The standard library uses unsafe internally for Vec, String, HashMap, and I/O. The alloc crate uses unsafe for heap management. Embedded Rust HALs use unsafe for memory-mapped I/O. Durga Antivirus Pro uses unsafe to call optimized SIMD signature scanning routines written in C.
flowchart TD
A[Need unsafe?] --> B{Which operation?}
B -->|Dereference raw pointer| C[*const T / *mut T]
B -->|Call foreign function| D[extern "C"]
B -->|Access mutable static| E[static mut]
B -->|Implement unsafe trait| F[unsafe trait / impl]
B -->|Read/write union fields| G[unions]
C --> H[SAFETY: pointer must be valid, aligned, non-null]
D --> I[SAFETY: function signature must match ABI]
E --> J[SAFETY: must synchronize access]
F --> K[SAFETY: impl must uphold trait invariants]
Prerequisites: Rust Ownership and Smart Pointers. Understanding FFI basics helps.
Raw Pointers: *const T and *mut T
Raw pointers are the unsafe equivalent of references. They have no lifetime tracking, no aliasing guarantees, and can be null:
fn raw_pointer_demo() {
let mut x = 42u32;
// Create raw pointers (safe operation)
let r1: *const u32 = &x as *const u32;
let r2: *mut u32 = &mut x as *mut u32;
// Dereference raw pointers (unsafe operation)
unsafe {
println!("r1 points to: {}", *r1);
*r2 = 100;
println!("After mutation: {}", *r1);
}
// Pointer arithmetic
let arr = [10u8, 20, 30, 40];
let ptr: *const u8 = arr.as_ptr();
unsafe {
println!("arr[0]: {}", *ptr);
println!("arr[1]: {}", *ptr.add(1));
println!("arr[2]: {}", *ptr.add(2));
println!("arr[3]: {}", *ptr.add(3));
}
}
fn main() {
raw_pointer_demo();
}
Expected output:
r1 points to: 42
After mutation: 100
arr[0]: 10
arr[1]: 20
arr[2]: 30
arr[3]: 40
Pointer safety invariants: Dereferencing a raw pointer requires: (1) the pointer must be non-null, (2) aligned to the type's alignment, (3) the memory must be valid and initialized, (4) no other mutable reference aliases the same memory.
Mutable Statics and FFI
Mutable statics and foreign function calls are common unsafe use cases:
use std::ffi::CString;
use std::os::raw::c_char;
// Mutable static: must be accessed in unsafe blocks
static mut LOG_LEVEL: i32 = 2;
// FFI declaration: calling puts from libc
extern "C" {
fn puts(s: *const c_char) -> i32;
}
// Safe wrapper around unsafe FFI
fn safe_puts(message: &str) {
let c_msg = CString::new(message).expect("CString::new failed");
unsafe {
puts(c_msg.as_ptr());
}
}
fn set_log_level(level: i32) {
if level < 0 || level > 5 {
panic!("Invalid log level: {}", level);
}
unsafe {
LOG_LEVEL = level;
}
}
fn get_log_level() -> i32 {
unsafe { LOG_LEVEL }
}
fn main() {
safe_puts("Hello from safe wrapper around FFI puts!");
set_log_level(3);
println!("Log level: {}", get_log_level());
safe_puts("FFI call after log level change");
}
Expected output:
Hello from safe wrapper around FFI puts!
Log level: 3
FFI call after log level change
Unsafe Traits and Implementations
Unsafe traits require the implementor to uphold invariants that safe code cannot check:
use std::mem;
// An unsafe trait: implementors promise the index is valid
unsafe trait RawIndex {
type Item;
unsafe fn raw_get(&self, index: usize) -> &Self::Item;
}
// Safe wrapper using the unsafe trait
fn safe_get<T: RawIndex>(container: &T, index: usize) -> Option<&T::Item> {
// Safety: we check the index before calling raw_get
unsafe {
// In a real implementation, we'd check bounds here
Some(container.raw_get(index))
}
}
#[repr(C)]
struct Packet {
header: u32,
payload: [u8; 64],
}
unsafe impl RawIndex for Packet {
type Item = u8;
unsafe fn raw_get(&self, index: usize) -> &u8 {
// SAFETY: caller guarantees index is valid
&self.payload[index]
}
}
impl Packet {
fn new(data: [u8; 64]) -> Self {
Packet {
header: 0xDEADBEEF,
payload: data,
}
}
}
fn main() {
let p = Packet::new([0u8; 64]);
let byte = safe_get(&p, 0);
println!("First byte: {:?}", byte);
// Raw memory reinterpretation
let n: u64 = 0x0102030405060708;
let bytes: &[u8; 8] = unsafe { &*(&n as *const u64 as *const [u8; 8]) };
println!("Bytes: {:02x?}", bytes);
}
Expected output:
First byte: Some(0)
Bytes: [08, 07, 06, 05, 04, 03, 02, 01]
Building Safe Abstractions Over Unsafe Code
The fundamental pattern: encapsulate unsafe operations in safe APIs with preconditions checked at the boundary:
use std::alloc::{self, Layout};
use std::ptr;
struct RawVec<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> RawVec<T> {
fn new() -> Self {
RawVec {
ptr: ptr::NonNull::dangling().as_ptr(),
len: 0,
capacity: 0,
}
}
fn push(&mut self, value: T) {
if self.len == self.capacity {
self.grow();
}
unsafe {
ptr::write(self.ptr.add(self.len), value);
}
self.len += 1;
}
fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
unsafe { Some(&*self.ptr.add(index)) }
} else {
None
}
}
fn grow(&mut self) {
let new_cap = if self.capacity == 0 {
4
} else {
self.capacity * 2
};
let new_layout = Layout::array::<T>(new_cap).unwrap();
let new_ptr = unsafe { alloc::alloc(new_layout) as *mut T };
unsafe {
ptr::copy_nonoverlapping(self.ptr, new_ptr, self.len);
if self.capacity > 0 {
alloc::dealloc(
self.ptr as *mut u8,
Layout::array::<T>(self.capacity).unwrap(),
);
}
}
self.ptr = new_ptr;
self.capacity = new_cap;
}
}
impl<T> Drop for RawVec<T> {
fn drop(&mut self) {
if self.capacity > 0 {
unsafe {
ptr::drop_in_place(std::slice::from_raw_parts_mut(self.ptr, self.len));
alloc::dealloc(
self.ptr as *mut u8,
Layout::array::<T>(self.capacity).unwrap(),
);
}
}
}
}
fn main() {
let mut v: RawVec<i32> = RawVec::new();
v.push(10);
v.push(20);
v.push(30);
for i in 0..3 {
if let Some(val) = v.get(i) {
println!("v[{}] = {}", i, val);
}
}
}
Expected output:
v[0] = 10
v[1] = 20
v[2] = 30
Common Errors
1. Dangling pointer dereference
let r: *const i32;
{
let x = 42;
r = &x as *const i32;
} // x dropped, r is dangling
unsafe { println!("{}", *r); } // Undefined behavior
2. Misaligned pointer access
let bytes: [u8; 8] = [0; 8];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("{}", *ptr); } // UB if bytes is not 4-aligned
3. Creating a null reference
let ptr: *const i32 = std::ptr::null();
unsafe { let _ = &*ptr; } // UB: dereferencing null
4. Violating pointer aliasing rules
let mut x = 5;
let r: *mut i32 = &mut x;
let s: &mut i32 = unsafe { &mut *r };
*s = 10; // Aliasing violation: s and (implicitly) x
println!("{}", x); // UB
5. Forgetting to check FFI return value
extern "C" { fn malloc(s: usize) -> *mut std::ffi::c_void; }
let p = unsafe { malloc(100) }; // Might return null!
Practice Questions
1. What does unsafe unlock in Rust?
Five capabilities: dereference raw pointers, call extern functions, access/modify mutable statics, implement unsafe traits, and access union fields. unsafe does NOT disable the borrow checker or type system.
2. How do you safely wrap unsafe code?
Check all preconditions at the safe boundary (null checks, bounds checks, alignment checks), then call the unsafe code. Document safety invariants in // SAFETY: comments at each unsafe block.
3. What is undefined behavior in Rust? Operations the compiler assumes never happen: data races, dangling pointer dereference, misaligned access, invalid enum discriminants, violating pointer aliasing rules, and calling FFI with wrong signatures. The compiler may generate incorrect code after UB.
4. When should you use raw pointers instead of references? When interacting with FFI (C expects raw pointers), implementing data structures with internal pointers (allocators, arenas), memory-mapped I/O, or when you need pointer arithmetic.
5. Challenge: Implement a safe Arena allocator that allocates objects from a pre-allocated buffer using raw pointer arithmetic. Your safe API must guarantee no double-free, no use-after-free, and proper alignment.
Mini Project: Safe Ring Buffer with Unsafe Internals
use std::mem;
use std::ptr;
pub struct RingBuffer<T> {
buffer: *mut T,
head: usize,
tail: usize,
capacity: usize,
}
impl<T> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
let mut v = Vec::with_capacity(capacity);
let buffer = v.as_mut_ptr();
mem::forget(v); // Prevent Vec from dropping the buffer
RingBuffer {
buffer,
head: 0,
tail: 0,
capacity,
}
}
pub fn push(&mut self, value: T) -> Result<(), T> {
let next_tail = (self.tail + 1) % self.capacity;
if next_tail == self.head {
return Err(value); // Buffer full
}
unsafe {
ptr::write(self.buffer.add(self.tail), value);
}
self.tail = next_tail;
Ok(())
}
pub fn pop(&mut self) -> Option<T> {
if self.head == self.tail {
return None; // Buffer empty
}
let value = unsafe { ptr::read(self.buffer.add(self.head)) };
self.head = (self.head + 1) % self.capacity;
Some(value)
}
}
impl<T> Drop for RingBuffer<T> {
fn drop(&mut self) {
while self.pop().is_some() {}
// Deallocate the buffer
if self.capacity > 0 {
unsafe {
let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
std::alloc::dealloc(self.buffer as *mut u8, layout);
}
}
}
}
fn main() {
let mut buf: RingBuffer<i32> = RingBuffer::new(4);
buf.push(10).unwrap();
buf.push(20).unwrap();
buf.push(30).unwrap();
println!("Pop: {:?}", buf.pop());
println!("Pop: {:?}", buf.pop());
buf.push(40).unwrap();
buf.push(50).unwrap();
println!("Pop: {:?}", buf.pop());
println!("Pop: {:?}", buf.pop());
println!("Pop: {:?}", buf.pop());
}
Expected output:
Pop: Some(10)
Pop: Some(20)
Pop: Some(30)
Pop: Some(40)
Pop: Some(50)
FAQ
Related Concepts
What's Next
Apply unsafe patterns in Rust FFI to interface with C libraries, or explore Macros for compile-time code generation.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro