| |

Rust 95 🦀 Implementing Unsafe Traits

⏱️ Estimated read: 13 min | |

An unsafe trait is a trait whose implementations must uphold a safety contract that the compiler cannot verify. Declaring the trait with unsafe trait tells implementors that there is a proof obligation; implementing it with unsafe impl is the programmer’s assertion that the obligation has been discharged. The pattern exists for traits like Send and Sync, where a wrong implementation can cause undefined behavior in code that relies on the trait’s guarantee, even though the implementation itself contains no unsafe operations .

This chapter covers the two-sided contract of an unsafe trait, the difference between a safe method in an unsafe trait and an unsafe method in a safe trait, and the documentation that makes the contract reviewable.

Key point: An unsafe trait does not make its methods unsafe to call. It makes the implementation unsafe to write. The unsafe impl block is a promise that the type upholds the invariant, and the compiler trusts that promise when it uses the trait’s guarantee elsewhere.


Why unsafe traits exist

The marker trait problem. Send and Sync have no methods. They are markers: a type that implements Send is safe to move to another thread, and a type that implements Sync is safe to share between threads through a shared reference. The compiler auto-derives these traits for most types, but a manual implementation is sometimes necessary—for a wrapper around a raw pointer, for example. The unsafe impl block is where the programmer asserts that the manual implementation is correct, because a wrong Send impl allows safe code to cause a data race .

The invariant problem. A trait can encode a postcondition that the compiler cannot enforce. The Rust documentation uses MakeEven as the example: the trait’s documentation says make_even must return an even number. The implementation uses unreachable_unchecked, which is only sound if the evenness invariant holds. The unsafe trait declaration tells the implementor that they are responsible for upholding the invariant; the unsafe impl block is the assertion that they have .

The unsoundness problem. If a safe trait with an unsafe method could be implemented by anyone, a safe implementation could fail to uphold the method’s contract. An unsafe trait closes that gap: the trait itself carries the obligation, so only an unsafe impl can satisfy it. This is the difference between “the method has preconditions the caller must check” and “the implementation has postconditions the implementor must guarantee” .


a. The Two-Sided Contract

The unsafe trait declaration defines a proof obligation. The unsafe impl block discharges it. The two keywords are duals: one declares the contract, the other asserts that the contract is upheld .

/// # Safety
///
/// Implementors must guarantee that `three()` always returns a valid
/// reference to the element at index 3. The implementation may use
/// unchecked indexing because callers rely on this guarantee.
unsafe trait ThreeIndexable<T> {
    fn three(&self) -> &T;
}

// The implementation for a 4-element array is sound because
// index 3 always exists.
unsafe impl<T> ThreeIndexable<T> for [T; 4] {
    fn three(&self) -> &T {
        // SAFETY: the array has exactly 4 elements, so index 3 is valid.
        unsafe { self.get_unchecked(3) }
    }
}

The three method is a safe function. A caller does not need an unsafe block to call it. The unsafety is entirely in the implementation: the trait’s contract says three always returns a valid reference, and the unsafe impl asserts that the get_unchecked(3) call is sound because the array length guarantees the index is in bounds. If the impl were written for a slice of unknown length without a check, it would violate the contract .


b. Unsafe Trait vs. Unsafe Method

A common confusion is between an unsafe trait and a trait with an unsafe method. The two are different, and the difference determines where the proof obligation lives.

PatternObligationWho Must Prove It
unsafe traitImplementation postconditionThe implementor
unsafe fn in safe traitCaller preconditionThe caller
unsafe fn in unsafe traitBothBoth

An unsafe fn in a safe trait has a precondition that the caller must satisfy. The implementation is safe to write, because the caller has already discharged the obligation . An unsafe trait has a postcondition that the implementation must guarantee. The caller is safe, because the implementation has already discharged the obligation.

// Unsafe method in a safe trait: caller's obligation
trait Indexable {
    const LEN: usize;
    /// # Safety
    /// `idx` must be less than `LEN`.
    unsafe fn idx_unchecked(&self, idx: usize) -> i32;
}

// The implementation is safe to write; the caller checks the bound.
impl Indexable for i32 {
    const LEN: usize = 1;
    unsafe fn idx_unchecked(&self, _idx: usize) -> i32 {
        *self
    }
}

// The implementation is safe to write.
impl<const N: usize> Indexable for [i32; N] {
    const LEN: usize = N;
    unsafe fn idx_unchecked(&self, idx: usize) -> i32 {
        // SAFETY: the caller guaranteed idx < LEN == N.
        *self.get_unchecked(idx)
    }
}

