So @RalfJung wrote up a pretty interesting blog post about the recent bug fix to MutexGuard
:
In there, he draws some lessons about auto traits and in particular their relationship to "unsafe types" that I think are worth digging into some more:
Again, I think the solution here is that types like
MutexGuard<T>
should not automatically implement unsafe auto traits. What do I mean by “types likeMutexGuard<T>
”? The point that makesMutexGuard<T>
special here is that there is an invariant attached with this type that all instances ofMutexGuard<T>
satisfy. [..] My proposal is to tell the compiler that this is the case.
In general, I strongly agree. The intention of the auto traits design was things would only be "automatically" Send
and Sync
when that was safe, obviously, but as this blog post demonstrates, the current design can't achieve that.
What went wrong, specifically, is that MutexGuard
contains an &Mutex
, which is considered (and righly so) to be a Sync
type. But that is true only for external observers, those who are limited to Mutex
's public API. MutexGuard
has privileged access to the internals of Mutex
, and hence it is not Sync
. This distinction (between the public/private API surface of a type) was not considered when designing auto traits.
Ralf's proposal is to add unsafe fields, which is something that I am in favor of in general, and then leverage that around auto traits. The idea would be that any type with unsafe fields cannot implement any auto traits. I have mixed feelings about this. On the one hand, it makes a lot of sense, and may be necessary, but on the other hand it doesn't feel sufficient for two reasons:
- One can easily neglect to call a field unsafe! The original design was intended to be robust against this sort of mistake. It basically said "we will consider a type "safe" if it only has access to safe things". (And then relies on the fact that one cannot engineer a data race (as opposed to a more general race, which we don't claim to prevent) from data-race free components that are composed together.)
- There may be auto traits for which this is too strong. My main example would be a "deep freeze" trait that scans for
UnsafeCell
-- I forget just why I had wanted this recently, but I remember there was an example, and it's something that even unsafe code ought not to be violating (technically this is something that the Unsafe Code Guidelines will settle, but I think there is and has always been universal agreement on this point).- That said, this depends. Maybe we would say that an
unsafe
field can be mutated, even when shared.
- That said, this depends. Maybe we would say that an
So, a related thought that I had is that perhaps auto traits ought not to recurse solely on the fields of the type in question, but also on the accessible fields of the types they contain. In this case, MutexGuard
would be !Sync
by default, because it would have access to the UnsafeCell
contained in Mutex
.
One final thought is that we may very well have to tinker with the conditions under which one can add an auto trait
impl in the first place. I've been reading into recent work on mixing inductive and co-inductive logic programming, and it proposes a basic rule that recursing between inductive (normal traits) and co-inductive (auto traits) predicates is inherently contradictory. We saw this ourselves in the various unsoundnesses that arose from mixing auto traits and super traits, and I suspect it may imply some kind of limits on the kinds of where clauses one can use in an impl Send for T
impl.