---
title: "25.3 Unsafe functions and safe abstractions"
description: "Functions marked unsafe and their preconditions, and the core pattern of wrapping unsafe code in a safe API, with split_at_mut as the example."
url: "https://learnrust.net/chapter-25/unsafe-functions-and-safe-abstractions/"
last_updated: "2026-06-13"
---

# 25.3 Unsafe functions and safe abstractions

Raw pointers (lesson [25.2](https://learnrust.net/chapter-25/raw-pointers/)) are dangerous in the open, but the Rust way is never to leave them in the open. The whole craft of using `unsafe` well is to *confine* it: write a tiny core of unsafe code, prove it correct, and wrap it in a **safe abstraction**, a normal, safe function that callers use without ever touching `unsafe` themselves. This lesson covers `unsafe` functions and the wrapping pattern that makes them livable, with the standard library's own `split_at_mut` as the canonical example. This is the most important idea in the chapter: unsafety, done right, doesn't spread.

## Unsafe functions

A function can be marked `unsafe`, which means it has **preconditions the caller must satisfy** for it to behave correctly, conditions the compiler can't check. Calling such a function requires an `unsafe` block, your acknowledgment that you've upheld its contract:

```rust
unsafe fn dangerous() {
    // ... does something that's only safe under certain conditions
}

fn main() {
    unsafe {
        dangerous();   // calling an unsafe fn requires unsafe
    }
}
```

The `unsafe` on the function definition is a warning label: "this function trusts you to have met its requirements; if you haven't, it can cause undefined behavior." The `unsafe` block at the call site is you signing for that. Marking a function `unsafe` doesn't make its *body* specially permissive in any new way, it's a signal to callers about the contract they're taking on. Standard-library functions like `slice::get_unchecked` (index without bounds-checking) are `unsafe` precisely because they trust the caller to pass a valid index; pass a bad one and it reads out of bounds.

## The pattern: a safe wrapper around unsafe code

Here's the move that makes `unsafe` manageable. Most functions that *contain* `unsafe` code are not themselves `unsafe`. They wrap a small unsafe core inside a safe function that *guarantees* the preconditions are met, so callers get a completely safe interface. If the wrapper genuinely upholds every invariant the unsafe code needs, then no matter what the caller does, nothing can go wrong, and the function is safe to expose.

This is exactly how the standard library is built. `Vec`, `String`, every collection, contains `unsafe` raw-pointer code internally, wrapped in the safe API you've used for chapters without ever writing `unsafe` yourself. You've been standing on a foundation of audited `unsafe` the whole time, and never knew, because the abstractions are airtight.

## split_at_mut: the canonical example

The textbook case is `split_at_mut`, a real slice method that splits one mutable slice into two mutable halves. Think about why this is hard for the borrow checker. You want *two* `&mut` slices into the *same* original slice at once, which sounds like an instant violation of chapter 9's "only one mutable borrow" rule. But it's actually safe: the two halves cover *disjoint* parts of the slice, no overlap, so there's no aliasing. The trouble is the borrow checker can't *see* that they're disjoint; it just sees two mutable borrows of one slice and refuses.

So the implementation drops to `unsafe` for the part the checker can't verify, and wraps it safely:

```rust
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);   // precondition: mid must be in range

    unsafe {
        // SAFETY: `mid <= len` (asserted above), so the two ranges
        // [0, mid) and [mid, len) are disjoint and both within the
        // slice's allocation. Disjoint ranges means the two &mut
        // slices never alias, upholding the borrow rules.
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut numbers, 3);
    left[0] = 100;
    right[0] = 200;
    println!("{numbers:?}");
}
```

```
[100, 2, 3, 200, 5, 6]
```

Look at what makes this sound. The `assert!(mid <= len)` guarantees the split point is in range, *before* any unsafe code runs, so a caller can't ask for an out-of-bounds split. Inside `unsafe`, `from_raw_parts_mut` builds two slices from raw pointers: one covering `[0, mid)`, one covering `[mid, len)`. Because `mid <= len`, those ranges are **disjoint**, they share no element, so the two `&mut` slices never point at the same memory, and the borrow rule isn't actually violated, only the compiler's ability to *prove* it was. The `// SAFETY:` comment records exactly this reasoning.

And crucially: `split_at_mut` itself is **not** `unsafe`. A caller uses it like any normal function, `let (left, right) = split_at_mut(&mut numbers, 3)`, with no `unsafe` block, no danger. The unsafety was real but it's been fully contained: bounded by the assert, justified by the disjointness argument, and sealed inside a safe interface. The caller cannot misuse it, because the wrapper checks everything that could go wrong.

> **Key insight**
>
> The goal of `unsafe` is never to expose it, it's to *bury* it. You write the smallest possible unsafe core, prove (and comment) that it's sound for all inputs the wrapper allows, enforce any preconditions with checks like `assert!`, and present callers a fully safe function. Done right, unsafety doesn't propagate: a million lines of safe code can sit atop a few audited unsafe blocks and inherit none of the risk. That's the entire reason Rust can have a safe standard library built on raw pointers. Unsafe is a sealed core, not a spreading stain.

## The module boundary as a safety argument

There's a subtle reason the wrapper has to be trustworthy: a safe function makes a *promise to the whole program* that it can't cause undefined behavior however it's called. So when you wrap `unsafe` in a safe API, you're asserting that the abstraction is sound for *every possible* input and use. That's a real proof obligation, and it's why the safe boundary usually sits at a module or function edge where you can reason about all the ways the code can be used. If even one input could make the unsafe core misbehave, the function should *not* be marked safe, it should stay `unsafe` and push the obligation to its callers. Drawing that boundary honestly, safe only when it's *truly* safe for all inputs, is the discipline that keeps the whole edifice standing.

## Quiz time

**Question #1**

What does it mean for a *function* to be marked `unsafe`, and what's required to call it?

<details class="solution">
<summary>Show solution</summary>

It means the function has preconditions the caller must uphold (which the compiler can't check) for it to behave correctly, calling it incorrectly can cause undefined behavior. The `unsafe` on the definition is a warning label about that contract. To call it, you must use an `unsafe` block, signaling that you've taken responsibility for meeting its requirements (e.g. `get_unchecked` trusts you to pass an in-bounds index).

</details>

**Question #2**

Why is `split_at_mut` able to be a *safe* function even though it uses `unsafe` internally?

<details class="solution">
<summary>Show solution</summary>

Because the wrapper guarantees every precondition the unsafe code needs, for all inputs. It `assert!`s `mid <= len` before the unsafe block, and the two resulting slices cover *disjoint* ranges (`[0, mid)` and `[mid, len)`), so they never alias, the borrow rule isn't actually violated, only un-provable by the compiler. Since no caller input can make the unsafe core misbehave, the function is safe to expose, and callers use it with no `unsafe`. The unsafety is fully contained.

</details>

**Question #3**

What proof obligation do you take on when you wrap `unsafe` code in a *safe* function?

<details class="solution">
<summary>Show solution</summary>

You're promising the whole program that the function cannot cause undefined behavior *no matter how it's called*, i.e. that the unsafe core is sound for *every possible* input and use the safe interface permits. If even one input could make it misbehave, it must not be marked safe (it should stay `unsafe` and push the obligation to callers). Drawing that boundary honestly, safe only when truly safe for all inputs, is what keeps a safe API built on unsafe code trustworthy.

</details>

You can now contain unsafety behind safe interfaces. The remaining two lessons apply this to the biggest real-world use of `unsafe`: talking to other languages. The next lesson ([25.4](https://learnrust.net/chapter-25/calling-c-from-rust/)) covers calling C functions from Rust, `extern "C"`, linking, and the unsafe boundary that crossing into another language always is.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
