PHASE 3 ← Back to Course
14 / 17
⚠️

Unsafe Rust

When and how to use unsafe, raw pointers, FFI, and bypassing the borrow checker safely.

1

When and Why to Use Unsafe

Unsafe Rust bypasses compile-time guarantees when necessary for performance or system-level operations. Use it sparingly and with careful documentation.

Rust
// When performance requires unsafe optimizations
fn summing_without_bounds_checking(slice: &[i32]) -> i32 {
    let mut sum = 0;
    unsafe {
        // Bypassing bounds checking when we know it's safe
        let ptr = slice.as_ptr();
        for i in 0..slice.len() {
            sum += *ptr.add(i);
        }
    }
    sum
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let result = summing_without_bounds_checking(&numbers);
    println!("Sum: {}", result);
}
⚠️

The Unsafe Contract

Inside an unsafe block, you are responsible for upholding Rust's safety invariants. The compiler trusts you, if you get it wrong, you get undefined behaviour.

2

Raw Pointers

Raw pointers (*const T and *mut T) allow direct memory access without safety checks. Creating them is safe, but dereferencing requires an unsafe block.

Rust
fn main() {
    let x = 5;
    let raw_ptr = &x as *const i32;
    // Can only dereference in unsafe blocks
    unsafe {
        println!("Value at pointer: {}", *raw_ptr);
    }
    // Mutable raw pointers
    let mut y = 10;
    let mut_ptr = &mut y as *mut i32;
    unsafe {
        *mut_ptr = 20;
        println!("Modified value: {}", *mut_ptr);
    }
}
Rust
// Pointer arithmetic - advanced usage
fn main() {
    let arr = [1, 2, 3, 4, 5];
    let ptr = arr.as_ptr();
    unsafe {
        println!("First element: {}", *ptr);
        println!("Third element: {}", *ptr.add(2));
        // Copy from pointer
        let mut dest = [0; 5];
        std::ptr::copy_nonoverlapping(ptr, dest.as_mut_ptr(), 5);
        println!("Copied array: {:?}", dest);
    }
}
💡

Creating vs Dereferencing

You can create raw pointers in safe code, that is perfectly fine. It is only dereferencing a raw pointer that requires unsafe, because the compiler cannot guarantee the pointer is valid.

3

Unsafe Functions and Blocks

Creating and calling unsafe functions requires explicit unsafe blocks. A good pattern is to wrap unsafe internals behind a safe public API.

Rust
// Unsafe function that splits a string at an arbitrary position
unsafe fn split_at_unsafe(s: &str, mid: usize) -> (&str, &str) {
    let bytes = s.as_bytes();
    let ptr = s.as_ptr();
    // Assume mid is a valid UTF-8 boundary
    (
        std::str::from_utf8_unchecked(
            std::slice::from_raw_parts(ptr, mid)),
        std::str::from_utf8_unchecked(
            std::slice::from_raw_parts(ptr.add(mid), bytes.len() - mid)),
    )
fn safe_split_wrapper(s: &str, mid: usize) -> Option<(&str, &str)> {
    if mid <= s.len() && s.is_char_boundary(mid) {
        unsafe { Some(split_at_unsafe(s, mid)) }
    } else {
        None
    }
fn main() {
    let text = "Hello, world!";
    if let Some((left, right)) = safe_split_wrapper(text, 5) {
        println!("Left: {}, Right: {}", left, right);
    }
}
Rust
// Unsafe traits
unsafe trait Dangerous {
    unsafe fn do_dangerous_thing(&self);
struct UnsafeStruct;
unsafe impl Dangerous for UnsafeStruct {
    unsafe fn do_dangerous_thing(&self) {
        println!("Doing something dangerous!");
    }
fn main() {
    let obj = UnsafeStruct;
    unsafe {
        obj.do_dangerous_thing();
    }
}

Safe Wrappers Pattern

The best practice is to validate all preconditions in a safe wrapper function, then call the underlying unsafe code only when the invariants are guaranteed. This keeps the unsafe surface area minimal.

4

Mutable Statics

Mutable static variables require unsafe access due to potential data races in multithreaded contexts.

Rust
static mut COUNTER: i32 = 0;
fn increment_counter() {
    unsafe {
        COUNTER += 1;
    }
fn get_counter() -> i32 {
    unsafe {
        COUNTER
    }
fn main() {
    increment_counter();
    increment_counter();
    println!("Counter: {}", get_counter());
}
⚠️

Prefer Atomics or Mutex

Mutable statics are inherently unsafe in multithreaded code. In practice, prefer AtomicI32, Mutex<T>, or once_cell::sync::Lazy for thread-safe global state.

5

FFI: Calling C from Rust

The Foreign Function Interface (FFI) allows calling C code from Rust. All FFI calls are unsafe because the Rust compiler cannot verify the correctness of external code.

Rust
// Using libc to call C functions
extern "C" {
    fn abs(x: i32) -> i32;
    fn sqrt(x: f64) -> f64;
fn main() {
    unsafe {
        println!("abs(-42) = {}", abs(-42));
        println!("sqrt(16.0) = {}", sqrt(16.0));
    }
}
Rust
// Wrapping C functions with safe interfaces
extern "C" {
    fn strlen(s: *const u8) -> usize;
fn safe_strlen(s: &str) -> usize {
    unsafe {
        strlen(s.as_ptr())
    }
fn main() {
    let text = "Hello";
    println!("Length: {}", safe_strlen(text));
}
💡

The extern "C" ABI

The "C" in extern "C" specifies the calling convention. This tells Rust to use the C ABI, which is the standard for cross-language interop on most platforms.

6

Safe Abstractions Over Unsafe Code

The best practice is to hide unsafe operations behind safe APIs. Validate inputs, manage lifetimes, and implement Drop for cleanup.

Rust
// Safe abstraction over unsafe pointer operations
struct SafeBuffer {
    ptr: *mut u8,
    len: usize,
impl SafeBuffer {
    fn new(size: usize) -> Self {
        unsafe {
            let ptr = libc::malloc(size) as *mut u8;
            SafeBuffer { ptr, len: size }
        }
    }
    fn get(&self, index: usize) -> Option<u8> {
        if index < self.len {
            unsafe { Some(*self.ptr.add(index)) }
        } else {
            None
        }
    }
    fn set(&mut self, index: usize, value: u8) -> bool {
        if index < self.len {
            unsafe { *self.ptr.add(index) = value; }
            true
        } else {
            false
        }
    }
impl Drop for SafeBuffer {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.ptr as *mut libc::c_void);
        }
    }
fn main() {
    let mut buf = SafeBuffer::new(10);
    buf.set(0, 42);
    if let Some(value) = buf.get(0) {
        println!("Value: {}", value);
    }
}

The Key Principle

A well-designed safe abstraction makes it impossible for callers to trigger undefined behaviour through the public API. All bounds checks and validity proofs happen inside the wrapper.

7

Common Unsafe Patterns

Several patterns in systems programming commonly involve unsafe code, such as type transmutation and #[repr(C)] structs for C interop.

Rust
// Pattern 1: Transmute (with extreme caution)
fn bytes_to_u32_unsafe(bytes: &[u8; 4]) -> u32 {
    unsafe {
        std::mem::transmute::<[u8; 4], u32>(*bytes)
    }
// Pattern 2: Accessing struct fields at offsets
#[repr(C)]
struct CStruct {
    a: i32,
    b: f64,
fn main() {
    let s = CStruct { a: 42, b: 3.14 };
    // Safe approach: use normal field access
    println!("a: {}, b: {}", s.a, s.b);
    // Unsafe approach: raw pointer access
    unsafe {
        let ptr = &s as *const CStruct as *const u8;
        println!("Field a offset: {}",
            std::mem::offset_of!(CStruct, a));
    }
}
⚠️

Transmute Is Dangerous

std::mem::transmute reinterprets the bits of a value as a different type. It can easily cause undefined behaviour if the source and target types have different invariants. Prefer from_ne_bytes / to_ne_bytes for numeric conversions.

← Previous Chapter 14 of 17 Next →