Skip to content
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

Closed
wants to merge 7 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions text/0000-immovable-types.md
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.

Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

@Amanieu Amanieu Jan 27, 2017

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.

Copy link

@comex comex Jan 27, 2017

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

Copy link
Contributor

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

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

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

The 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 !Move propagates from members to containing structs). Since it cannot be placed in a heap pointer, RC's and such can't be used to leak them.

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

Copy link
Member

Choose a reason for hiding this comment

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

The PhantomData rule might not be the best choice, although all usecases of PhantomData I can think of involve indirection, so they wouldn't hide any capability. I suppose if we need "!Move if T: !Move" we can do:

struct MoveIffMove<T: ?Sized+?Move>(ImmovableData, PhantomData<T>);
unsafe impl<T: ?Sized+Move> Move for MoveIffMove<T> {}

Copy link
Author

Choose a reason for hiding this comment

The 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 PhantomData<T> inBox<T:? Move> (which is needed for dropck), while keeping Box movable.

Copy link
Member

Choose a reason for hiding this comment

The 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 Box could just do that.

Choose a reason for hiding this comment

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

@eddyb why is Move an unsafe impl?

Copy link
Member

Choose a reason for hiding this comment

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

@ubsan You could cause unsafety by implementing Move for a type that contains a !Move type. Moving the outer type would cause the inner type to be moved.

Choose a reason for hiding this comment

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

@cramertj oh, you're right. thanks :)

Copy link
Member

@cramertj cramertj Jan 27, 2017

Choose a reason for hiding this comment

The 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> {}

Copy link
Author

Choose a reason for hiding this comment

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

@eddyb We could put PhantomData inside MobileCell to ensure it won't cause Box to be immovable

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

  • to the standard library where the types are implemented
  • to the Reference, under 9. Special Traits
  • (probably) to the book in the FFI section

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!

Copy link
Contributor

@Ixrec Ixrec Feb 20, 2017

Choose a reason for hiding this comment

The 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?