Skip to content

Place traits#3921

Open
davidv1992 wants to merge 8 commits intorust-lang:masterfrom
davidv1992:place-traits
Open

Place traits#3921
davidv1992 wants to merge 8 commits intorust-lang:masterfrom
davidv1992:place-traits

Conversation

@davidv1992
Copy link

@davidv1992 davidv1992 commented Feb 18, 2026

This introduces a Place trait, intended to make the special move behavior of Box possible 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:

  • Introduce separate place and place_mut to soundly deal with partially initialized content.
  • Rewrote safety discussion of trait.
  • Added section on Nadrieril's proposal within the custom refs work in the alternate approaches.
  • Renamed trait to DerefMove to keep name Place more available for the custom refs people.
  • Fixed a bunch of spelling mistakes.

davidv1992 and others added 3 commits February 18, 2026 22:30
This also reworks some of the other sections to further improve the
clarity of the message.
This proposal introduces a new unsafe trait `Place`:
```rust
unsafe trait Place: DerefMut {
fn place(&mut self) -> *mut Self::Target

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +79 to +81
- 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

@davidv1992 davidv1992 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

@clarfonthey clarfonthey Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 initialized SmartPointer<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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
}

Copy link

@197g 197g Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

@197g 197g Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ehuss ehuss added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Feb 19, 2026
}
```

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -0,0 +1,257 @@
- Feature Name: `place_traits`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

- 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +113 to +114
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment on lines +113 to +114
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines +292 to +293
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants