Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c86637f

Browse files
committedJul 30, 2023
Add "Move out of deref for ManuallyDrop" RFC
1 parent abc967a commit c86637f

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed
 

‎text/3465-manuallydrop-deref-move.md

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
- Feature Name: `manuallydrop_deref_move`
2+
- Start Date: 2023-07-30
3+
- RFC PR: [rust-lang/rfcs#3465](https://github.com/rust-lang/rfcs/pull/3465)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
6+
# Summary
7+
8+
Extend the special-case move-out-of-deref behavior of `Box<T>` to `ManuallyDrop<T>`. Additionally, allow partial moves out of a `T` stored inside `ManuallyDrop<T>` even when there is a `Drop` impl for `T`.
9+
10+
# Motivation
11+
12+
Often, instead of dropping a struct, one wants to move out of one of its fields. However, this is impossible to do in safe code when the struct implements `Drop`, requiring use of `unsafe` APIs like `std::ptr::read`, or runtime-expensive workarounds like wrapping the field in an `Option` and using `take()`.
13+
14+
```rust
15+
pub struct Foo(String);
16+
17+
impl Drop for Foo {
18+
fn drop(&mut self) { /* ... */ }
19+
}
20+
21+
pub struct Bar(String);
22+
23+
impl Foo {
24+
pub fn into_bar(self) -> Bar {
25+
let m = core::mem::ManuallyDrop::new(foo);
26+
Bar(unsafe { core::ptr::read(&m.0) }) // Need to use `unsafe`
27+
}
28+
}
29+
```
30+
31+
I ran into this limitation while working on the [`async-lock` library](https://github.com/smol-rs/async-lock/blob/8045684f996b15b3dd9bfd621cfc3864d3760923/src/rwlock.rs#L879-L883). It has also been discussed elsewhere, including [this blog post from 2018](https://phaazon.net/blog/rust-no-drop) (which recommends `mem::uninitialized` as a workaround!) and [this pre-RFC on Internals](https://internals.rust-lang.org/t/pre-rfc-destructuring-values-that-impl-drop/10450).
32+
33+
# Explanation
34+
35+
In today's Rust, `Box<T>` has the unique capability that it is possible to move out of a dereference of it.
36+
37+
```rust
38+
let b: Box<String> = Box::new("hello world".to_owned());
39+
let s: String = *b; // `b`'s backing allocation is dropped here
40+
```
41+
42+
Partial moves are also permitted.
43+
44+
```rust
45+
let s: String;
46+
{
47+
let b: Box<(String, String)> = Box::new(("hello".to_owned(), "world".to_owned()));
48+
s = b.1;
49+
// `b.0` and `b`'s backing allocation dropped here
50+
}
51+
```
52+
53+
This RFC extends this capability to `ManuallyDrop`.
54+
55+
```rust
56+
use core::mem::ManuallyDrop;
57+
58+
let m: ManuallyDrop<(String, String)> = ManuallyDrop::new(("hello".to_owned(), "world".to_owned()));
59+
let _: String = m.1; // `m.1` dropped here
60+
// `m.0` is never dropped
61+
```
62+
63+
In addition, partial moves out of a `ManuallyDrop<T>`'s contents are allowed even when there is a `Drop` impl for `T`.
64+
65+
```rust
66+
struct Foo(String, String);
67+
68+
impl Drop for Foo {
69+
fn drop(&mut self) {
70+
println!("down to the ground");
71+
}
72+
}
73+
74+
let m: ManuallyDrop<Foo> = ManuallyDrop::new(Foo("hello".to_owned(), "world".to_owned()));
75+
let _: String = m.1; // `m.1` dropped here
76+
// `m.0` is never dropped, and nothing is printed.
77+
```
78+
79+
The example from the motivation section would be rewritten as:
80+
81+
```rust
82+
impl Foo {
83+
fn into_bar(self) -> Bar {
84+
let m = core::mem::ManuallyDrop::new(foo);
85+
Bar(m.0)
86+
}
87+
}
88+
```
89+
90+
# Drawbacks
91+
92+
- Adds more "magic" to the language.
93+
- This change would give safe Rust code a new capability (moving out of fields of `Drop`-implementing structs). It's theoretically possible that existing `unsafe` is relying on this capability not existing for soundness:
94+
95+
```rust
96+
use core::hint::unreachable_unchecked;
97+
98+
/// If the bomb is dropped while armed,
99+
/// it explodes and triggers undefined behavior.
100+
pub struct Bomb {
101+
is_armed: bool,
102+
}
103+
104+
impl Drop for Bomb {
105+
fn drop(&mut self) {
106+
if self.is_armed {
107+
println!("💥 BOOM 💥");
108+
109+
// SAFETY: the only way for arbitrary safe code to obtain a `Bomb`
110+
// is via `DefuseWrapper::new()`. Because `DefuseWrapper` implements `Drop`,
111+
// it is impossible to move the `Bomb` out of it.
112+
// `DefuseWrapper`'s destructor ensures that
113+
// `is_armed` is set to `false` before the `Bomb` is dropped,
114+
// so this branch is unreachable.
115+
unsafe { unreachable_unchecked(); }
116+
}
117+
}
118+
}
119+
120+
/// Disarms the bomb before dropping it.
121+
pub struct DefuseWrapper {
122+
pub bomb: Bomb,
123+
}
124+
125+
impl Drop for DefuseWrapper {
126+
fn drop(&mut self) {
127+
self.bomb.is_armed = false;
128+
}
129+
}
130+
131+
impl DefuseWrapper {
132+
pub fn new() -> Self {
133+
DefuseWrapper {
134+
bomb: Bomb {
135+
is_armed: true,
136+
}
137+
}
138+
}
139+
}
140+
```
141+
142+
If code like the above example actually exists in the ecosystem, this proposal would make it unsound.
143+
144+
# Rationale and alternatives
145+
146+
- A more general mechanism for move-out-of-deref, which would subsume `Box`'s special-case support, has long been desired. There have been [three](https://github.com/rust-lang/rfcs/pull/178) [different](https://github.com/rust-lang/rfcs/pull/1646) [RFCs](https://github.com/rust-lang/rfcs/pull/2439) attempting it, and extensive discussion going back to 2014. However, these proposals have all gone nowhere; finding a good design for this API seems to be a hard problem. Also, such an API would not subsume this RFC, as partial moves out of structs with `Drop` impls would still need hard-coded compiler support. In light of these facts, I think adding an existing lang-item type to an existing special case is justified while we wait for a more general `DerefMove`.
147+
- It's possible that that such partial moves could be supported via a different API, such as an attribute (as suggested by the pre-RFC linked in the motivation section) or macro. However, move-out-of-deref is already familiar to Rust developers who have worked with `Box`, and `ManuallyDrop` is the recommended API for avoiding drop. Combining the two seems like the most minimal and intuitive solution.
148+
149+
# Prior art
150+
151+
This RFC addresses a problem unique to Rust's move and destructor semantics, so there is no analogue in other languages.
152+
153+
# Unresolved questions
154+
155+
None, as far as I am aware.
156+
157+
# Future possibilities
158+
159+
A more general `DerefMove` mechanism would be the natural next step, though it would not subsume this RFC, as explained in the [Rationale](#rationale-and-alternatives) section.

0 commit comments

Comments
 (0)
Failed to load comments.