The unsafe block inside the array implementation is discharged by the caller’s obligation. The unsafe fn signature is the trait’s way of saying “the caller must prove idx < LEN“. The implementation does not have to prove it; it can assume it .


c. Documentation and the 2024 Edition

The # Safety doc comment is not optional for an unsafe trait. It is the specification of the postcondition that the implementation must uphold. Without it, the unsafe impl block is an unverified assertion, and a reviewer has no way to check whether the implementation is sound.

The 2024 edition tightens the rules around unsafe code in several ways that affect unsafe traits. The unsafe_op_in_unsafe_fn lint is now warn-by-default, which means an unsafe fn body is no longer automatically treated as an unsafe block. Operations inside an unsafe fn must be wrapped in an explicit unsafe {} block, which separates the caller’s obligation from the implementation’s operations . The unsafe extern block and the #[unsafe(no_mangle)] attribute changes are separate, but they follow the same principle: every unsafe obligation must be explicit and reviewable.

unsafe trait EvenProducer {
    fn next_even(&mut self) -> i32;
}

unsafe impl EvenProducer for Counter {
    fn next_even(&mut self) -> i32 {
        self.value += 2;
        // The postcondition (evenness) is guaranteed by the increment.
        self.value
    }
}

The implementation contains no unsafe block because it does not call any unsafe operations. The unsafe impl is still required because the trait’s postcondition is a safety contract. The implementation upholds it through ordinary safe arithmetic, and the unsafe impl asserts that the arithmetic is correct.


Complete Example Session

// ============================================
// PART 1: UNSAFE TRAIT DECLARATION
// ============================================
/// # Safety
///
/// Implementors must guarantee that `get_unchecked` returns a valid
/// reference for any index less than `len`.
unsafe trait FixedLen {
    fn len(&self) -> usize;
    fn get_unchecked(&self, idx: usize) -> &i32;
}


// ============================================
// PART 2: UNSAFE IMPL
// ============================================
struct FourInts([i32; 4]);

unsafe impl FixedLen for FourInts {
    fn len(&self) -> usize { 4 }

    fn get_unchecked(&self, idx: usize) -> &i32 {
        // SAFETY: the contract says idx < len == 4.
        unsafe { self.0.get_unchecked(idx) }
    }
}


// ============================================
// PART 3: SAFE CALLER
// ============================================
fn use_fixed_len<T: FixedLen>(t: &T) -> &i32 {
    // No unsafe block needed; the trait's contract covers it.
    t.get_unchecked(0)
}


// ============================================
// PART 4: UNSAFE METHOD IN SAFE TRAIT
// ============================================
trait Indexable {
    const LEN: usize;
    /// # Safety
    /// `idx` must be less than `LEN`.
    unsafe fn idx_unchecked(&self, idx: usize) -> i32;
}

impl Indexable for [i32; 4] {
    const LEN: usize = 4;
    unsafe fn idx_unchecked(&self, idx: usize) -> i32 {
        // SAFETY: caller guaranteed idx < LEN.
        unsafe { *self.get_unchecked(idx) }
    }
}


// ============================================
// PART 5: CALLER DISCHARGES PRECONDITION
// ============================================
fn call_indexable(arr: &[i32; 4]) -> i32 {
    // SAFETY: 0 < LEN == 4.
    unsafe { arr.idx_unchecked(0) }
}


// ============================================
// PART 6: THE POSTCONDITION CONTRACT
// ============================================
unsafe trait EvenProducer {
    fn next_even(&mut self) -> i32;
}

struct Counter { value: i32 }

unsafe impl EvenProducer for Counter {
    fn next_even(&mut self) -> i32 {
        self.value += 2;
        self.value
    }
}


// ============================================
// PART 7: THE `unsafe_op_in_unsafe_fn` LINT
// ============================================
// In 2024 edition, this warns:
unsafe fn bad_example(p: *const i32) -> i32 {
    *p  // warning: unsafe operation in unsafe fn
}

// Explicit block:
unsafe fn good_example(p: *const i32) -> i32 {
    // SAFETY: caller guarantees p is valid.
    unsafe { *p }
}


// ============================================
// PART 8: SEND/SYNC MANUAL IMPL
// ============================================
struct MyPtr(*mut i32);
// SAFETY: access is externally synchronized.
unsafe impl Send for MyPtr {}


// ============================================
// PART 9: DOCUMENTATION REQUIREMENT
// ============================================
// Every unsafe trait needs a `# Safety` section
// documenting the postcondition the implementation
// must uphold.


