Skip to content

AssertThreadSafe (name TBD) – a more general API for lifting conservative !Send or !Sync implementations #322

Open
@steffahn

Description

Proposal

Problem statement

This proposes a generalization of the existing unstable API SyncUnsafeCell. The SyncUnsafeCell type – as far as I understand from the PR that adds it – serves (at least) two purposes:

  • allow to be a replacement for static mut
  • simplify implementation of synchronization primitives, because explicit Send or Sync implementations could be skipped.

The motivation for skipping explicit Send or Sync implementations isn’t fully explained. However my take on this is that it allows to reduce verbosity, and maybe even to increase accuracy, lowering the chance to mess up manual Send or Sync implementations: For instance, if you are implementing a synchronization primitive that closely mirrors another existing one in terms of its API – say, a custom Mutex re-implementation – then you could simply use SyncUnsafeCell<T>, together with a PhantomData<Mutex<T>> and then be confident that you got the right Send and Sync implementation to inherit from std’s Mutex. (This example also doesn’t fully work with SyncUnsafeCell<T> because unlike a Mutex, SyncUnsafeCell<T>: Sync requires T: Sync, something that makes it not a full static mut replacement, anyways, but that’s a discussion separate from this proposal which can support either of keeping or dropping this T: Sync restriction.)

My observation now is: There is another type in the standard library that comes with a conservative, but non necessary for soundness, implementation of !Send and !Sync: Raw pointers. And there, too, it’s very common one has to re-write Send and Sync implementations manually, even when the type very closely resembles another existing one.

Just to give one example: The type std::slice::Iter

pub struct Iter<'a, T: 'a> {
    ptr: NonNull<T>,
    end_or_len: *const T,
    _marker: PhantomData<&'a T>,
}

which even contains a PhantomData marker already, where it could simply inherit Send and Sync implementations from &'a T, needs to verbosely (and with a chance of typo / mistake), feature some manual unsafe impl Send and unsafe impl Sync implementations mirroring the ones from &'a T.

There likely countless other use cases of pointers used in ways where the pointer is “almost like a reference” in most ways including thread safety, and coming with an accompanying PhantomData (taking care of the lifetime). For example if one uses a pointer only because the data is not aligned properly, or to opt out of strict aliasing requirements as in aliasable::boxed::AliasableBox.

Instead of offering a full copy of UnsafeCell’s API called SyncUnsafeCell without the conservative !Sync restriction, and then addressing the above analogous problems for pointers, too, by mirroring their full API in hypothetical things like SyncConstPtr<T>, SyncMutPtr<T>, SyncNonNull<T>, we can introduce a single unified way (API) for lifting instance of types with a “lint-like !Sync and !Send implementation”, quite similar to the “lint-like” nature of !UnwindSafe being lifted by AssertUnwindSafe, I’m using the name AssertThreadSafe here.

Motivating examples or use cases

To take the above example of slice::Iter, that could then look like

pub struct Iter<'a, T: 'a> {
    ptr: AssertThreadSafe<NonNull<T>>,
    end_or_len: AssertThreadSafe<*const T>,
    _marker: PhantomData<&'a T>,
}

and needs no additional Send or Sync implementation anymore.

Existing SyncUnsafeCell<T> is replaced by AssertThreadSafe<UnsafeCell<T>>.

Solution sketch

A minimal / starting point of such an API looks as follows

use core::ptr::NonNull;
use core::cell::UnsafeCell;

#[derive(Debug, Copy, Clone)]
#[repr(transparent)]
pub struct AssertThreadSafe<T>(pub T);

unsafe impl<T> Send for AssertThreadSafe<*const T> {}
unsafe impl<T> Send for AssertThreadSafe<*mut T> {}
unsafe impl<T> Send for AssertThreadSafe<NonNull<T>> {}
// UnsafeCell has safe API that makes it usable by mutable reference or owned
// access; so the `T: Send` bound is still necessary, and `AssertThreadSafe<UnsafeCell<T>>`
// only lifts the restrictions on the `Sync` implementation.
unsafe impl<T: Send> Send for AssertThreadSafe<UnsafeCell<T>> {}


unsafe impl<T> Sync for AssertThreadSafe<*const T> {}
unsafe impl<T> Sync for AssertThreadSafe<*mut T> {}
unsafe impl<T> Sync for AssertThreadSafe<NonNull<T>> {}
unsafe impl<T> Sync for AssertThreadSafe<UnsafeCell<T>> {}

This particular approach also gives some cross-crate traits with a default impl, like `Send`, should not be specialized warnings, so I’m not sure whether or not this needs some additional (!Send + !Sync) marker field to reduce the Send and Sync implementations to just the listed ones.

Alternatively, there’s the option to make this (eventually) extensible, usable even for other crates that may feature some conservatively-!Sync types, too. The trait names are placeholders, but the idea would look like this:

use core::ptr::NonNull;
use core::cell::UnsafeCell;

/// Trait for types that (generally) don't implement Send,
/// but do so only as a precaution. The standard library types `*const T`
/// and `*mut T` are typical examples.
///
/// The types `*const T` and `*mut T` can soundly implement `Send`, even though they don't enforce
/// thread safety, because all API usage that could result in producing data races when the type
/// is sent between threads is already `unsafe`, anyway.
///
/// # Safety
/// This trait may only be implemented for types that can soundly implement `Send`.
/// This is also allowed to be implemented for types that *already do* implement `Send`.
pub unsafe trait SendIsNotUnsound {}

