diff --git a/text/3918-maybe-dropped.md b/text/3918-maybe-dropped.md new file mode 100644 index 00000000000..7c0a1f7779e --- /dev/null +++ b/text/3918-maybe-dropped.md @@ -0,0 +1,335 @@ +- Feature Name: `maybe_dropped` +- Start Date: 2026-10-2 +- RFC PR: [rust-lang/rfcs#3918](https://github.com/rust-lang/rfcs/pull/3918) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +## Summary +[summary]: #summary + +A wrapper type for values that may have already been dropped. + +This is similar to `mem::MaybeUninit` in that it can represent invalid data when the inner value is not needed, but implies a different lifecycle for the contained value. + +Note this type does not provide safe access to `T` and does not run destructors when dropped + +## Motivation +[motivation]: #motivation + +Some types/systems do not always require the value be active for their entire duration. In these cases, an `Option` is often used. + +However, an `Option` here is correct, but the following issues arise: + +1. It is misleading here. + +While an `Option` works, its not truly an `Option`. For example, the type should not be instantiated as `None`, and the value should only be +`None` after it's usage is completed. + +Take this example. + +```rust +pub struct Fuse { + done: bool, + iter: I +} +``` + +The actual iterator is not needed after it yields `None` once. So in this case, We can replace the `I` with a `MaybeDropped`, which can optimize code by dropping the value early, since we track if it is done elsewhere. +(eg. by freeing allocator space, releasing a lock, etc.) + +2. it has no additional storage. + +`MaybeDropped` has the same memory layout as `ManuallyDrop`, which has the same memory layout as `T`. This can allow optimizations if +there is no need to store the drop state of `T`. + +Additionally, types/systems which would need `MaybeDropped` likely already have a means of tracking whether `T` is still needed or not +elsewhere. Storing it again (for example, using `Option`) is wasteful of memory. + +### Why this should be seperate from `MaybeUninit` + +`MaybeUninit` implies a value is initialized once and never goes back, having the following lifecycle. + +- MaybeUninit created (possibly uninit) +- MaybeUninit is initialized/confirmed to be already initialized. +- it stays initialized. + +`MaybeDropped` on the other hand implies the following lifecycle, where data is created in initialized state and later dropped. + +- MaybeDropped created initialized or in a `already dropped` state +- The MaybeDropped is used. (if not dropped) +- it is dropped. (if not dropped) +- it stays dropped (not written to) + +### Advantages over `T` + +For some cases, early drop is *required*; for example: + +- Locks +- Allocations (in performance critical code) +- Mechanisms requiring a value is dropped. + +`T` does not permit a dropped state, for this reason `MaybeDropped` is used. + +### Usage in FFI + +`MaybeDropped` is also FFI safe if `T` is FFI safe, this allows for an FFI safe transfer of data that has possibly already been dropped. +This can also allow for an FFI safe `Option`, with additional argmuents + +## Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +### Usage +Users would use `MaybeDropped` just like a `MaybeUninit`, in reverse. + +Take this example: + +```rust +struct OnceImage { + loader: MaybeDropped, + stored: Option // drop state of `loader` is stored here. +} +``` + +suppose `ImageLoader` has alot of open resources: + +- locks +- OS requests +- File Descriptors +- Services + +the `loader` is only used once, while `OnceImage` can be long-lived. It makes no sense here to keep the loader for the lifetime of `OnceImage`. +And `stored` tracks the drop-state of `loader`, so it would be redundant to store it twice. + +#### Where to use `MaybeDropped` rather than `MaybeUninit` + +The seperation is clear. + +`MaybeDropped`: Values which are initialized, and the dropped later. + +`MaybeUninit`: Values which may not be initialized yet. + +#### Usage in FFI + +`MaybeDropped` can be used with `bool` for FFI safe `Option`s + +```c +// c code + +// extern definition... + +void do_some_work() { + // ... + rust_work(possibly_dropped, is_dropped); +} +``` +And then, in rust +```rust +// imports... + +#[unsafe(no_mangle)] +extern "C" fn rust_work(dropped: MaybeDropped, is_dropped: bool) { + let as_option = if is_dropped { + None + } else { + Some(dropped.assume_alive()) + } +} +``` + +### Undefined Behaviour in `MaybeDropped` + +The following is *__not__* undefined behaviour: + +- Creating a `MaybeDropped` that is already dropped. (`MaybeDropped::dropped`) +- Dropping a `MaybeDropped` (so long it is the first time) +- Obtaining raw pointers to the inner memory. + +the following *__is__* undefined behaviour: + +- Dropping a `MaybeDropped` twice. +- Creating an uninitialized `MaybeDropped` (where `T` cannot be unintialized) +- any access to `T` after drop. +- Obtaining references to `T` after drop. +- Writing a value to the `MaybeDropped` after it has already been dropped. + +#### Examples +```rust +let dropped = MaybeDropped::::dropped(10); // creating a logically dropped `MaybeDrop` without a value to drop would be the same as making it uninitialized, which isn't always valid. + +// this is not ok +// let uninit = MaybeUninit::>::uninit().assume_init(); +// this is ok (ZST) +let uninit_unit = MaybeUninit::>::unint().assume_init(); + +// this is not ok +// let reference = dropped.assume_alive(); + +// this is ok +let ptr = dropped.as_ptr(); + +// this fails to compile (not mutable), but is otherwise ok UB-wise +// let mut_ptr = dropped.as_mut_ptr(); + +let to_be_dropped = MaybeDropped::new(10); + +unsafe { + // this is ok + to_be_dropped.assume_alive_drop(); + // this is not + // to_be_dropped.assume_alive_drop(); +}; + +// this is not ok +// *ptr.cast_mut() = 42; +// this is not ok +// *ptr +``` + +## Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +This is the definition + +``` +#[repr(transparent)] +pub struct MaybeDropped { + value: MaybeUninit> +} +``` + +it does *not* store any drop-state information. + +the public API will expose the following: + +``` +impl MaybeDropped { + pub unsafe fn assume_alive_drop(self); + pub unsafe fn assume_alive(self) -> T; + pub unsafe fn assume_alive_ref(&self) -> &T; + pub unsafe fn assume_alive_mut(&mut self) -> &mut Self; + pub unsafe fn as_ptr(&self) -> *const T; + pub unsafe fn as_mut_ptr(&mut self) -> *mut T; +} +``` + +### Dropping + +`MaybeDropped` will not drop on its own. + +it must be dropped with `.assume_alive_drop()` + +After drop: + +- the `MaybeDropped`'s inner value is logically dropped. +- all access is undefined behaviour (both reads and writes) + +### Safety + +All accesses to the inner value (outside of raw pointers) is `unsafe`, including references. + +The inner value must be dropped *at most once*, not dropping is permitted but is considered a leak. + +### Interaction with Traits +`MaybeDropped` will not implement any traits which access the inner values (or, more generally, have any safe methods that access the wrapped value) + +Trait implementations such as Send and Sync follow the same rules as `MaybeUninit` and are conditional on the corresponding traits of `T`. + +### Relation with `MaybeUninit` + +MaybeDropped is closely related to mem::MaybeUninit, but represents a distinct lifecycle. While MaybeUninit models memory that may not yet have been initialized, MaybeDropped models memory that was once initialized but may have been destroyed. + +This distinction allows low-level code to express post-drop states explicitly without additional storage or ad-hoc flags. + +## Drawbacks +[drawbacks]: #drawbacks + +- It can already be done with `Option` +- It can already be done with `MaybeUninit` +- It is unnecessarily unsafe (in most cases) +- error prone + - if the drop-state tracking is not clear, users may accedeintly drop the wrapped value twice. + +## Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +- `Option` + - Safer, + - Less error prone and + - tracks drop state + + Option wasn't chosen because: + + - it is not FFI safe + - it is not optimized for size + +- `MaybeUninit` + - Already in code + - supported by external crates. + +`MaybeUninit` wasn't chosen because: + +- It represents a completely different lifecycle +- its methods dont align with the requirements of `MaybeDropped` + +### Impact of not doing this + +there isnt much of an impact on regular code, the performance increase is negligble in regular code. + +## Prior art +[prior-art]: #prior-art + +`MaybeUninit` is already a way, inside of Rust, to represent data that may be invalid. + +### mem::MaybeUninit + +`mem::MaybeUninit` is the closest existing abstraction to `MaybeDropped`. It represents memory that may not yet be initialized as a valid T and provides no safe access to the underlying value. + +While `MaybeUninit` and `MaybeDropped` share similar safety properties, they model different lifecycles. `MaybeUninit` is intended for values that may be initialized at a later point, whereas `MaybeDropped` models values that were once initialized but whose destructor may already have been executed. Conflating these lifecycles would obscure intent and make code harder to reason about. + +### mem::ManuallyDrop + +`mem::ManuallyDrop` prevents Rust from automatically running the destructor of T while preserving all of T’s invariants. Safe access to the underlying value remains available at all times. + +This makes `ManuallyDrop` unsuitable for representing post-drop states, as it assumes the value remains valid even after the destructor is manually invoked. In contrast, `MaybeDropped` explicitly removes any guarantee that the underlying value is valid. + +### Option + +`Option` is commonly used to model early destruction by replacing a value with `None` once it is no longer needed. This approach is safe and idiomatic in many cases. + +However, `Option` introduces additional storage to track the presence of the value and semantically models optional ownership rather than post-drop invalidation. In cases where the drop state is already tracked elsewhere or where layout constraints matter, `Option` is less suitable than `MaybeDropped`. + +### Raw pointers and `drop_in_place` + +Low-level Rust code can model post-drop states using raw pointers combined with ptr::drop_in_place. This approach is flexible but error-prone, as the drop state is tracked implicitly and must be enforced by convention. + +MaybeDropped encapsulates this pattern in a dedicated abstraction, making the intent explicit and reducing the likelihood of accidental misuse. + +Additionally, also (usually) force borrowing rather than ownership (outside of `Box`) + +### Patterns in existing Rust code + +Several low-level Rust abstractions internally rely on patterns similar to `MaybeDropped`, including intrusive data structures, iterator adaptors, and runtime systems that perform early destruction for performance or correctness reasons. These implementations typically use `Option`, `ManuallyDrop`, or raw pointers to represent post-drop states. + +`MaybeDropped` provides a more direct and expressive way to model these patterns without additional storage or loss of clarity. + +### Other languages and systems + +In systems programming contexts outside of Rust, it is common to distinguish between allocation, initialization, and destruction explicitly. Languages and runtimes such as C and C++ permit objects to be destroyed while their storage remains allocated, with correctness enforced by convention. + +`MaybeDropped` enables similar low-level control within Rust while preserving its safety model by confining such patterns to unsafe code. + +## Unresolved questions +[unresolved-questions]: #unresolved-questions + +> - What parts of the design do you expect to resolve through the RFC process before this gets merged? + +> - What parts of the design do you expect to resolve through the implementation of this feature before stabilization? + - Trait implementations + - For example, `Unpin`, `Send`, `Sync`? + +## Future possibilities +[future-possibilities]: #future-possibilities + +### More general Lifetime Types + +Currently, we support `uninit` -> `init` (and with this RFC, `init` -> `dropped`), but what if we want to add an abstraction layer over lifetimes as a whole? +Well, thats out of scope for this RFC, but it is a thought.