// ============================================
// PART 10: REVIEW CHECKLIST
// ============================================
// - Does the trait have a `# Safety` section?
// - Does the impl discharge the documented postcondition?
// - Are unsafe operations inside explicit unsafe blocks?
// - Is the unsafe region as small as possible?

The ten parts trace the declaration, the implementation, the safe caller, and the contrasting pattern of an unsafe method in a safe trait. Part 7 shows the 2024 edition lint that requires explicit unsafe blocks inside unsafe fn.


Quick Reference

Unsafe Trait vs. Unsafe Method

DeclarationObligationImplementor’s JobCaller’s Job
unsafe traitPostconditionProve itNone
unsafe fn in safe traitPreconditionNoneProve it
unsafe fn in unsafe traitBothProve postconditionProve precondition

Keyword Roles

KeywordRoleExample
unsafe traitDeclares a contractunsafe trait Send {}
unsafe implDischarges the contractunsafe impl Send for T {}
unsafe fnDeclares a caller obligationunsafe fn get_unchecked()
unsafe {}Discharges an obligationunsafe { *ptr }

Standard Library Unsafe Traits

TraitMeaning
SendSafe to move to another thread
SyncSafe to share between threads
GlobalAllocCustom memory allocator

Best Practices

✅ Do This:

/// # Safety
/// Implementors must guarantee that the index is always in bounds.
unsafe trait BoundedIndex {
    fn get(&self, idx: usize) -> &i32;
}                                                                       // ✅
// SAFETY: the array has 4 elements, so index 3 is always valid.
unsafe impl<T> ThreeIndexable<T> for [T; 4] { ... }                     // ✅
// Wrap unsafe operations in explicit blocks (2024 edition)
unsafe fn f(p: *const i32) -> i32 { unsafe { *p } }                     // ✅

❌ Don’t Do This:

// Implement an unsafe trait without unsafe impl
impl Send for MyPtr {}  // compile error                           // ❌
// Document the contract vaguely
/// # Safety
/// Be careful.                                                     // ❌
// Assume the unsafe trait makes methods unsafe to call
// The contract is on the implementation, not the call               // ❌

Common Pitfalls

PitfallWhy It HappensFix
impl without unsafeUnsafe trait requires unsafe implAdd unsafe
Unsound Send implManual impl does not uphold thread safetyEnsure external synchronization
Missing # Safety docsContract not specifiedDocument the postcondition
unsafe fn body assumed safe2024 lint warnsUse explicit unsafe {} blocks
Trait postcondition violatedImplementation does not uphold invariantReview and fix the implementation

Real-World Examples

1. Manual Send Impl

unsafe impl Send for MyPtr {}

2. ThreeIndexable

unsafe impl<T> ThreeIndexable<T> for [T; 4] { ... }

3. EvenProducer

unsafe impl EvenProducer for Counter { ... }

4. Unsafe Method in Safe Trait

trait Indexable { unsafe fn idx_unchecked(&self, idx: usize) -> i32; }

5. Explicit Unsafe Block in Unsafe Fn

unsafe fn f(p: *const i32) -> i32 { unsafe { *p } }

6. Safety Documentation

/// # Safety
/// `idx` must be less than `self.len()`.

7. Custom Allocator

unsafe impl GlobalAlloc for MyAlloc { ... }

8. Sync Impl

unsafe impl Sync for MyType {}

9. Trait Postcondition

/// Implementors must return an even number.
unsafe trait EvenProducer { fn next_even(&mut self) -> i32; }

10. Caller Precondition

// SAFETY: idx < LEN is checked before the call.
unsafe { arr.idx_unchecked(0) }

Visual

The Two-Sided Contract

┌──────────────────────────────────────────────────────────────┐
│  unsafe trait EvenProducer {                                 │
│      fn next_even(&mut self) -> i32;                         │
│  }                                                           │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ POSTCONDITION: the return value must be even            ││
│  │ The compiler cannot check this.                         ││
│  └─────────────────────────────────────────────────────────┘│
│           │                                                  │
│           │ unsafe impl                                      │
│           ▼                                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ IMPLEMENTOR'S JOB: prove the postcondition              ││
│  │ "self.value += 2; return self.value;"                   ││
│  │ Adding 2 to any integer preserves evenness.             ││
│  └─────────────────────────────────────────────────────────┘│
│           │                                                  │
│           ▼                                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ CALLER: safe to call next_even()                        ││
│  │ The implementation has guaranteed the postcondition.    ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  The unsafe keyword is on the impl, not the call.            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Unsafe Trait vs Unsafe Method