unsafe impl<T> SendIsNotUnsound for *const T {}
unsafe impl<T> SendIsNotUnsound for *mut T {}
unsafe impl<T> SendIsNotUnsound for NonNull<T> {}
// UnsafeCell has safe API that makes it usable by mutable reference or owned
// access; so the `T: Send` bound is still necessary, and `AssertThreadSafe<UnsafeCell<T>>`
// only lifts the restrictions on the `Sync` implementation.
unsafe impl<T: Send> SendIsNotUnsound for UnsafeCell<T> {}

/// Trait for types that (generally) don't implement Sync,
/// but do so only as a precaution. The standard library types `*const T`,
/// `*mut T`, and `UnsafeCell<T>` are typical examples.
///
/// The types `*const T`, `*mut T`, and `UnsafeCell<T>` can soundly implement `Sync`, even though they don't enforce
/// thread safety, because all API usage that could result in producing data races when the type
/// is shared immutably between threads is already `unsafe`, anyway.
///
/// # Safety
/// This trait may only be implemented for types that can soundly implement `Sync`.
/// This is also allowed to be implemented for types that *already do* implement `Sync`.
pub unsafe trait SyncIsNotUnsound {}

unsafe impl<T> SyncIsNotUnsound for *const T {}
unsafe impl<T> SyncIsNotUnsound for *mut T {}
unsafe impl<T> SyncIsNotUnsound for NonNull<T> {}
unsafe impl<T> SyncIsNotUnsound for UnsafeCell<T> {}

#[derive(Debug, Copy, Clone)]
#[repr(transparent)]
pub struct AssertThreadSafe<T>(pub T);
unsafe impl<T: SendIsNotUnsound> Send for AssertThreadSafe<T> {}
unsafe impl<T: SyncIsNotUnsound> Sync for AssertThreadSafe<T> {}

One could also support additional types from std. For example Option<NonNull<T>> comes to mind, because AssertThreadSafe<Option<NonNull<T>>> would be easier to work with perhaps than Option<AssertThreadSafe<NonNull<T>>>.

unsafe impl<T> SendIsNotUnsound for Option<NonNull<T>> {}
unsafe impl<T> SyncIsNotUnsound for Option<NonNull<T>> {}

To that end, this kind if impl could even be further generalized, at least in principle.

unsafe impl<T: SendIsNotUnsound> SendIsNotUnsound for Option<T> {}
unsafe impl<T: SyncIsNotUnsound> SyncIsNotUnsound for Option<T> {}

though it’s hard to say where to stop, and also this all can be happening subsequently, anyways.

Beyond the above #[derive(Debug, Copy, Clone)], AssertThreadSafe could implement other traits, too. In particular Default. Furthermore, Deref/DerefMut (pointing to the wrapped type, similar to how ManuallyDrop does it) would also be an option. The field of AssertThreadSafe could also be private instead of public, and/or named.

Alternatives

Removing or keeping SyncUnsafeCell as-is is the alternatives regarding UnsafeCell. For pointer types: As already mentioned in the “Problem Statement” section, one could also offer non-!Sync/!Send pointer types via a dedicated API, such as individual types SyncConstPtr<T>, SyncMutPtr<T>, SyncNonNull<T>, or a wrapper only for the pointer types. But if those would be introduced in the future, we definitely need to compare the option of multiple separate types, or a smaller-scope wrapper type, to the unified API from this ACP.

Or we can decide non-!Sync/!Send pointer types will never be wanted enough, suggest relying on the crate ecosystem for anyone that may find use in it, in which case, for UnsafeCell alone, a generalized wrapper like this may be overkill.

Links and related work

Existing discussions on SyncUnsafeCell should be relevant, I don’t have a full overview of the most important ones at hand; I can update this section if anything relevant/related gets suggested.

I’ve read rust-lang/rust#53639 (comment)
and the answer rust-lang/rust#53639 (comment)
where a hypothetical wrapper type came up being called UnsafeSync<UnsafeCell<T>> , and my proposal is partially inspired by that, even though I’m not sure what API (and most likely something different from this) was envisioned for “UnsafeSync” in that comment. Also the observation in the answer that

On names, two alternative possibilities to evaluate would be something like UnsafeSync<UnsafeCell<T>> / UnsafeCell<UnsafeSync<T>> and something with a new defaulted type parameter like UnsafeCell<T, Sync>.

For UnsafeSync separated from UnsafeCell to be useful it would have to be Sync unconditionally, but with a "dual-purpose" SyncUnsafeCell type we can have impl Sync for SyncUnsafeCell<T> where T: Sync with a bound. Now that I’ve typed this out I’m not sure which is preferable.

doesn’t apply here, because AssertThreadSafe<UnsafeCell<T>> can support requiring T: Sync for Self: Sync, just as it does (and has to do to stay sound) for Send. As mentioned above, I’m questioning whether that T: Sync is even wanted, but it works either way. (Just not both ways, in case we wanted both versions for some reason.1) (Here the idea to distinguish SyncUnsafeCell from a different AlwaysSyncCell is mentioned, for instance.)

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Footnotes

  1. Or maybe it can even work both ways, dropping the T: Sync requirement only once AssertThreadSafe<AssertThreadSafe<UnsafeCell<T>>> is used to really emphasize all thread-safety that can safely be dropped will be dropped.

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions