Rust 95 🦀 Implementing Unsafe Traits
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.
| Pattern | Obligation | Who Must Prove It |
|---|---|---|
unsafe trait | Implementation postcondition | The implementor |
unsafe fn in safe trait | Caller precondition | The caller |
unsafe fn in unsafe trait | Both | Both |
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
| Declaration | Obligation | Implementor’s Job | Caller’s Job |
|---|---|---|---|
unsafe trait | Postcondition | Prove it | None |
unsafe fn in safe trait | Precondition | None | Prove it |
unsafe fn in unsafe trait | Both | Prove postcondition | Prove precondition |
Keyword Roles
| Keyword | Role | Example |
|---|---|---|
unsafe trait | Declares a contract | unsafe trait Send {} |
unsafe impl | Discharges the contract | unsafe impl Send for T {} |
unsafe fn | Declares a caller obligation | unsafe fn get_unchecked() |
unsafe {} | Discharges an obligation | unsafe { *ptr } |
Standard Library Unsafe Traits
| Trait | Meaning |
|---|---|
Send | Safe to move to another thread |
Sync | Safe to share between threads |
GlobalAlloc | Custom 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
impl without unsafe | Unsafe trait requires unsafe impl | Add unsafe |
Unsound Send impl | Manual impl does not uphold thread safety | Ensure external synchronization |
Missing # Safety docs | Contract not specified | Document the postcondition |
unsafe fn body assumed safe | 2024 lint warns | Use explicit unsafe {} blocks |
| Trait postcondition violated | Implementation does not uphold invariant | Review 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
| Item | Value |
|---|---|
| Unsafe trait | Trait with a safety contract on implementations |
| Unsafe impl | Assertion that the contract is upheld |
| Marker traits | Send, Sync; no methods, only guarantees |
| Unsafe method in safe trait | Caller’s obligation |
| Unsafe trait postcondition | Implementor’s obligation |
# Safety docs | Specification of the contract |
| 2024 edition | unsafe_op_in_unsafe_fn warns by default |
| Safe caller of unsafe trait method | No unsafe block needed |
Key takeaways:
- An unsafe trait is a contract on the implementation, not on the call. The
unsafe traitdeclaration defines a postcondition that the implementor must guarantee. Theunsafe implblock asserts that the guarantee holds. A caller of the trait’s methods does not need anunsafeblock because the implementation has already discharged the obligation . - The
# Safetydocumentation is the specification. Without it, theunsafe implis an unverified assertion. The documentation states what the implementation must guarantee, and the implementation’sSAFETYcomments 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 fnin a safe trait has a precondition that the caller must satisfy. Anunsafe traithas a postcondition that the implementor must satisfy. Anunsafe fnin anunsafe traithas both . - The 2024 edition requires explicit
unsafeblocks insideunsafe fn. Theunsafe_op_in_unsafe_fnlint separates the caller’s obligation (theunsafe fnsignature) from the implementation’s operations (theunsafe {}block). This makes the unsafe region visible and reviewable . - The standard library’s
SendandSyncare the canonical examples. A manualunsafe impl Sendasserts 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. Theunsafekeyword 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.
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!