ExamplesBy LevelBy TopicLearning Paths
702 Advanced

unsafe trait

Functional Programming

Tutorial

The Problem

This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.

🎯 Learning Outcomes

  • • The specific unsafe feature demonstrated: unsafe trait
  • • When this feature is necessary vs when safe alternatives exist
  • • How to use it correctly with appropriate SAFETY documentation
  • • The invariants that must be maintained for the operation to be sound
  • • Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • Code Example

    #![allow(clippy::all)]
    //! 702 — Unsafe Traits
    //! unsafe trait, unsafe impl, and Send/Sync marker traits.
    
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    // ── Custom unsafe trait ───────────────────────────────────────────────────
    
    /// Marker trait: implementors guarantee this type can cross thread boundaries.
    ///
    /// # Safety
    /// The type must not contain non-thread-safe interior mutability or raw
    /// pointers that could alias across threads.
    pub unsafe trait ThreadSafe: Send + Sync {
        fn describe(&self) -> String;
    }
    
    // ── A type safe to implement ThreadSafe on ────────────────────────────────
    
    pub struct AtomicCounter {
        value: std::sync::atomic::AtomicI64,
    }
    
    impl AtomicCounter {
        pub fn new(v: i64) -> Self {
            Self {
                value: std::sync::atomic::AtomicI64::new(v),
            }
        }
        pub fn get(&self) -> i64 {
            self.value.load(std::sync::atomic::Ordering::SeqCst)
        }
        pub fn increment(&self) {
            self.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        }
    }
    
    // SAFETY: AtomicCounter uses AtomicI64 — all operations are thread-safe.
    // No raw pointers, no non-Sync interior mutability.
    unsafe impl ThreadSafe for AtomicCounter {
        fn describe(&self) -> String {
            format!("AtomicCounter({})", self.get())
        }
    }
    
    // ── Type that is NOT Send (contains *mut T) ───────────────────────────────
    
    pub struct NotSend {
        _ptr: *mut i32, // *mut T is !Send by default
    }
    
    // fn use_not_send_in_thread(x: NotSend) {
    //     thread::spawn(move || drop(x));  // compile error: *mut i32 is not Send
    // }
    
    // ── Demonstrate Send/Sync in practice ────────────────────────────────────
    
    fn run_in_thread<T: ThreadSafe + 'static>(val: Arc<T>) {
        let v = Arc::clone(&val);
        thread::spawn(move || println!("Thread: {}", v.describe()))
            .join()
            .unwrap();
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_atomic_counter_send_sync() {
            fn assert_send_sync<T: Send + Sync>() {}
            assert_send_sync::<AtomicCounter>(); // won't compile if wrong
        }
    
        #[test]
        fn test_threadsafe_describe() {
            let c = AtomicCounter::new(42);
            assert_eq!(c.describe(), "AtomicCounter(42)");
        }
    
        #[test]
        fn test_concurrent_increment() {
            let c = Arc::new(AtomicCounter::new(0));
            let handles: Vec<_> = (0..10)
                .map(|_| {
                    let cc = Arc::clone(&c);
                    thread::spawn(move || cc.increment())
                })
                .collect();
            for h in handles {
                h.join().unwrap();
            }
            assert_eq!(c.get(), 10);
        }
    }

    Key Differences

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • • C FFI via the ctypes library for external function calls
  • Bigarray for controlled raw memory access
  • • The GC for memory management (no manual allocators needed)
  • Bytes.t for mutable byte sequences
  • OCaml programs rarely need operations equivalent to these Rust unsafe patterns.

    Full Source

    #![allow(clippy::all)]
    //! 702 — Unsafe Traits
    //! unsafe trait, unsafe impl, and Send/Sync marker traits.
    
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    // ── Custom unsafe trait ───────────────────────────────────────────────────
    
    /// Marker trait: implementors guarantee this type can cross thread boundaries.
    ///
    /// # Safety
    /// The type must not contain non-thread-safe interior mutability or raw
    /// pointers that could alias across threads.
    pub unsafe trait ThreadSafe: Send + Sync {
        fn describe(&self) -> String;
    }
    
    // ── A type safe to implement ThreadSafe on ────────────────────────────────
    
    pub struct AtomicCounter {
        value: std::sync::atomic::AtomicI64,
    }
    
    impl AtomicCounter {
        pub fn new(v: i64) -> Self {
            Self {
                value: std::sync::atomic::AtomicI64::new(v),
            }
        }
        pub fn get(&self) -> i64 {
            self.value.load(std::sync::atomic::Ordering::SeqCst)
        }
        pub fn increment(&self) {
            self.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        }
    }
    
    // SAFETY: AtomicCounter uses AtomicI64 — all operations are thread-safe.
    // No raw pointers, no non-Sync interior mutability.
    unsafe impl ThreadSafe for AtomicCounter {
        fn describe(&self) -> String {
            format!("AtomicCounter({})", self.get())
        }
    }
    
    // ── Type that is NOT Send (contains *mut T) ───────────────────────────────
    
    pub struct NotSend {
        _ptr: *mut i32, // *mut T is !Send by default
    }
    
    // fn use_not_send_in_thread(x: NotSend) {
    //     thread::spawn(move || drop(x));  // compile error: *mut i32 is not Send
    // }
    
    // ── Demonstrate Send/Sync in practice ────────────────────────────────────
    
    fn run_in_thread<T: ThreadSafe + 'static>(val: Arc<T>) {
        let v = Arc::clone(&val);
        thread::spawn(move || println!("Thread: {}", v.describe()))
            .join()
            .unwrap();
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_atomic_counter_send_sync() {
            fn assert_send_sync<T: Send + Sync>() {}
            assert_send_sync::<AtomicCounter>(); // won't compile if wrong
        }
    
        #[test]
        fn test_threadsafe_describe() {
            let c = AtomicCounter::new(42);
            assert_eq!(c.describe(), "AtomicCounter(42)");
        }
    
        #[test]
        fn test_concurrent_increment() {
            let c = Arc::new(AtomicCounter::new(0));
            let handles: Vec<_> = (0..10)
                .map(|_| {
                    let cc = Arc::clone(&c);
                    thread::spawn(move || cc.increment())
                })
                .collect();
            for h in handles {
                h.join().unwrap();
            }
            assert_eq!(c.get(), 10);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_atomic_counter_send_sync() {
            fn assert_send_sync<T: Send + Sync>() {}
            assert_send_sync::<AtomicCounter>(); // won't compile if wrong
        }
    
        #[test]
        fn test_threadsafe_describe() {
            let c = AtomicCounter::new(42);
            assert_eq!(c.describe(), "AtomicCounter(42)");
        }
    
        #[test]
        fn test_concurrent_increment() {
            let c = Arc::new(AtomicCounter::new(0));
            let handles: Vec<_> = (0..10)
                .map(|_| {
                    let cc = Arc::clone(&c);
                    thread::spawn(move || cc.increment())
                })
                .collect();
            for h in handles {
                h.join().unwrap();
            }
            assert_eq!(c.get(), 10);
        }
    }

    Exercises

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos