Course contentsShow
← Rust
Lesson 1140 of 1,91010. Concurrency PrimitivesFree lesson

Designing Send-Only APIs

You will build abstractions that enforce thread-affinity for specific resource types.

In this lesson, you will learn how to design "Send-Only" APIs that enforce thread-affinity by leveraging the !Send marker trait to keep specific resources bound to their original thread.

The Strategy of Thread-Affinity

In concurrent systems, certain resources—such as hardware device handles, UI elements, or legacy C libraries—are not thread-safe and must be accessed only from the thread that initialized them. While Rust’s Send trait usually allows a type to be moved across thread boundaries, we can intentionally opt-out of this behavior to build robust, thread-affine abstractions.

By explicitly designing an API that cannot be moved, we transform runtime concurrency bugs into compile-time errors.

Implementing a Send-Only Wrapper

To enforce this, we use the PhantomData trick combined with a !Send marker. If we wrap our resource in a struct that contains PhantomData<*const ()>, the compiler treats that struct as !Send because raw pointers are not Send.

use std::marker::PhantomData;

/// A resource that must live and die on the thread it was created.
pub struct ThreadBoundResource<T> {
    data: T,
    _marker: PhantomData<*const ()>, // This makes the struct !Send
}

impl<T> ThreadBoundResource<T> {
    pub fn new(data: T) -> Self {
        Self {
            data,
            _marker: PhantomData,
        }
    }

    pub fn access(&self) -> &T {
        &self.data
    }
}

Why This Matters for APIs

When you hand this ThreadBoundResource to a user of your library, they simply cannot use it in a thread::spawn block or pass it to a channel receiver located on another thread. If they try, the compiler will stop them with: "*const () cannot be sent between threads safely."

This is the essence of a "Send-Only" API: you aren't just protecting data from races; you are proactively restricting the topology of your application to ensure that non-thread-safe logic never leaves its home. By combining this with the Sync trait principles we discussed earlier, you create APIs that guide users toward safe usage patterns, ensuring that the heavy lifting of concurrency is handled by your architecture rather than the consumer.

Key Takeaway: By using PhantomData with a *const () pointer, you can mark your types as !Send to guarantee they remain pinned to their originating thread, effectively turning thread-affinity requirements into compile-time safety rules.