AssertThreadSafe
(name TBD) – a more general API for lifting conservative !Send
or !Sync
implementations #322
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
orSync
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 likeUnsafeCell<T, Sync>
.For
UnsafeSync
separated fromUnsafeCell
to be useful it would have to beSync
unconditionally, but with a "dual-purpose"SyncUnsafeCell
type we can haveimpl 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
-
Or maybe it can even work both ways, dropping the
T: Sync
requirement only onceAssertThreadSafe<AssertThreadSafe<UnsafeCell<T>>>
is used to really emphasize all thread-safety that can safely be dropped will be dropped. ↩