Skip to content

Unsafe Rust Explained — When & How to Use Raw Pointers, FFI & Unsafe Operations

DodaTech Updated 2026-06-23 9 min read

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]
â„šī¸ Info

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
âš ī¸ Warning

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

Does unsafe disable the borrow checker?

No. unsafe only enables the five special operations (raw pointers, FFI, mutable statics, unsafe traits, union fields). The borrow checker still applies to all safe code within the block. unsafe is not a free pass — it's a circumvention of specific checks.

How do I debug undefined behavior?

Use cargo miri (Miri interpreter) to detect UB in unsafe code. Use AddressSanitizer (-Z sanitizer=address) for runtime memory checks. Use Valgrind for more comprehensive analysis. Enable debug assertions with -C debug-assertions=on for checked arithmetic and bounds.

Can I write all systems software without unsafe?

Most application code can be 100% safe. Even many system-level crates (Linux kernel drivers, web browsers) minimize unsafe to small, audited modules. A good rule: encapsulate all unsafe behind safe abstractions and audit the boundaries.

Unsafe Rust Basics
Rust FFI
Rust FFI Guide
Macros Guide

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