-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Immovable types #1858
Immovable types #1858
Changes from 1 commit
ea780d3
a57a888
7886d57
9a8240e
c235cf2
1783835
ef155ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
- Feature Name: immovable_types | ||
- Start Date: 2017-01-09 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
This add an new built-in trait `Move` which all existing types will implement. Types which do not implement it cannot move after they have been borrowed. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Interacting with C/C++ code often require data that cannot change its location in memory. To work around this we allocate such data on the heap. For example the standard library `Mutex` type allocates a platform specific mutex on the heap. This prevents the use of `Mutex` in global variables. If we add immovable types, we can have an alternative immovable mutex type `StaticMutex` which we could store in global variables. If the lifetime of the mutex is limited to a lexical scope, we could also have a `StaticMutex` in the stack frame and avoid the allocation. | ||
|
||
The key motivation for this proposal is to allow generators to have "stack frames" which do not move in memory. The ability to take references to local variables rely on those variable being static in memory. If a generator is moved, the local variables contained inside also move, which invalidates references to them. So references to local variables stored inside the generator cannot be allowed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another motivation could be types whose destructors must run - If it is on the stack and can't move from there, can safe code prevent its destructor from running? This would also require a way to mark types as only place-able on the stack, but immovability is still a requirement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You may still place your "unleakable" type inside a wrapper type which may not run destructors and cause it to leak. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I explained it more clearly here. The basic idea is adding a way to mark types as immovable + only place-able on the stack. This way they cannot be placed inside a wrapper to be leaked. If the wrapper is completely on the stack (no heap pointer), the unleakable type is not leaked (since I don't know if such a marker can be created/enforced. |
||
|
||
Since generators can only move during suspend points we can require that references to local variables do not live across suspend points and so they would not get invalidated. This is still quite restrictive compared to normal functions and will result in code containing unnecessary allocations. If generators are immovable, no such restrictions apply, and references would work like in normal functions. It does however place a burden on the user of those generators to not move them. This isn't a problem for use cases such as awaiting on a future in asynchronous code or iterating over a structure, since the generator would be stored in the stack frame (which is immovable). | ||
|
||
# Detailed design | ||
[design]: #detailed-design | ||
|
||
A new marker trait `Move` is introduced in `core::marker`. All type parameters (including `Self` for traits) and associated types implement `Move` by default. | ||
|
||
If you want to allow types which may not implement `Move`, you would use the `?Move` trait bound which means that the type may or may not implement `Move`. | ||
|
||
All primitive types implement `Move`. Composite types implement `Move` if all their fields do. The trait implementations for `Move` are provided by the compiler and users cannot implement `Move` themselves. These are the rules for dynamically sized types: | ||
- `[T]` is `Move` if `T` is | ||
- `str` is `Move` | ||
- Trait objects are not `Move` by default | ||
|
||
A new marker struct `ImmovableData` is also introduced in `core::marker`. This struct does not implement `Move` and allows users to make composite immovable types. `PhantomData` should be extended to accept `?Move` types, but `PhantomData` itself should always implement `Move`. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The struct MoveIffMove<T: ?Sized+?Move>(ImmovableData, PhantomData<T>);
unsafe impl<T: ?Sized+Move> Move for MoveIffMove<T> {} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have such implementation for built-in traits? The reason for this rule is to allow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, OIBITs have some precedent here. And if we can do it at all then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eddyb why is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ubsan You could cause unsafety by implementing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cramertj oh, you're right. thanks :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. E.g. struct MoveWhatever<T: ?Move>(T);
impl<T: ?Move> Move for MoveWhatever<T> {} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eddyb We could put |
||
You can freely move values which do not implement `Move` as long as you don't borrow them. Once we borrow such a value, we'd know it's address and code should be able to rely on the address not moving. | ||
|
||
Static variables allow types which do not implement `Move`. | ||
|
||
## Borrowing immovable types | ||
|
||
Borrowing values of types which do not implement `Move` is only allowed if the borrows lasts for the entire lifetime of the values, including the drop of the value, since `drop` takes a reference to it. Reborrows of such borrows follow existing rules. | ||
|
||
This means that the following borrow would not be allowed: | ||
```rust | ||
let mutex = StaticMutex::new(0); | ||
{ | ||
*mutex.lock() += 1; | ||
} | ||
let moved = mutex; | ||
*moved.lock() += 1; | ||
``` | ||
Here `lock` borrows the `mutex` variable. The borrow only last until the end of the same statement. That means we'd be allowed to move `mutex` into a new variable `moved` and call `lock` on it, this time with an different address for `&self`! | ||
|
||
We rely on the fact that borrows prevent moves. We cannot change the lifetime of the borrow to encompass the moving statement using the current borrow checker. This can be changed once we get non-lexical lifetime and we'd get an error on the move instead. | ||
|
||
Conceptually we can think of borrows of `?Move` values as introducing 2 borrows: | ||
- one borrow with as short lifetime as possible with normal restrictions | ||
- one borrow which must match the lifetime of the borrowed value. The only restriction placed is that the value must not be moved out of. | ||
|
||
This RFC suggests an approach where we use only the shorter borrow and require it to match the lifetime of the value. This is less flexible, but results in minimal implementation changes. A more flexible solution can be introduced with non-lexical lifetimes. | ||
|
||
We can easily work around issues with this in code by using a single borrow of the immovable value and just reborrow. | ||
|
||
Illegal: | ||
```rust | ||
let mutex = StaticMutex::new(0); | ||
*mutex.lock() += 1; | ||
*mutex.lock() += 1; | ||
``` | ||
Workaround using reborrows: | ||
```rust | ||
let mutex = &StaticMutex::new(0); | ||
*mutex.lock() += 1; | ||
*mutex.lock() += 1; | ||
``` | ||
|
||
A borrow such as `&var.field` where `field` is immovable will last as long as the lifetime of `var` to ensure it matches the lifetime of the field. | ||
|
||
We need to prevent assignment to immovable types once they have been borrowed. This is because assignment actually moves the l-value before calling `Drop::drop`. If there are any restrictions on the l-value or if the l-value has a dereference operation, assignment to immovable types is not allowed. | ||
|
||
Types which implement `Copy`, but not `Move` are allowed. You can still copy them around, but borrows follows the restrictions of `?Move` types. | ||
|
||
## Immovable types contained in movable types | ||
|
||
To allow immovable types to be contained in movable types, we introduce a `core::cell::MobileCell` wrapper which itself implements `Move`. It works similarly to `Cell` in that it disallows references to the value inside. | ||
```rust | ||
#[lang = "mobile_cell"] | ||
pub struct MobileCell<T: ?Move> { | ||
value: T, | ||
} | ||
|
||
impl<T: ?Move> MobileCell<T> { | ||
pub const fn new(value: T) -> Movable<T> { | ||
Movable { | ||
value: value, | ||
} | ||
} | ||
|
||
pub fn into_inner(self) -> T { | ||
self.value | ||
} | ||
|
||
pub fn replace(&mut self, new_value: T) -> T { | ||
let mut result = MobileCell::new(new_value); | ||
core::mem::replace(self, &mut result); | ||
result.into_inner() | ||
} | ||
} | ||
``` | ||
|
||
## Implications for language traits | ||
|
||
In order to allow functions to take immovable types and arguments and return them, we need to change `FnOnce`, `FnMut` and `Fn`. A `?Move` bound should be added for the `Args` type parameter to these traits. We also need a `?Move` bound on `FnOnce::Output`, which is backwards incompatible. `FnOnce::Output` was stabilized in 1.12, so hopefully there aren't any code relying on it yet. | ||
|
||
Having a `?Move` bound on `Deref::Target` would be nice. It would allow us to use the dereference operator on `Box`, `Rc`, and `Arc` containing immovable types. | ||
|
||
A `?Move` bound on `IntoIterator::IntoIter` and `Iterator::Self` would also be useful, since you could then use immovable iterators in for-loops. | ||
|
||
I suggest we do a `crater` run to investigate if these breakages are feasible. | ||
|
||
Changing these associated types will be insta-stable. You would be unable to write stable code which would conflict with this proposal. `?Move` bounds would also show up in documentation, although we would be able to filter those out if desired. | ||
|
||
# How We Teach This | ||
[how-we-teach-this]: #how-we-teach-this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation RFC pedantry hat on here. 🤓 This section could use to be expanded a bit. Specifically, if accepted this RFC would need to include updates:
I also think the hand-wave that "the concept is likely familiar to users of C, C++, and C FFIs" is insufficient. This is something which people writing FFIs for the first time will need a place to learn, and we can't assume that everyone writing an FFI has prior C or C++ experience. There are many, and an increasing number of, Rust users who are coming to Rust wanting to do FFI for e.g. Python or Ruby and who haven't done it before in other contexts. That said, discussions of the immovability in the C, C++, and FFI contexts might be really helpful prior art—it could possibly be cited, and it should certainly be considered as useful background for whoever writes the docs if this is accepted! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To add to that, despite my experience with C++, I never really had a clear concept of "immovable type" until the need for them arose in the recent Rust coroutine discussions. The closest concept I think I had when doing just C++ was the rather muddy "things that can be invalidated if this other thing moves around, like iterators and their containers, so don't move that other thing until you're done with the things that can be invalidated". |
||
|
||
Rust already has the concept of immovable values when a value is borrowed. This adds types where borrows always last until the value is dropped. | ||
|
||
The concept of immovable types is likely familiar to users of C, C++ and C FFIs. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
This adds a new builtin trait and more logic to the borrow checker. It also requires `?Move` bounds. It may also break existing programs. | ||
|
||
# Alternatives | ||
[alternatives]: #alternatives | ||
|
||
- Instead of having a `Move` trait, we can add final reference types `&final` `&final mut`. Borrowing with these would correspond to borrows of `?Move` types in this RFC. This would require much move invasive changes to the language and may rule out the possiblity of self borrowing types with a `'self` lifetime. | ||
|
||
- Do nothing, but not having this makes generators interact rather poorly with references. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
Which associated types can we change in a backwards incompatible way? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in #1858 (comment), focusing on mutexes might not be relevant anymore, and I'm not aware of other specific C APIs that need this sort of thing - usually you use an opaque pointer and don't even know the size of the allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parking lots require allocations. It is not unreasonable to have a mutex which does not allocate.
I may drop the note on interacting with C/C++. I haven't come up with any cases either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parking_lot
only allocates memory once at startup. Creating, destroying, locking and unlocking a mutex does not allocate memory.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be useful if for some reason you need to bind a C++ class type by value, as C++ objects generally don't expect to be silently moved. In particular, this applies if you want to subclass a C++ class from Rust (in which case the class has to be embedded at the beginning of the struct).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One example from boost is
offset_ptr
which stores the pointer as an offset fromthis
. The idea is to be able to put containers and other data structures in shared memory and have it work even if the memory gets mapped to a different address.