Rust introduces auto traits—such as Send and Sync—to solve the ergonomic burden of manually proving thread-safety for every composite type. Historically, systems programmers had to annotate each struct with complex concurrency contracts, which was error-prone and verbose. The compiler solves this by automatically implementing these traits for aggregate types (structs, enums, tuples) if and only if all their constituent fields implement them.
The problem arises with raw pointers (*const T and *mut T). Unlike references or smart pointers, raw pointers carry no ownership or aliasing semantics that the compiler can verify. They may point to thread-local storage, unallocated memory, or shared mutable state managed via external synchronization. Blindly applying Send or Sync to raw pointers based solely on T would violate memory safety, as the compiler cannot guarantee that the pointer is used correctly across thread boundaries.
The solution bifurcates the derivation logic. For aggregates, the compiler performs structural recursion: it checks every field. For raw pointers, the compiler explicitly withholds these implementations, treating them as opaque, potentially unsafe handles. This forces developers to use unsafe impl Send or unsafe impl Sync, taking personal responsibility for upholding the thread-safety guarantees that the compiler cannot infer.
use std::ptr::NonNull; // An aggregate type struct Container<T> { data: Vec<T>, // Vec<T> is Send if T is Send index: usize, } // Container<T> is automatically Send if T: Send // A type with a raw pointer struct Node<T> { value: T, next: *mut Node<T>, // Raw pointer breaks auto-derivation } // Explicit opt-in required unsafe impl<T: Send> Send for Node<T> {} unsafe impl<T: Sync> Sync for Node<T> {}
While developing a zero-allocation, lock-free MPMC (multi-producer, multi-consumer) ring buffer for a high-frequency trading application, I needed nodes to reside in a pre-allocated array to avoid jemalloc contention. The Node struct contained the payload and a *mut Node<T> next pointer forming an intrusive linked list. Upon attempting to send the buffer handle to a worker thread, the compiler rejected the code because Node did not implement Send, despite my knowledge that nodes were only accessed via atomic compare-and-swap operations.
I evaluated three solutions. First, replacing the raw pointer with Box<Node<T>>. This was rejected because Box implies heap ownership and individual allocations, which fragmented the cache-friendly ring buffer and introduced allocation latency unacceptable in HFT. Second, using NonNull<Node<T>> wrapped in AtomicPtr. While AtomicPtr itself is Send if T is Send, the containing Node struct still failed auto-derivation because the raw pointer inside NonNull (which is a wrapper around a raw pointer) blocked the structural check. Third, manually implementing Send and Sync using unsafe impl blocks.
I chose the third approach after formally verifying that all accesses to the next pointer were guarded by SeqCst atomic operations on a separate state index, ensuring that happens-before relationships prevented data races. This solution preserved the lock-free, zero-allocation architecture while satisfying Rust's type system. The result was a production-grade queue capable of processing millions of events per second without mutex overhead, though it required extensive SAFETY comments for future maintainers.
Why does a raw pointer to a Send type not automatically implement Send?
Candidates frequently assume that Send is "transitive" through all fields, including raw pointers. They fail to recognize that raw pointers are primitive types with no intrinsic ownership semantics. The compiler cannot distinguish between a pointer to thread-local storage and a pointer to shared heap memory, nor can it verify aliasing rules. Consequently, *const T and *mut T never implement Send or Sync automatically, regardless of T, forcing the programmer to use unsafe impl to take responsibility for the pointer's thread-safety contract.
How can I conditionally implement Send for a generic struct containing unsafe internals?
Many developers assume that unsafe impl must be unconditional. In reality, you can write unsafe impl<T> Send for MyType<T> where T: Send + 'static {}. This is essential for generic containers (like a custom UnsafeCell wrapper) that should only be Send when their contents are. Candidates miss that the where clause in an unsafe impl allows the same expressive power as safe traits, ensuring that thread-safety constraints propagate correctly through generic code without over-constraining the implementation.
What distinguishes the safety requirements for implementing Sync versus Send on a type with raw pointers?
Send requires only that transferring ownership of the value across thread boundaries is safe. For a raw pointer, this usually means moving the address value is safe if the pointee is Send. Sync, however, requires that sharing immutable references (&Self) across threads is safe. If &Node exposes the raw pointer value (which could be dereferenced), and another thread mutates the pointee through a mutable reference, this constitutes a data race. Therefore, Sync implementations for raw-pointer-containing types almost always require proof of synchronized access (e.g., the pointer is only accessed under a Mutex or via atomic operations), whereas Send might only require proof of unique ownership transfer.