How to Implement Unsafe Traits (Send, Sync)

You cannot manually implement `Send` or `Sync` for types containing interior mutability or unsafe code without using `unsafe` blocks, as these are marker traits that the compiler enforces automatically. To opt-in a custom type to these traits, you must declare an `unsafe impl` block asserting that y

When the compiler blocks your threads

You built a struct that wraps a raw pointer to a C library. It works perfectly in single-threaded code. You wrap it in an Arc to share it across threads, and the compiler rejects you with E0277. You wrap it in a Mutex, so you know the data is protected, but the compiler still blocks you. The compiler doesn't know your internal invariants. You have to tell it.

This happens when your type contains raw pointers, UnsafeCell, or other unsafe primitives. Rust's safety system automatically grants Send and Sync to types that are provably safe, but it strips those traits away the moment it sees unsafe components. You can restore them, but you must prove safety yourself.

Send and Sync are just stickers

Send and Sync are marker traits. They have no methods. They exist solely to tell the compiler whether a type can cross thread boundaries.

Think of them as stickers on a box. Send means the box can be shipped from one warehouse to another. Sync means the box can sit on a table while multiple people inspect it at the same time without breaking anything.

Rust slaps these stickers on automatically for most types. A String is Send and Sync. A Vec<i32> is Send and Sync. The compiler checks the fields recursively. If every field is Send, the struct is Send. If every field is Sync, the struct is Sync.

The moment you add a *mut T or an UnsafeCell, the compiler rips the stickers off. It assumes the worst. It assumes you might have a data race. It assumes you might corrupt memory. It blocks you from sending or sharing the type.

If you know the type is actually safe, you can paste the stickers back on using unsafe impl. This is a declaration to the compiler: "I have verified this type satisfies the safety guarantees. Trust me."

If you lie, you get undefined behavior. The compiler will not check your proof. It trusts the unsafe block. The burden of correctness shifts entirely to you.

The minimal unsafe impl

Here is the pattern. You have a struct with a raw pointer. You know the pointer is managed safely. You implement Send and Sync manually.

/// A wrapper around a raw pointer that is known to be thread-safe.
/// The underlying resource guarantees no data races.
struct SafePointer {
    ptr: *mut i32,
}

// SAFETY: SafePointer is Send because the underlying resource
// can be moved between threads. The pointer remains valid
// after the move, and no aliasing rules are violated.
unsafe impl Send for SafePointer {}

// SAFETY: SafePointer is Sync because the underlying resource
// supports concurrent access. Multiple threads can hold
// references to SafePointer without data races.
unsafe impl Sync for SafePointer {}

fn main() {
    // The struct is now eligible for threading.
    let ptr = SafePointer { ptr: std::ptr::null_mut() };
    
    // This compiles. The compiler trusts the unsafe impl.
    println!("SafePointer is Send and Sync");
}

The // SAFETY: comment is not optional flavor text. It is the proof. It lists the invariants that make the implementation correct. If you cannot write the invariants, you do not have a proof. Do not write the unsafe impl.

Convention aside: The Rust community treats // SAFETY: comments as legal contracts. Reviewers check them. If the comment is vague or missing, the code is rejected. Write the proof clearly.

How the compiler decides

Rust derives Send and Sync recursively. The rules are simple but strict.

A struct is Send if all its fields are Send. A struct is Sync if all its fields are Sync. This applies to enums and tuples as well.

Certain types break the chain. *mut T is neither Send nor Sync. *const T is neither Send nor Sync. UnsafeCell<T> is Send but not Sync. These types signal potential unsafety. The compiler stops auto-deriving when it encounters them.

UnsafeCell is the root of interior mutability. It allows mutation through shared references. Because mutation can cause data races, UnsafeCell is never Sync. Types like Cell<T>, RefCell<T>, and Rc<T> contain UnsafeCell, so they are not Sync. You cannot share them across threads via Arc.

Mutex<T> and RwLock<T> also contain UnsafeCell, but they implement Sync manually. They use unsafe impl to assert that their internal locking logic prevents data races. They restore the Sync sticker that UnsafeCell removed.

You follow the same pattern. If you build a thread-safe abstraction using UnsafeCell, you must implement Sync manually. If you build a type that can be moved between threads but not shared, you implement Send but not Sync.

The Sync shortcut

Here is a surprising fact. Sync is just Send for references.

A type T is Sync if and only if &T is Send. This equivalence is built into the language. You can think of Sync as "references to this type can be sent to other threads."

If &T is Send, multiple threads can hold &T simultaneously. Each thread has its own copy of the reference. The reference points to the same data. If the data is safe to access concurrently, T is Sync.

This explains why String is Sync. &String is Send. You can send a reference to a string to another thread. The string data is immutable through the reference. No data race.

This also explains why Cell<i32> is not Sync. &Cell<i32> is not Send. If you could send a reference to a Cell, two threads could mutate the same Cell through shared references. That is a data race. The compiler blocks it.

When you implement Sync, you are really proving that &T is safe to send. Your // SAFETY: comment should reflect this. State why concurrent reads (and writes, if applicable) are safe.

Real-world: Wrapping an FFI handle

FFI is the most common reason to implement Send and Sync. C libraries often use opaque pointers as handles. The C documentation says the handle is thread-safe. Rust sees a raw pointer and assumes it is not.

Here is a realistic example. You wrap a handle from a C library. The library guarantees the handle can be used from multiple threads. You need to tell Rust.

use std::os::raw::c_void;

/// Opaque handle to a C library context.
/// The C library documentation guarantees this handle
/// is safe to use from multiple threads concurrently.
struct LibContext {
    handle: *mut c_void,
}

// SAFETY: LibContext is Send because the C library allows
// the handle to be moved between threads. The handle
// remains valid after the move.
unsafe impl Send for LibContext {}

// SAFETY: LibContext is Sync because the C library allows
// concurrent access to the handle. Multiple threads can
// call C functions with this handle simultaneously.
unsafe impl Sync for LibContext {}

impl LibContext {
    /// Creates a new context.
    ///
    /// # Safety
    /// The caller must ensure the C library is initialized.
    pub unsafe fn new() -> Self {
        // Call C function to create handle.
        let handle = std::ptr::null_mut();
        Self { handle }
    }
}

impl Drop for LibContext {
    fn drop(&mut self) {
        // Call C function to destroy handle.
        // Safety: handle is valid until drop.
        unsafe {
            if !self.handle.is_null() {
                // destroy_handle(self.handle);
            }
        }
    }
}

The // SAFETY: comments cite the C library documentation. This is the proof. The reviewer checks the C docs. If the C docs say the handle is thread-safe, the unsafe impl is correct.

Convention aside: When wrapping FFI, always document the safety assumptions in the struct doc comment and the // SAFETY: blocks. Future maintainers need to know which C guarantees you are relying on. If the C library changes, your unsafe impl might become invalid.

Pitfalls and errors

Implementing Send and Sync incorrectly causes undefined behavior. The compiler will not catch it. The program might crash, corrupt data, or appear to work while silently destroying state.

Common pitfalls include:

  • Implementing Sync when the type has interior mutability without synchronization. A struct with a Cell field is not Sync. If you implement Sync manually, you enable data races.
  • Implementing Send when the type holds thread-local resources. A thread ID or a file descriptor bound to a specific thread cannot be moved. If you implement Send, you move the resource to the wrong thread. The behavior is undefined.
  • Forgetting that Send and Sync are recursive. If your struct contains a field that is not Send, the struct is not Send, even if you implement Send for the struct. Wait, that's wrong. If you implement Send manually, you override the derivation. The field does not matter. The manual impl takes precedence. This is a trap. You can implement Send for a struct containing a non-Send field, but only if you prove the field is safe to move. If the field is truly not Send, your proof is wrong.
  • Lying about invariants. You implement Sync because you want the code to compile. You haven't checked the invariants. You get UB.

The compiler error you see when you don't implement the traits is E0277 (trait bound not satisfied). This error tells you the type lacks Send or Sync. It points to the field causing the problem. It suggests wrapping the type in Arc<Mutex<T>> or implementing the traits manually.

If you see E0277, check the field. If the field is a raw pointer or UnsafeCell, you need unsafe impl. If the field is a Cell or RefCell, you need synchronization. Arc<Mutex<T>> is usually the right answer for RefCell-like types.

Don't fight the compiler here. If the compiler blocks you, there is a reason. Verify the safety before implementing the traits.

Decision matrix

Use unsafe impl Send when your type contains unsafe components but can be safely moved between threads. Use unsafe impl Send when you wrap an FFI handle that the C library documents as movable. Use unsafe impl Send when you build a custom smart pointer that manages heap allocation and can transfer ownership across threads.

Use unsafe impl Sync when your type contains unsafe components but supports concurrent access. Use unsafe impl Sync when you wrap an FFI handle that the C library documents as thread-safe. Use unsafe impl Sync when you build a synchronization primitive like a lock or a channel.

Use #[derive(Send, Sync)] when your type contains only safe fields. Use #[derive(Send, Sync)] when you don't need manual control. The compiler handles it automatically.

Use Arc<Mutex<T>> when you have a type that is not Sync and you need to share it. Use Arc<Mutex<T>> when the type has interior mutability and you need exclusive access. Use Arc<Mutex<T>> when you don't want to write unsafe code.

Use Rc<RefCell<T>> when you are in a single-threaded context. Use Rc<RefCell<T>> when you need shared ownership and interior mutability without threading.

Treat the // SAFETY: comment as a proof. If you can't write it, you don't have one.

Where to go next