Conversation
This also reworks some of the other sections to further improve the clarity of the message.
text/3921-place-traits.md
Outdated
| This proposal introduces a new unsafe trait `Place`: | ||
| ```rust | ||
| unsafe trait Place: DerefMut { | ||
| fn place(&mut self) -> *mut Self::Target |
There was a problem hiding this comment.
So, it's a little strange why this method is necessary when it appears that any implementation would just put a call to deref_mut here: the mutable reference would coerce to a pointer, and casting to a pointer obviously removes all reference to lifetimes and lets the compiler do whatever it wants with it.
There was a problem hiding this comment.
Best I understand the semantics of mutable references, it would be unsound to have one to a value that is uninitialized. And the semantics of moving in and out of the Place would involve calling this function in cases where *mut Self::Target would then have to point to something that is uninitialized.
There was a problem hiding this comment.
Hmm, but wouldn't that mean that you have to take *mut self, not &mut self? What you're saying makes sense, but it's unclear how the pointer could be initialized when this function is called. Effectively, the mutable borrow ends after the function returns, so, it's totally valid for something that was originally &mut Self::Target to become initialized as long as the original lifetime has ended and it's only used as a pointer at that point.
There was a problem hiding this comment.
Ok, I answered more completely in the other thread, see #3921 (comment). It basically comes down to that the argument to place is the Box-Like, which is (and should be, otherwise use of this trait is going to be a nightmare) still initialized, even though its Contents might not be. The non-initialized status of the Contents rules out the use of references for that, hence the extra function and the pointer.
text/3921-place-traits.md
Outdated
| - Safe code shall not modify the initialization status of the contents. | ||
| - Unsafe code shall preserve the initialization status of the contents between two derefences of teh type's values. | ||
| - Values of the place type for which the content is uninitialized shall not be able to be created in safe code. |
There was a problem hiding this comment.
This is… a very confusing set of safety requirements considering how they're effectively already guaranteed by the intrinsic safety requirements of the language: you can't de-initialize the contents of something with a mutable reference, and unsafe code is expected to uphold this regardless of whether the reference is converted into a pointer or not.
There was a problem hiding this comment.
I agree that the formulation here is less than ideal, however there is something real that is asked here, that is unfortunately not guaranteed by the borrow checker alone. For example, if somebody defines the type InplaceBox as follows:
struct InplaceBox<T>(MaybeUninit<T>);
impl<T> Deref for InplaceBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { self.0.assume_init_ref() }
}
}
impl<T> DerefMut for InplaceBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { self.0.assume_init_mut() }
}
}
unsafe impl<T> Place for InplaceBox<T> {
type NewArg = ();
fn place(&mut self) -> *mut Self::Target {
self.0.as_mut_ptr()
}
}Then as part of the unsafe contract for place they promise to for example not to do stuff like
#[derive(Debug, Clone, Copy)]
enum NonZero { One = 1, Two = 2 }
pub fn foo() {
let ipb = InplaceBox(MaybeUninit::init(NonZero::One))
println!("{:?}", *ipb); // Still OK, ipb contains something
ipb.0 = MaybeUninit:zeroed(); // Should be forbidden, because borrow checker should still think following is ok.
println!("{:?}", *ipb); // UB happens here now, since even though the borrow checker thinks this is fine, it is not, because of the line above.
}Forbidding these sorts of shenanigans is what I am trying to capture with these requirements. If you have suggestions for how to better formulate that I'd love those.
There was a problem hiding this comment.
I guess that, as I also pointed out in the other comment thread, it's not really clear how this UB is possible based upon what the API is trying to achieve. Yeah, that example is obviously UB, but it's unclear how the Place implementation needs these guarantees in order to work.
Like, no matter what, safe code should not be allowed to create an invalid value, and if you're specifically moving a value out of a place, you kind of necessitate that the container be dropped in some way as part of this process, so, further references will not be possible. For Box, this happens via deallocating the pointer, but it also requires running the drop glue for the field beforehand, and it needs to know whether that should have to occur or not.
There was a problem hiding this comment.
Possibly the trait could remain unstable itself while having a pointer-field based compiler generated implementation path, similar to CoerceUnsize and its CoercePointee's relationship. Consider that we may not need to name the trait explicitly in many use cases, only the compiler must be aware of the trait implementation for variables (that it then has a borrow tree for). This would solve two other issues:
- The macro implemented in the compiler can initially verify a very narrow subset of types to qualify, for which the semantics we want are quite clear. The overlap seems quite large, too. For smart pointers with one pointer-like field from which to derives its behavior seems reasonably well-defined. The contentious method / MIR would be compiler generated, too, which means the meat of the unresolved question is punted to future relaxations of the macro or arbitrary user-defined derives.
- We could define that
SmartPointer<MaybeUninit<T>>is allowed to initializedSmartPointer<T>by filling the place (through compiler defined init-sequence support) and transmutation. This would be based on very similar layout requirements than placement but again the macro could annotate the type to guarantee them.
There was a problem hiding this comment.
We could define that SmartPointer<MaybeUninit> is allowed to initialized SmartPointer by filling the place (through compiler defined init-sequence support) and transmutation. This would be based on very similar layout requirements than placement but again the macro could annotate the type to guarantee them.
You can currently move out of a Box<T> and then back into it again without moving the Box<T> itself. Transmuting requires moving it.
fn main() {
let mut a = Box::new(Box::new(vec![0]));
drop(**a); // Deinitialize the inner box without moving it.
**a = vec![]; // Reinitialize the inner box without moving it.
}There was a problem hiding this comment.
I do disagree. You'd be writing uninit bytes into a (stack-)allocation of type NonZero (it is declared let mut storage = NonZero::one); that is UB with no relation to any new magic. How should the trait help justify the code to operate within the defined set of semantics? In comparison if we were not to expose that customization point (yet) we only need to justify that some code that the compiler can internally verify, with full access to internal type and layout info, follows some operational semantics (hopefully modifying operational semantics as little as necessary). It's the public trait that introduced a need of articulating operational impl-requirements in the first place.
As for how I think of it, the major novelty of the place API is manipulating information about places, not types. Using traits for more than markers is odd to me since those would be type-properties and algorithms—neither of which express information about places. That invites an unrelated layer of complexity around a new IR operation. The unaddressed question of what happens internally (expressed in MIR maybe) seems more pressing—then we can check how the preconditions for those can be encoded into traits / other interfaces.
There was a problem hiding this comment.
Apologies, perhaps having the storage on the stack was an unwise choice on my part. Where would you articulate the unsoundness being in this example:
#[derive(Debug, Clone, Copy)]
enum NonZero { One = 1, Two = 2 }
pub fn foo() {
let mut storage = unsafe { libc::malloc(std::mem::size::<T>()) }
let ipb = MyKindOfBox{ alloc: storage }
println!("{:?}", *ipb); // Still OK, ipb contains something
unsafe { std::ptr::copy(MaybeUninit::zeroed().as_ptr(), ipb.alloc.as_mut_ptr(), 1); } // Should be forbidden even though the copy itself is sound, because borrow checker should still think following is ok.
println!("{:?}", *ipb); // UB happens here now, since even though the borrow checker thinks this is fine, it is not, because of the line above.
}
Assuming that the particular malloc is infallible and does not violate alignment.
To me, this is no different than the previous example, but this time there is no requirement on the storage that it be a valid value, so there are still coming requirements from the fact that MyKindOfBox is Boxlike, and therefore does want specific things from its (exposed!) backing storage. I dont think we can avoid specifying those assumptions just because we can't explicitly implement the trait that makes it a Boxlike.
There was a problem hiding this comment.
I'm still not entirely sure what you're asking. The trait moves responsibility for the place to the function body; you'd also required that all values have had their places initialized (following the section at 'requires are met …') and from that point on the storage has to be valid for NonZero according to that (your latest comment example is still missing an initialization of storage, let's assume it happened).
The RFC is missing an operational semantics of how the pointer is invalidated for other writes in a way that the compiler can actually do something with the information but the moment you implement the trait and create a value of it you opt-in to not allow the write. How to disallow it seems to be the responsibility of the author of the type, here by privacy and not doing a raw write itself.
You were asking how the derive makes this clearer. Well first of all we can then actually talk about operation semantics. By identifying the pointer as a field instead of a method we have an actual pointer value to manipulate (and invalidate), not an ephemeral derived copy. Secondly, we can say that construction does something with that value when it is assigned as a field (which makes construction come with a precondition, a valid pointer, so it must be usually unsafe; but that could also mean we require such types to be publicly only creatable by verified method as part of the trait impl / an unsafe(derive())).
When you justify ptr::copy you need to justify it for the argument value. Not just how some original pointer was created (via malloc) but also everything that happened to the value in between. That is what provenance means after all, the pointer validity depends on how it got here. Well in this case the pointer value would have got there by being copied from value that that was assigned as a field of MyKindOfBox from a libc::malloc. And in that assignment it could have got invalidated for non-NonZero writes. Through the macro we would be able to talk about the underlying raw pointer value as part of the Place-value very concretely.
Edit: having operational semantics of struct-expressions invalidate the pointer is just an example to demonstrate the difference between the two approaches; I think it's not entirely what we'd want. Alternatively, and stronger, we might even require the pointer validity as part of the representational invariants of the macro-annotated type. This would allow dereferentiablity assumptions recursively through other references / … (with the decision if we want recursive representation invariants still being outstanding). Or something in between. As long as we have a well-defined notion of the raw pointer / pointee place inside the custom place there should be ample options.
There was a problem hiding this comment.
Ok, I see what you are getting at, and focussing more on the operational semantics of the contents is likely a good direction for making the explanation in this area clearer. I have some time for rewrites on friday and will try to rework this section to clarify that.
I am not yet convinced that a derive macro yields enough value to make up for the extra implementation complexity and reduced utility, but that is probably better evaluated once I have a new version of the requirements language here.
There was a problem hiding this comment.
Ok, I have done the promised rewrite, and I at least feel much happier with the new section on requirements and provided guarantees by the compiler in the rfc. It can be found on lines 85-102, and I suggest we continue the discussion in a review comment on those lines.
text/3921-place-traits.md
Outdated
| } | ||
| ``` | ||
|
|
||
| When implementing this trait, the type itself effectively transfers some of the responsibilities for managing the value behind the pointer returned by `Place::place`, also called the content, to the compiler. In particular, the type itself should no longer count on the ccontent being properly initialized and dropable when its `Drop` implementation or `Place::place` implementation is called. However, the compiler still guarantees that, as long as the type implementing the place is always created with a value in it, and that value is never removed through a different mechanism than dereferencing the type, all other calls to member functions can assume the value to be implemented. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Just to clarify for those following along, yes the intent is that the compiler will always emit the relevant drop code for the contents, and the Place never has to drop the contents.
text/3921-place-traits.md
Outdated
| @@ -0,0 +1,257 @@ | |||
| - Feature Name: `place_traits` | |||
There was a problem hiding this comment.
Is this RFC under the umbrella of https://rust-lang.github.io/rust-project-goals/2026/in-place-init.html? Was this discussed on https://rust-lang.zulipchat.com/#narrow/channel/528918-t-lang.2Fin-place-init?
From my understanding, the Rust project is still in the evaluation stage for in-place initialization and is still working on their "design space RFC". I would recommend checking with the developers already in this space.
There was a problem hiding this comment.
The in-place-init project is aware of this. Having looked in detail at what in-place-init is discussing I would classify this as orthogonal to the concerns what they are discussing.
It does overlap somewhat with the custom-refs plans, however I believe this can be a good initial step that can provide results far faster than the complex field projection based designs they are producing.
text/3921-place-traits.md
Outdated
| - The pointer returned by `place` should be safe to mutate through, and should be live | ||
| for the lifetime of the mutable reference to `self` passed to `Place::place`. | ||
| - On consecutive calls to `Place::place`, the status of whether the content is initialized should not be changed. | ||
| - Drop must not drop the contents, only the storage for it. |
There was a problem hiding this comment.
If Place::place was not called at all, and Drop doesn't drop the contents, wouldn't this cause a memory leak, since nobody dropped the contents?
There was a problem hiding this comment.
Here's an idea: Have an additional trait:
unsafe trait DropShell: Drop + Place {
fn drop_shell(&mut self);
}If a type implements DropShell, then drop_shell will be called instead of Drop::drop in order to drop the thing while the place inside has already been consumed.
Implementing DropShell has to follow the same restrictions as Drop: You may only implement DropShell with the same generic bounds as the struct definition.
There was a problem hiding this comment.
So how this currently works for Box is that the compiler is always responsible for dropping the contents. The suggestion here is to use the same contract for places.
There was a problem hiding this comment.
Fwiw we have this trait (we call it DropHusk) in the Field Projections proposal. You may find the "Moving values out" of my blog post interesting
During this process I also sharpened the thinking on abort behavior, resulting in some changes to that unresolved question as well.
| As a consequence, `Pin<Foo>` does not automatically satisfy all the requirements of Pin | ||
| when Foo implements place. This will need to be verified on an implementation by |
There was a problem hiding this comment.
| As a consequence, `Pin<Foo>` does not automatically satisfy all the requirements of Pin | |
| when Foo implements place. This will need to be verified on an implementation by | |
| As a consequence, `Pin<Foo>` does not automatically satisfy all the requirements of `Pin` | |
| when Foo implements `DerefMove`. This will need to be verified on an implementation by |
| As a consequence, `Pin<Foo>` does not automatically satisfy all the requirements of Pin | ||
| when Foo implements place. This will need to be verified on an implementation by |
There was a problem hiding this comment.
This will need to be verified on an implementation by implementation basis.
What does “this” mean here? What does the verification consist of? What actions does a library author need to take or avoid taking to make sure that they do not have broken pinning?
| Should the trait become stabilized, it may become interesting to implement non-copying | ||
| variants of the various pop functions on containers within the standard library. Such |
There was a problem hiding this comment.
What would the signatures of these non-copying variants be like? I'm guessing you mean something looking like:
impl<T> Vec<T> {
fn pop_in_place<'a>(&'a mut self) -> Option<impl DerefMove<Target = T> + use<'a, T>> {
self.set_len(self.len().checked_sub(1)?);
Some(OwnInUninit(&mut self.spare_capacity_mut()[0]))
}
}
// Safety: self.0 must point to a valid `T` when constructed
struct OwnInUninit<'a, T>(&'a mut MaybeUninit<T>);
unsafe impl<T> DerefMove for OwnInUninit<'_, T> {
/* type Target = T */
fn place(&self) -> *const Self::Target {
self.0.assume_init_ref()
}
fn place_mut(&mut self) -> *mut Self::Target {
self.0.assume_init_mut()
}
}Presuming I’m correct about this sketch, something interesting I notice is that OwnInUninit doesn’t have any obligatory relationship to Vec (every container with a pop_in_place() could use it), and that it looks an awful lot like an &move reference. The prior art suggests that &move references would be complex. So, is this RFC successfully avoiding the complexity somehow, or are there features &move would naturally have that OwnInUninit cannot implement?
There was a problem hiding this comment.
The main difference from this to #1646 seems to me to be that latter is an operation to be written at any point whereas this work through the initialization itself. By not giving an independent choice of where the initialization of the smart pointer itself relative to the pointer to the interior is valid it can avoid some of the drop-order discussion. You could never run into the example in Impure DerefMove for instance.
You also can't 'move' from a Box into an OwnInUninit with this proposal—there is no new operation to unset the liveness-state of an existing place where the construction of&own by move-borrow is attempting exactly that (take liveness from a path and transfer it to the &own). Vec works regardless because the init state of its content does not live in the drop-checker but instead is a dynamic property of the value which can be split off and transferred without any special operations. (As for generalization, you might write a macro that consumes a Box<_> by value into a Box<MaybeUninit<_>> and OwnInUninit<_>; or one that shadows a ManuallyDrop with an OwnInUninit to it. But there must remain this intermediate sequence point in the splitting where the pointee is, judging by the live drop-glue, forgotten.) This moves complexity to the type authors as you'd need to consider transfer into an OwnInUninit for more types individually but it does avoid opsem complexity.
This introduces a
Placetrait, intended to make the special move behavior ofBoxpossible for other types.Important
When responding to RFCs, try to use inline review comments (it is possible to leave an inline review comment for the entire file at the top) instead of direct comments for normal comments and keep normal comments for procedural matters like starting FCPs.
This keeps the discussion more organized.
Rendered
Update 6th of March 2026 13:21 UTC: