#send-sync #traits #unsafe #auto #sync #no-alloc

no-std unchecked_wrap

Convenient UncheckedSync/Send wrapper types

1 unstable release

Uses new Rust 2024

new 0.1.0 Mar 23, 2025

#849 in Rust patterns

MIT license

13KB
101 lines

unchecked_wrap

This crate provides the UncheckedSync<T>, UncheckedSend<T>, and UncheckedSyncSend<T> types. They are transparent wrappers around T, except that they unconditionally implement Sync, Send, or both Sync and Send, respectively. They can be used for, e.g., a static mut replacement, sending pointers over thread boundaries, storing raw pointers in thread-safe abstractions, etc.

Constructing these wrappers is unsafe. However, the Unchecked wrapper types implement Deref and DerefMut, making them mostly usable as if they're just plain values of type T.

UncheckedSync<T>, UncheckedSend<T>, and UncheckedSyncSend<T> are all guaranteed to have the same size, alignment, and ABI as T.

Examples

A static mut replacement

Generally, global mutable state should be avoided. However, sometimes it is the right tool. This crate makes this usage pattern more convenient.

mod my_module {
    use std::cell::Cell;

    use unchecked_wrap::UncheckedSync;

    // SAFETY: Only accessed via functions in this module that should be
    // used only in a single thread
    static THING: UncheckedSync<Cell<u64>> = unsafe { UncheckedSync::new(Cell::new(0)) };

    /// # Safety
    /// Must be called in the same thread as other functions in this module.
    pub unsafe fn increment() {
        THING.set(THING.get() + 1);
    }

    /// # Safety
    /// Must be called in the same thread as other functions in this module.
    pub unsafe fn get() -> u64 {
        THING.get()
    }
}

// SAFETY: This doctest is single-threaded.
unsafe {
    my_module::increment();
    my_module::increment();
    assert_eq!(my_module::get(), 2);
}

Sending a raw pointer across thread boundaries

use unchecked_wrap::UncheckedSend;
let x = 123;
// Suppose that there's some reason that needs to be a raw pointer.
// SAFETY: A raw pointer doesn't actually have thread-safety invariants.
let ptr = unsafe { UncheckedSend::new(&raw const x) };
std::thread::scope(|scope| {
    scope.spawn(move || {
        // SAFETY: `x` is not deallocated yet, and is not modified
        assert_eq!(unsafe { **ptr }, 123);
    });
    scope.spawn(move || {
        // SAFETY: `x` is not deallocated yet, and is not modified
        assert_eq!(unsafe { **ptr }, 123);
    });
});

Storing a raw pointer in a thread-safe abstraction

use std::marker::PhantomData;
use std::ptr::NonNull;

use unchecked_wrap::UncheckedSyncSend;

struct MyBox<T> {
    // We use UncheckedSyncSend to ignore the auto traits from NonNull,
    // then we use PhantomData to get back the correct auto trait impls.
    // That is, MyBox<T> implements Send/Sync iff T implements Send/Sync.
    ptr: UncheckedSyncSend<NonNull<T>>,
    _phantom: PhantomData<T>,
}

// No need for error-prone implementations of Send and Sync.

impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        let ptr = NonNull::new(Box::into_raw(Box::new(value))).unwrap();
        Self {
            // SAFETY: A MyBox<T> is treated as if it owns T,
            ptr: unsafe { UncheckedSyncSend::new(ptr) },
            _phantom: PhantomData,
        }
    }
}
impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        let ptr = *self.ptr;
        // SAFETY: ptr was allocated via a Box
        unsafe {
            drop(Box::from_raw(ptr.as_ptr()));
        }
    }
}

let a_box = MyBox::new(String::from("abc"));
let join_handle = std::thread::spawn(move || {
    drop(a_box);
});
join_handle.join().unwrap();

A note on trait implementations

UncheckedSync, UncheckedSend, and UncheckedSyncSend do not implement common traits such as Debug or Hash

This is because, if, for example, UncheckedSync<T> were to implement Debug by forwarding to the Debug implementation of the T inside, and if a struct were to store an UncheckedSync/UncheckedSend, and a #[derive(Debug)] were to be applied to the struct, then the automatically-generated Debug impl might read the T value inside in a way that violates thread-safety. (There was previously an unsoundness in std due to a similar issue with ManuallyDrop.) In order to avoid this footgun, the Unchecked wrappers do not implement such traits at all.

However, UncheckedSync<T>, UncheckedSend<T>, and UncheckedSyncSend<T> each implement Copy when T: Copy. This is to facilitate storing raw pointers in contexts where they are known to be thread-safe, and then easily copy them around. I believe that Copy is unlikely to be a footgun in the aforementioned way.

No runtime deps