Skip to Content
Course content

199: Implementing Unsafe Traits

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're a building inspector. Usually, the city has a strict code that automatically rejects any blueprint that doesn't meet specific safety standards. But sometimes, a world-class architect comes along with a revolutionary design that doesn't fit the standard code, even though it's actually safer than the traditional way. To get that building approved, the architect doesn't just ask for a favor; they sign a legal affidavit. They are essentially saying, "I have personally verified that this structure is sound, and I take full legal responsibility if it collapses."

Implementing an unsafe trait is exactly like signing that affidavit. In most of Rust, the compiler is the inspector, checking every line of your code. But with unsafe traits, the compiler steps back and says, "I can't prove this is safe, so I'm going to let you decide. If you tell me it's safe, I'll trust you—but if you're wrong, the resulting memory corruption or data race is entirely on your head."

The Handshake Between You and the Compiler

Normally, when you implement a trait, you're just providing some functionality. But when a trait is marked as unsafe, the act of implementing it is what's dangerous. This is different from calling an unsafe fn. With an unsafe function, you're saying "this specific call is safe." With an unsafe trait, you're making a permanent claim about the nature of your type.

The syntax reflects this gravity. You don't just write impl Trait for Type; you have to write unsafe impl Trait for Type. That unsafe keyword is your signature on the affidavit.

Dealing with the Heavy Hitters: Send and Sync

You've probably encountered Send and Sync before. They are the most common unsafe traits in the ecosystem. Most of the time, the compiler implements these for you automatically if all the members of your struct are also Send or Sync. But what happens when you're dealing with raw pointers?

Raw pointers (*mut T and *const T) are explicitly !Send and !Sync because Rust has no way of knowing if the memory they point to is managed in a thread-safe way. But if you're wrapping a C library that you know is thread-safe, you'll find yourself stuck. The compiler will refuse to let you move your wrapper across thread boundaries.

struct MyCWrapper {
    handle: *mut libc::c_void,
}

// This will fail if we try to pass it to std::thread::spawn
// because MyCWrapper is !Send.

To fix this, you have to step in and tell Rust that your wrapper is safe to move between threads:

unsafe impl Send for MyCWrapper {}
unsafe impl Sync for MyCWrapper {}

I want to be clear: there is no code inside those curly braces. You aren't "implementing" a method. You are simply asserting a property about your type. If MyCWrapper actually points to some non-thread-safe global state in a C library, you've just introduced a subtle, nightmare-inducing data race into your program.

When the Compiler Gets Too Nervous

You might wonder why we don't just make everything Send. The reason is that the compiler is conservative for a good reason. If you have a type that relies on thread-local storage, moving it to another thread would cause a crash or undefined behavior. By making Send an unsafe trait, Rust ensures that the human developer—the only one who knows the external context of the data—is the one making the call.

When I'm reviewing code and I see unsafe impl, it's the first place I look. I don't look for the code; I look for the justification. If you're writing this in a real project, always leave a comment above the implementation explaining exactly why the trait's invariants are upheld. Future you (and your teammates) will thank you when you're debugging a segfault six months from now.




📋 Practical Task

Implementing Thread-Safety for a Raw Pointer Wrapper

You are building a wrapper for a legacy C-API that manages a shared configuration object. The C-API documentation guarantees that the configuration object is read-only and can be safely accessed from any number of threads simultaneously. However, because your wrapper uses a raw pointer, Rust is treating it as !Send and !Sync.

Your Task: Complete the code below. Create a struct named ConfigHandle that wraps a *const u8. Then, implement the necessary unsafe traits to allow ConfigHandle to be sent across threads and shared between threads. Finally, demonstrate this by moving the handle into a new thread using std::thread::spawn.


use std::thread;

struct ConfigHandle {
    ptr: *const u8,
}

// TODO: Implement the unsafe traits here to make ConfigHandle 
// Send and Sync.

fn main() {
    let handle = ConfigHandle { ptr: std::ptr::null() };

    // This will only compile if you've correctly implemented the unsafe traits
    let thread_handle = thread::spawn(move || {
        println!("Successfully moved the handle to a new thread!");
    });

    thread_handle.join().unwrap();
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.