From 3d90c43a0cb1bc0cf93a5421b1cd5003d8d78909 Mon Sep 17 00:00:00 2001 From: IonicMC Date: Tue, 10 Feb 2026 20:54:15 +0200 Subject: [PATCH 1/4] Create 0000-maybe-dropped.md Start RFC --- text/0000-maybe-dropped.md | 329 +++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 text/0000-maybe-dropped.md diff --git a/text/0000-maybe-dropped.md b/text/0000-maybe-dropped.md new file mode 100644 index 00000000000..2ce5653970a --- /dev/null +++ b/text/0000-maybe-dropped.md @@ -0,0 +1,329 @@ +- Feature Name: `maybe_dropped` +- Start Date: 2026-10-2 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) (todo) +- 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, from the standard library (simplified) + +```rust +pub struct Fuse { + iter: I +} +``` + +The actual iterator is not needed after it yields `None` once, so in this case (not in the standard library, +however, due to a possible breaking change), We can replace the `I` with a `MaybeDropped`, which can optimize code by dropping the value early +(eg. by freeing allocator space, releasing a lock, etc.) + +But you may realize this is exactly the same as using an `Option` (Which `Fuse` does). However, the following point contradicts this. + +2. it is more optimized (for size) + +`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() { + // ... + if (!dropped) { + rust_work(possibly_dropped, false) + } else { + rust_work(possibly_dropped, true) + } +} +``` +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` +- any access to the inner memory if it is already dropped. +- Obtaining references to the inner memory (even if not used) +- Writing a value to the `MaybeDropped` after it has already been dropped. + +#### Examples +```rust +let dropped = MaybeDropped::::dropped(); +// this is not ok +// let uninit = MaybeUninit::>::uninit().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(); + +unsafe { + // this is ok + dropped.assume_alive_drop(); + // this is not + 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 feature's implementation will be similar to `MaybeUninit` + +``` +union MaybeDropped { + value: ManuallyDrop, + dropped: () +} +``` + +it does *not* store any drop-state information. + +the public API will expose methods to either create it with an initialized value, or create it *logically dropped*. + +### 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 is considered `undefined behaviour` in `MaybeDropped`? + +> - What parts of the design do you expect to resolve through the implementation of this feature before stabilization? + + - How will we expose the public API? + - 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. From e7f6c41e2d609f9bdf76d976c8a40b517ffbf46d Mon Sep 17 00:00:00 2001 From: IonicMC Date: Tue, 10 Feb 2026 21:03:33 +0200 Subject: [PATCH 2/4] Update and rename 0000-maybe-dropped.md to 3918-maybe-dropped.md --- text/{0000-maybe-dropped.md => 3918-maybe-dropped.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{0000-maybe-dropped.md => 3918-maybe-dropped.md} (99%) diff --git a/text/0000-maybe-dropped.md b/text/3918-maybe-dropped.md similarity index 99% rename from text/0000-maybe-dropped.md rename to text/3918-maybe-dropped.md index 2ce5653970a..38ed2e7e2a7 100644 --- a/text/0000-maybe-dropped.md +++ b/text/3918-maybe-dropped.md @@ -1,6 +1,6 @@ - Feature Name: `maybe_dropped` - Start Date: 2026-10-2 -- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) (todo) +- 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 From f1b76e7078009250f8a6acf4e4ebb233a1aa7627 Mon Sep 17 00:00:00 2001 From: IonicMC Date: Wed, 11 Feb 2026 18:59:30 +0200 Subject: [PATCH 3/4] Update code examples --- text/3918-maybe-dropped.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/text/3918-maybe-dropped.md b/text/3918-maybe-dropped.md index 38ed2e7e2a7..3bb5bea7195 100644 --- a/text/3918-maybe-dropped.md +++ b/text/3918-maybe-dropped.md @@ -120,11 +120,7 @@ The seperation is clear. void do_some_work() { // ... - if (!dropped) { - rust_work(possibly_dropped, false) - } else { - rust_work(possibly_dropped, true) - } + rust_work(possibly_dropped, is_dropped); } ``` And then, in rust @@ -132,7 +128,7 @@ And then, in rust // imports... #[unsafe(no_mangle)] -extern "C" fn rust_work(dropped: MaybeDropped, is_dropped: bool) { +extern "C" fn rust_work(dropped: MaybeDropped, is_dropped: bool) { let as_option = if is_dropped { None } else { From d6eebffa06a10d8b579073e26f3747927fed0a93 Mon Sep 17 00:00:00 2001 From: IonicMC Date: Thu, 12 Feb 2026 10:30:35 +0200 Subject: [PATCH 4/4] Fix issues --- text/3918-maybe-dropped.md | 54 ++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/text/3918-maybe-dropped.md b/text/3918-maybe-dropped.md index 3bb5bea7195..7c0a1f7779e 100644 --- a/text/3918-maybe-dropped.md +++ b/text/3918-maybe-dropped.md @@ -24,21 +24,19 @@ However, an `Option` here is correct, but the following issues arise: 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, from the standard library (simplified) +Take this example. ```rust -pub struct Fuse { +pub struct Fuse { + done: bool, iter: I } ``` -The actual iterator is not needed after it yields `None` once, so in this case (not in the standard library, -however, due to a possible breaking change), We can replace the `I` with a `MaybeDropped`, which can optimize code by dropping the value early +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.) -But you may realize this is exactly the same as using an `Option` (Which `Fuse` does). However, the following point contradicts this. - -2. it is more optimized (for size) +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`. @@ -148,16 +146,19 @@ The following is *__not__* undefined behaviour: the following *__is__* undefined behaviour: - Dropping a `MaybeDropped` twice. -- Creating an uninitialized `MaybeDropped` -- any access to the inner memory if it is already dropped. -- Obtaining references to the inner memory (even if not used) +- 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(); +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(); @@ -168,11 +169,13 @@ 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 - dropped.assume_alive_drop(); + to_be_dropped.assume_alive_drop(); // this is not - dropped.assume_alive_drop(); + // to_be_dropped.assume_alive_drop(); }; // this is not ok @@ -184,18 +187,29 @@ unsafe { ## Reference-level explanation [reference-level-explanation]: #reference-level-explanation -This feature's implementation will be similar to `MaybeUninit` +This is the definition ``` -union MaybeDropped { - value: ManuallyDrop, - dropped: () +#[repr(transparent)] +pub struct MaybeDropped { + value: MaybeUninit> } ``` it does *not* store any drop-state information. -the public API will expose methods to either create it with an initialized value, or create it *logically dropped*. +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 @@ -307,12 +321,8 @@ In systems programming contexts outside of Rust, it is common to distinguish bet [unresolved-questions]: #unresolved-questions > - What parts of the design do you expect to resolve through the RFC process before this gets merged? - - - What is considered `undefined behaviour` in `MaybeDropped`? > - What parts of the design do you expect to resolve through the implementation of this feature before stabilization? - - - How will we expose the public API? - Trait implementations - For example, `Unpin`, `Send`, `Sync`?