┌──────────────────────────────────────────────────────────────┐
│  unsafe trait                       safe trait               │
│  ┌─────────────────┐                ┌─────────────────┐     │
│  │ fn three(&self) │                │ unsafe fn       │     │
│  │ → &T            │                │ idx_unchecked() │     │
│  │                 │                │ → i32           │     │
│  └────────┬────────┘                └────────┬────────┘     │
│           │                                  │               │
│           ▼                                  ▼               │
│  ┌─────────────────┐                ┌─────────────────┐     │
│  │ IMPL must prove │                │ CALLER must     │     │
│  │ postcondition   │                │ prove           │     │
│  │ (index valid)   │                │ precondition    │     │
│  └─────────────────┘                │ (idx < LEN)     │     │
│                                     └─────────────────┘     │
│                                                              │
│  unsafe impl                        unsafe { call }          │
│                                                              │
│  The obligation moves.                                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2024 Edition: Explicit Unsafe Blocks

┌──────────────────────────────────────────────────────────────┐
│  2021 EDITION                   2024 EDITION                 │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ unsafe fn f() { │            │ unsafe fn f() { │         │
│  │   *p  // ok     │            │   unsafe { *p } │         │
│  │ }               │            │ }               │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  Body treated as                Explicit block required      │
│  unsafe block by default        (warning by default)         │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ The unsafe_op_in_unsafe_fn lint separates the two roles:││
│  │ - unsafe fn: caller's obligation                        ││
│  │ - unsafe {}: implementation's discharge                 ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

When to Use Which

┌──────────────────────────────────────────────────────────────┐
│  DO YOU NEED A TRAIT?                                        │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Marker trait    │────► unsafe trait (Send, Sync)         │
│  │ (no methods)    │      Implementor proves the guarantee   │
│  └─────────────────┘                                        │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Method with     │────► unsafe fn in safe trait           │
│  │ precondition    │      Caller proves the bound            │
│  └─────────────────┘                                        │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Method with     │────► unsafe trait + unsafe fn          │
│  │ postcondition   │      Both sides prove their part        │
│  └─────────────────┘                                        │
│                                                              │
│  The choice depends on whether the obligation belongs        │
│  to the caller or the implementor.                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Unsafe traitTrait with a safety contract on implementations
Unsafe implAssertion that the contract is upheld
Marker traitsSend, Sync; no methods, only guarantees
Unsafe method in safe traitCaller’s obligation
Unsafe trait postconditionImplementor’s obligation
# Safety docsSpecification of the contract
2024 editionunsafe_op_in_unsafe_fn warns by default
Safe caller of unsafe trait methodNo unsafe block needed

Key takeaways:

  • An unsafe trait is a contract on the implementation, not on the call. The unsafe trait declaration defines a postcondition that the implementor must guarantee. The unsafe impl block asserts that the guarantee holds. A caller of the trait’s methods does not need an unsafe block because the implementation has already discharged the obligation .
  • The # Safety documentation is the specification. Without it, the unsafe impl is an unverified assertion. The documentation states what the implementation must guarantee, and the implementation’s SAFETY comments state why it does. The two together make the contract reviewable .
  • Unsafe trait vs. unsafe method is a question of where the obligation lives. An unsafe fn in a safe trait has a precondition that the caller must satisfy. An unsafe trait has a postcondition that the implementor must satisfy. An unsafe fn in an unsafe trait has both .
  • The 2024 edition requires explicit unsafe blocks inside unsafe fn. The unsafe_op_in_unsafe_fn lint separates the caller’s obligation (the unsafe fn signature) from the implementation’s operations (the unsafe {} block). This makes the unsafe region visible and reviewable .
  • The standard library’s Send and Sync are the canonical examples. A manual unsafe impl Send asserts that the type is safe to move to another thread. A wrong assertion allows safe code to cause a data race, which is undefined behavior. The unsafe keyword is the programmer’s acceptance of that responsibility .

Remember: The unsafe keyword has two roles: it declares an obligation and it discharges one. The unsafe trait declares the obligation; the unsafe impl discharges it. The unsafe fn declares a caller obligation; the unsafe {} block discharges it. Mixing these roles is the source of confusion, and the 2024 edition’s lints exist to separate them. An unsafe trait is not a trait whose methods are unsafe to call; it is a trait whose implementations are unsafe to write. The implementor is the one who must prove the contract, and the unsafe impl block is where the proof is asserted. When the contract is documented and the implementation upholds it, the trait’s users can rely on the guarantee as if the compiler had checked it.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!