Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC]
core::marker::Freeze
in bounds #3633base: master
Are you sure you want to change the base?
[RFC]
core::marker::Freeze
in bounds #3633Changes from 20 commits
af62890
106bc46
902b79d
126f8f0
94ef594
34b9775
3a104b1
6e15d06
c5b3fe8
2b4f996
33ffcc6
b339b0d
30a03fc
c8fa805
492a594
c01e96c
405b322
14abf77
c1fedd5
04e39d4
c7eab79
File filter
Filter by extension
Conversations
Jump to
There are no files selected for viewing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is a plausible possibility. If you never use interior mutability, just don't use a type with UnsafeCell. What is the possible use-case for having an UnsafeCell that you will definitely never use for interior mutability? (That would be the only situation where
unsafe impl Freeze
would be sound.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've considered wanting this for zerocopy. The use case would be to add a wrapper type
Frozen<T>
(joking - no clue what we'd name it) that isFreeze
even ifT
isn't. IIUC this would requireFreeze
to be a runtime property rather than a type property (ie, interior mutation never happens), and would require being able to writeunsafe impl<T> Freeze for Frozen<T>
.It's not something we've put a ton of thought into, so I wouldn't necessarily advocate for it being supported right now, but IMO it's at least worth keeping this listed as a potential future direction and trying to keep the possibility open of moving to that future w/ whatever design we choose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Speculation: Perhaps a data structure whose contents are computed using interior mutability, then put in a wrapper struct which disables all mutation operations and implements
Freeze
. The reason to do this using a wrapper rather than transmuting to a non-interior-mutable type would be to avoid layout mismatches due toUnsafeCell<T>
not having niches thatT
might have.This is only relevant if such a type also wants to support the functionality enabled by implementing
Freeze
, of course.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would that type be good for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's been a while since we considered this design, so the details are hazy in my memory, but roughly we wanted to be able to validate that a
&[u8]
contained the bytes of a bit-valid&T
. We wanted to encode in the type system that we'd already validated size and alignment, so we wanted a type that represented "this has the size and alignment ofT
and all of its bytes are initialized, but might not be a bit-validT
." To do this, we experimented with aMaybeValid<T>
wrapper.When you're only considering by-value operations, you can just do
#[repr(transparent)] struct MaybeValid<T>(MaybeUninit<T>);
(MaybeUninit
itself isn't good enough because it doesn't promise that its bytes are initialized - the newtype lets you add internal invariants). The problem is that this doesn't work by reference - if you're usingFreeze
as a bound to prove that&[u8] -> &MaybeValid<T>
is sound, it won't work ifT: !Freeze
.As I said, we didn't end up going that route - instead of constructing a
&MaybeValid<T>
, we construct (roughly) aNonNull<T>
(okay, actually aPtr
) and just do the operations manually. The code in question starts at this API if you're curious.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we want to be able to use
MaybeValid<T>
with APIs that are safe and use trait bounds (likeFreeze
) to prove their soundness internally. One of our golden rules for developing zerocopy is to be extremely anal about internal abstraction boundaries [1]. While we could just do what you're suggesting usingunsafe
(namely, useunsafe
to directly do the&[u8] -> &MaybeValid<T>
cast), we have existing internal APIs such as this one [2] that permit this conversion safely (ie, the method itself is safe to call), using trait bounds to enforce soundness.In general, we've found that, as we teach our APIs to handle more and more cases, it quickly becomes very unwieldy to program "directly" using one big
unsafe
block (or a sequence ofunsafe
blocks) in each API. Decomposition using abstractions likePtr
has been crucial to us being confident that the code we're writing is actually sound.[1] See "Abstraction boundaries and unsafe code" in our contributing guidelines
[2] This API is a bit convoluted to follow, but basically the
AliasingSafe
bound bottoms out atImmutable
, which is ourFreeze
polyfillThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid it is not clear to me how
Frozen
helps build up internal abstraction barriers, and your existing codebase is way too big for me to be able to extract that kind of information from it with reasonable effort.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TLDR: We can only make this code compile if
MaybeValid<T>: Freeze
despiteT: ?Freeze
. I'm arguing that, in order to support this use case, we should design the RFC to be forwards-compatible with permitting unsafeimpl<T> Freeze for MaybeValid<T>
even if we don't support it out of the gate.Here's a minification of the sort of code I'm describing. It has three components that, in reality, would live in unrelated parts of our codebase (they wouldn't be right next to each other like they are here, so we'd want to be able to reason about their behavior orthogonally):
MaybeValid<T>
try_cast_into<T>(bytes: &[u8]) -> Option<&MaybeValid<T>> where MaybeValid<T>: Freeze
FromBytes
(which needs no validity check) and byTryFromBytes
(which performs a validity check after casting)TryFromBytes
, with:try_ref_from(bytes: &[u8]) -> Option<&Self> where Self: Freeze
try_mut_from(bytes: &mut [u8]) -> Option<&mut Self>
Notice the
MaybeValid<T>: Freeze
bound ontry_cast_into
(and the safety comments inside that function). That bound permits us to make the function safe to call (without that bound, we'd need a safety precondition about whether interior mutation ever happens using the argument/returned references).try_ref_from
can satisfy that bound because it already requiresSelf: Freeze
, which means thatMaybeValid<Self>: Freeze
. However,try_mut_from
does not requireSelf: Freeze
. Today, the linked code fails to compile thanks to line 114 intry_mut_from
:What we'd like to do is be able to guarantee that interior mutation will never be exercised via
&MaybeValid<T>
, and thus be able to writeunsafe impl<T> Freeze for MaybeValid<T>
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What goes wrong if it's not Freeze? There can't even be a proof obligation attached to this since you want it to be trivially true. I think all you'd lose is some optimization potential.
If
MaybeValid
was alwaysFreeze
, you'd have to extend the safety comments onderef_unchecked
to say "also you must never ever mutate via this shared reference, even ifT
is e.g.Cell<T>
". Anunsafe impl Freeze for MyType
type has the responsibility of ensuring that its interior is never mutated through any pointer derived from an&MyType
. That's whatFreeze
means: no mutation through shared references to this type, including all pointers ever derived from any such shared reference.So henceforth I will assume that
MaybeValid
has added this requirement to its public API. You can do that without havingunsafe impl Freeze
, it's just a safety invariant thing. Now you can drop theFreeze
side-condition ontry_cast_into
, since no interior mutability is exposed by exposing aMabeValid
. And then your problem goes away, no?This requires
try_cast_into
to rely on a property ofMaybeValid
, but sinceMaybeValid
appears intry_cast_into
's signature, there's no new coupling here -- you already depend on the fact that MaybeValid can be soundly constructed for invalid data.So I still don't see a motivation for
impl Freeze
here, except if you somehow want more optimizations onMaybeValid
.tl;dr you can already do that, just put these requirements into the public API of
MaybeValid
. And you'd have to put these requirements in the public API ofMaybeValid
even if we allowed you tounsafe impl Freeze
, so you'd not gain anything from that as far as I can see.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point, and I think that's a compelling argument. I can imagine there being cases where you'd want to pass
MaybeValid
to a generic API that acceptedT: Freeze
, in which case you'd still need this, but I don't have a concrete example of this off the top of my head.I'd still advocate for keeping this listed as a future possibility since doing so doesn't have any effect on the current proposal (ie, the current proposal is already forwards-compatible with this). Obviously we'd need to adjudicate it much more thoroughly if it was ever proposed to actually go through with permitting
unsafe impl Freeze
, and I assume you'd push back if that ever happened just as you are now, but I think it's at least worth mentioning that it's something that we've considered and that there might be some desire for.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth mentioning that these are not the same, and so one may not be a straightforward alternative for the other.
Including a
PhantomNotFreeze
field removes theFreeze
impl for the type.impl !Freeze for T
expresses a guarantee that there will never be aFreeze
impl for the type.The actually-equivalent language feature would have to be something like
impl ?Freeze for T
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right you are, if we stick to the "
impl !Trait for X
means thatimpl Trait for X
requires a major bump" interpretation (which is something I'd really enjoy to have), they do indeed yield different results