# Unsafe Rust & FFI

> Know what unsafe permits and what it doesn't, wrap it in sound safe APIs, verify it with Miri, and call C from Rust and Rust from C.

Rust roadmap · Systems & production · 17 min read · https://levelcode.org/roadmaps/rust/unsafe-and-ffi

Safe Rust's guarantees come from rules the compiler can check. Some correct programs can't be expressed within those rules: talking to hardware, calling a C library, implementing `Vec` itself. For those, Rust has `unsafe`: a way to say "I've checked this part myself." Most Rust programmers write very little unsafe code, but everyone depends on it (the standard library is full of it), and anyone wrapping a C library needs it. This lesson covers what `unsafe` actually unlocks, the idea of a sound safe abstraction, undefined behaviour and how Miri catches it, and the foreign function interface (FFI) in both directions.

## What unsafe unlocks

An `unsafe` block lets you do exactly five things the compiler otherwise forbids:

1. Dereference a **raw pointer** (`*const T`, `*mut T`).
2. Call an **unsafe function** (including foreign functions from C).
3. Access or modify a **mutable static** variable.
4. Implement an **unsafe trait** (such as `Send` or `Sync` by hand).
5. Access fields of a **union**.

That's all. `unsafe` does **not** turn off the borrow checker, type checking or bounds checks on safe operations. References inside an unsafe block still obey the borrowing rules; you've only gained the ability to work with raw pointers, which don't.

```rust
fn main() {
    let mut x = 10;
    let p = &mut x as *mut i32; // creating a raw pointer is safe
    unsafe {
        *p += 1;                // dereferencing it is not
    }
    println!("{x}"); // 11
}
```

Creating raw pointers is safe because a pointer that's never dereferenced can't hurt anyone. Dereferencing one is unsafe because the compiler can't know whether it points at valid, aligned, initialized memory that nothing else is mutating.

> [!IMPORTANT]
> `unsafe` doesn't mean "this code is dangerous." It means "the compiler can't verify this; a human has." The obligation moves from the compiler to you, and to whoever reviews the code.

## Undefined behaviour

The rules you must uphold in unsafe code aren't just "don't crash". Violating them is **undefined behaviour (UB)**, and the optimizer assumes UB never happens. A program with UB may work in debug builds and break mysteriously in release builds, or after a compiler upgrade. Things that are UB in Rust include:

- Dereferencing a dangling, null or misaligned pointer.
- Reading uninitialized memory.
- Creating two `&mut` references to the same data at the same time, or a `&mut` while a `&` is live, even through raw pointers.
- Producing an invalid value: a `bool` that's not 0 or 1, a `char` outside Unicode, an enum with an invalid discriminant, a non-UTF-8 `str`.
- Data races.

The aliasing rule is the one that surprises people: the rules for references apply even when unsafe code created them. Turning a raw pointer into two overlapping `&mut` references is UB even if you never use them at the same time in a way that looks conflicting.

## Sound safe abstractions

The real skill isn't writing unsafe code; it's **containing** it. You write a small unsafe core and wrap it in a safe API that makes misuse impossible. If every possible use of the safe API, by any safe caller, avoids UB, the abstraction is **sound**.

`split_at_mut` from the standard library is the classic example. It returns two non-overlapping mutable slices of one slice, which safe code can't express, because the borrow checker sees two `&mut` borrows of the same slice:

```rust
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();
    assert!(mid <= len, "mid out of bounds"); // the check that makes the unsafe below sound

    // SAFETY: `ptr` is valid for `len` elements. The two ranges [0, mid) and
    // [mid, len) don't overlap, and both are within bounds thanks to the assert.
    // The returned slices borrow from `values`, so they can't outlive it.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut v = [1, 2, 3, 4, 5];
    let (a, b) = split_at_mut(&mut v, 2);
    a[0] = 10;
    b[0] = 30;
    println!("{v:?}"); // [10, 2, 30, 4, 5]
}
```

Three habits make unsafe code reviewable:

- **Check preconditions in safe code** before the unsafe block (the `assert!` above). Remove it and `split_at_mut(&mut v, 99)` would create slices past the end: the function would be unsound.
- **Write a `// SAFETY:` comment** on every unsafe block explaining why each requirement holds. Clippy's `undocumented_unsafe_blocks` lint enforces this.
- **Document unsafe functions with a `# Safety` section** listing what callers must guarantee. Since the 2024 edition, the compiler warns (`unsafe_op_in_unsafe_fn`) unless operations inside an `unsafe fn` sit in their own `unsafe {}` block, so each one is visible and justified.

```rust
/// Returns the element at `index` without bounds checking.
///
/// # Safety
/// `index` must be less than `values.len()`.
unsafe fn get_unchecked(values: &[u32], index: usize) -> u32 {
    // SAFETY: the caller guarantees index < len.
    unsafe { *values.as_ptr().add(index) }
}
```

Keep unsafe modules small and the safe API narrow, and add `#![forbid(unsafe_code)]` (or `unsafe_code = "forbid"` in `[lints.rust]`) to crates that don't need it at all.

## Miri

Unsafe bugs often don't crash; they silently corrupt something. **Miri** is an interpreter for Rust's intermediate representation that checks every memory access against Rust's rules and reports UB as it happens: out-of-bounds pointer use, use-after-free, invalid values, aliasing violations and data races.

```bash
rustup +nightly component add miri
cargo +nightly miri test
```

```text
error: Undefined Behavior: attempting a read access using <2843> at alloc1234[0x14],
       but that tag does not exist in the borrow stack for this location
```

Miri runs your tests 10–100× slower and can't call most foreign functions, so it's for unit tests of the unsafe core, not the whole application. Run it in CI on any crate with meaningful unsafe code.

## Calling C from Rust

Foreign functions are declared in an `extern "C"` block. Since the 2024 edition, these blocks must be written `unsafe extern`, a reminder that the signatures are unchecked promises: the compiler trusts that you've described the C function correctly.

```rust
use std::ffi::{CStr, CString, c_char, c_int};

unsafe extern "C" {
    // From the C standard library, which Rust programs already link against.
    fn abs(x: c_int) -> c_int;
    fn strlen(s: *const c_char) -> usize;
    fn getenv(name: *const c_char) -> *mut c_char;
}

fn c_strlen(s: &str) -> usize {
    let c = CString::new(s).expect("no interior NUL bytes");
    // SAFETY: `c` is a valid NUL-terminated string that lives until the end of this call.
    unsafe { strlen(c.as_ptr()) }
}

fn env_var(name: &str) -> Option<String> {
    let key = CString::new(name).ok()?;
    // SAFETY: `key` is NUL-terminated. getenv returns null or a pointer to a C string.
    // (getenv isn't thread-safe against concurrent setenv; real code uses std::env::var.)
    let ptr = unsafe { getenv(key.as_ptr()) };
    if ptr.is_null() {
        return None;
    }
    // SAFETY: ptr is non-null and points to a NUL-terminated string owned by the C library.
    let s = unsafe { CStr::from_ptr(ptr) };
    Some(s.to_string_lossy().into_owned())
}

fn main() {
    // SAFETY: abs is defined for all inputs except INT_MIN.
    println!("{}", unsafe { abs(-7) });
    println!("{}", c_strlen("hello"));
    println!("{:?}", env_var("HOME"));
}
```

The rules for crossing the boundary:

- **Strings**: Rust strings aren't NUL-terminated and may contain NUL bytes. Convert with `CString` going out and `CStr` coming in. Keep the `CString` alive while C uses the pointer: `CString::new(s).unwrap().as_ptr()` in one expression creates a dangling pointer, because the temporary is dropped at the end of the statement.
- **Types**: use `std::ffi::{c_int, c_char, c_long, …}` and `#[repr(C)]` structs, which lay fields out the way C does. Rust's default layout is unspecified.
- **Ownership**: memory allocated by C must be freed by C (call the library's free function), and memory allocated by Rust by Rust. Never mix allocators.

Hand-writing declarations is fine for a few functions. For real C libraries, **`bindgen`** generates them from header files in a `build.rs` script, and the convention is a `foo-sys` crate with raw bindings plus a `foo` crate exposing a safe, idiomatic wrapper.

## Calling Rust from C

The other direction exposes Rust functions with a C ABI and a stable symbol name:

```rust
/// Adds two numbers. Callable from C as `int32_t pm_add(int32_t, int32_t)`.
#[unsafe(no_mangle)]
pub extern "C" fn pm_add(a: i32, b: i32) -> i32 {
    a.wrapping_add(b)
}

/// Returns the length of a C string, or -1 for null.
///
/// # Safety
/// `s` must be null or point to a valid NUL-terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pm_strlen(s: *const std::ffi::c_char) -> i64 {
    if s.is_null() {
        return -1;
    }
    // SAFETY: the caller guarantees a valid NUL-terminated string.
    unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes().len() as i64
}
```

```toml
[lib]
crate-type = ["cdylib", "staticlib"]   # a .so/.dylib/.dll and a .a for C to link
```

`#[unsafe(no_mangle)]` (the 2024 edition requires the `unsafe(...)` wrapper, because a symbol name can clash with another and cause UB) keeps the function's name as written. **`cbindgen`** generates the matching C header. Panics must never unwind across the boundary into C: a panic escaping an `extern "C"` function aborts the process, so catch errors and return status codes instead.

For other languages there are dedicated tools on the same foundation: **PyO3** for Python extensions, **napi-rs** for Node.js, **wasm-bindgen** for JavaScript via WebAssembly, and **uniffi** for Swift and Kotlin.

## Practice

1. Write a safe function `fn swap_halves(v: &mut [u8])` that swaps the first and second halves of a slice of even length (`[1,2,3,4]` → `[3,4,1,2]`). Implement it with your own `split_at_mut` built on raw pointers, plus `swap_with_slice`. Panic with a clear message for odd lengths.
2. Write a test, then run it under Miri with `cargo +nightly miri test`.
3. Deliberately break the safety: remove the bounds check and call your `split_at_mut` with `mid > len` in a test. What does Miri report?
4. Declare `qsort` from the C standard library (`fn qsort(base: *mut c_void, n: usize, size: usize, cmp: extern "C" fn(*const c_void, *const c_void) -> c_int)`) and use it to sort a `Vec<i32>` through a safe wrapper `fn c_sort(v: &mut [i32])`.

**Solution**

```rust
use std::ffi::{c_int, c_void};

fn split_at_mut<T>(values: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = values.len();
    assert!(mid <= len, "mid {mid} out of bounds for length {len}");
    let ptr = values.as_mut_ptr();
    // SAFETY: [0, mid) and [mid, len) are in bounds (checked above) and disjoint,
    // and the results borrow `values` for their whole lifetime.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

pub fn swap_halves(v: &mut [u8]) {
    assert!(v.len() % 2 == 0, "swap_halves needs an even length, got {}", v.len());
    let mid = v.len() / 2;
    let (a, b) = split_at_mut(v, mid);
    a.swap_with_slice(b);
}

unsafe extern "C" {
    fn qsort(
        base: *mut c_void,
        n: usize,
        size: usize,
        cmp: extern "C" fn(*const c_void, *const c_void) -> c_int,
    );
}

extern "C" fn cmp_i32(a: *const c_void, b: *const c_void) -> c_int {
    // SAFETY: qsort passes pointers to elements of the slice we gave it, which are i32s.
    let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) };
    a.cmp(&b) as c_int // Ordering is -1, 0 or 1
}

pub fn c_sort(v: &mut [i32]) {
    // SAFETY: base and n describe a valid, exclusively borrowed buffer of i32,
    // size is the element size, and cmp_i32 never panics or unwinds.
    unsafe { qsort(v.as_mut_ptr().cast(), v.len(), size_of::<i32>(), cmp_i32) }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn swaps() {
        let mut v = [1, 2, 3, 4];
        swap_halves(&mut v);
        assert_eq!(v, [3, 4, 1, 2]);
    }

    #[test]
    fn sorts_with_c() {
        let mut v = vec![5, -1, 3, 0];
        c_sort(&mut v);
        assert_eq!(v, [-1, 0, 3, 5]);
    }
}

fn main() {
    let mut v = [1u8, 2, 3, 4, 5, 6];
    swap_halves(&mut v);
    println!("{v:?}"); // [4, 5, 6, 1, 2, 3]
}
```

Without the `assert!`, `split_at_mut(&mut v, 10)` on a 4-element slice computes `len - mid`, which underflows: a panic in debug builds, or a huge length in release. If you instead build a second slice that runs past the allocation, Miri reports undefined behaviour along the lines of "pointer arithmetic … is out of bounds" or "memory access failed … out-of-bounds", pointing at the exact line. That's the kind of bug that can pass every normal test and still corrupt memory in production. Miri can't run the `qsort` test, because it doesn't support most foreign functions; mark it `#[cfg_attr(miri, ignore)]`.

## Key takeaways

- `unsafe` unlocks five operations — raw pointer dereferences, unsafe and foreign calls, mutable statics, unsafe traits and unions — and turns off nothing else.
- Undefined behaviour includes aliasing violations and invalid values, not just crashes; the optimizer assumes it never happens.
- Contain unsafe code in small, sound safe APIs: check preconditions first, and justify every block with a `// SAFETY:` comment.
- Run Miri on tests that exercise unsafe code; it catches UB that normal tests miss.
- FFI needs `unsafe extern "C"`, `CString`/`CStr`, `#[repr(C)]`, matching allocators and no panics across the boundary; use bindgen/cbindgen and a `-sys` crate plus a safe wrapper.

## Quiz

1. Which of these does an unsafe block allow?

   A. Ignoring borrow-checker errors on references
   B. Dereferencing a raw pointer
   C. Skipping type checking for a function call
   D. Disabling bounds checks on v[i]

   **Answer: B.** unsafe unlocks five specific operations, including raw-pointer dereferences. References, types and indexing are still fully checked inside an unsafe block.

2. When is a safe function that contains unsafe code 'sound'?

   A. When no safe caller, passing any arguments, can cause undefined behaviour through it
   B. When it has a // SAFETY: comment
   C. When it passes the existing tests
   D. When it compiles without warnings

   **Answer: A.** Soundness is a guarantee about every possible use from safe code. Comments and tests help you argue it, but a single input that triggers UB makes the abstraction unsound.

3. What's wrong with passing CString::new(s).unwrap().as_ptr() directly to a C function stored for later use?

   A. CString pointers are always null
   B. C can't read UTF-8
   C. The temporary CString is dropped at the end of the statement, leaving a dangling pointer
   D. as_ptr() copies the string to the C heap

   **Answer: C.** as_ptr() borrows from the CString without extending its life. Bind the CString to a variable that outlives every use of the pointer.

4. What does Miri do?

   A. Compiles Rust to C
   B. Generates FFI bindings from header files
   C. Proves code has no panics
   D. Interprets your program and reports undefined behaviour, such as out-of-bounds or aliasing violations, when it happens

   **Answer: D.** Miri executes tests in an interpreter that tracks pointer provenance, initialization and borrows, so it catches UB that may not crash natively. bindgen generates bindings.
