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 all commits
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
148 changes: 148 additions & 0 deletions text/0000-immovable-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
- 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 may 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.
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 builtin 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`.

A new marker struct `Immovable` is also introduced in `core::marker`. This struct does not implement `Move` and allows users to make composite immovable types.

You can freely move values which are known to implement `Move` after they are borrowed, however you cannot move types which aren't known to implement `Move` after they have been borrowed. Once we borrow an immovable type, we'd know its address and code should be able to rely on the address not changing. This is sound since the only way to observe the address of a value is to borrow it. Before the first borrow nothing can observe the address and the value can be moved around.
Copy link

Choose a reason for hiding this comment

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

I'm not sure how useful this is - the main motivation for this RFC is self-referencing generators (or self-referencing structs in general, I assume), and this doesn't apply to them.

Copy link

Choose a reason for hiding this comment

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

This is useful for interacting with libraries in other languages that store pointers to Rust values that would be invalidated by those values being moved.

Copy link
Author

Choose a reason for hiding this comment

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

@nbaraz This is the most important part of this RFC. This is the part which enables immovable types to be moved. This is very useful for generators. In particular you can return generators from functions, since returning is a move.

Copy link

Choose a reason for hiding this comment

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

Any self-referential structure would need to be immediately considered frozen, as it borrows itself from the beginning of its own existence, so the "until it is borrowed" clause would come into effect immediately.

This section does concern me for its implications in rental, however. It's not at all clear to me what exact conditions a value is to be considered "observed". If a function accepts a ?Move type by value, we could presume that it hasn't bee observed yet otherwise passing it would have been illegal in the first place, but what about immovable types behind a Box or Arc. Are they considered implicitly frozen when accepted as arguments or when returned from a function?

Copy link
Author

Choose a reason for hiding this comment

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

Any self-referential structure would need to be immediately considered frozen, as it borrows itself from the beginning of its own existence, so the "until it is borrowed" clause would come into effect immediately.

This is not possible in Rust though (even with this RFC). You can only borrow values after they have been constructed.

It's not at all clear to me what exact conditions a value is to be considered "observed".

One way to determine this is to ask if you can get a pointer to it in safe Rust. Since Box implements Deref we can call Deref::deref to obtain a pointer to the inner value, thus the inner value must be counted as observed once its placed in a Box. It is also possible to have some other MovableBox type, which do not allow access to the inner value, except by moving it out again.

In general, we can allow immovable types in an movable container if we either, disallow all methods of accessing the address of the contained immovable types or prevent the type from actually moving once it's inside.

Copy link

Choose a reason for hiding this comment

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

This is not possible in Rust though (even with this RFC). You can only borrow values after they have been constructed.

Right, I mean in a theoretical future where rust can express a self-referential type, it would need to be aware that it's in an immediate state of borrowed-ness.

Since Box implements Deref we can call Deref::deref to obtain a pointer to the inner value, thus the inner value must be counted as observed once its placed in a Box.

Excellent, that's what I was hoping for. To be able to add support for immovables to rental, I need to ensure that anything I return is already considered observed and frozen, since rust itself is unaware of the self-referentiality and might otherwise allow the type to move when it shouldn't.
Still poses some challenges with how to allow the user to select which container the value should be constructed into, but ATCs are likely the best solution for that.


Static variables allow types which do not implement `Move`.

These are the rules to determine if a type implements `Move`:
- Integers, floats, `char` and `bool` are `Move`
- Function types and function pointers are `Move`
- Closures are `Move` if their captured variables are `Move`
- Movable generators are `Move` if all values (including capture variables) which are live during a suspension point are `Move`
- Immovable generators are never `Move`
- The `Immovable` type is never `Move`
- Trait objects are `Move` if they have an explicit `Move` bound
- Struct, enums and tuples are `Move` if all their elements are `Move`
- Existential types (`impl Trait`) are `Move` if their underlying type are `Move`
Copy link

Choose a reason for hiding this comment

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

I think that this needs to be explicit. If you have:

struct Bar;
impl Foo for Bar {}

fn baz() -> impl Foo {
    Bar
}

And then change baz() to:

impl Foo for Immovable {}

fn baz() -> impl Foo {
    Immovable
}

Code that assumes that baz() returns a movable value will break.

In order to return an existantial + immovable struct, you should have to specify it as being ?Move:

struct Bar;
impl Foo for Bar {}
impl Foo for Immovable {}

fn baz() -> impl Foo + ?Move {
    Immovable
}

(I haven't used existantial types, I don't know if impl Foo + Baz works).

The other solution would be to have all existantial types be ?Move by default, which wouldn't work.

Copy link

Choose a reason for hiding this comment

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

Actually, this example/use case isn't straightforward because returning / passing an argument is a move. But it could interact with placement-new.

- `[T]` and `[T; n]` are `Move` if `T` is `Move`
- `str` is `Move`
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably add "borrowed and mutable references are always Move" for completeness.


## Move checking

We need to ensure that values we have borrowed no longer can be moved. When we borrow a value we can find its address in memory. For example:
```rust
struct Foo {
field: bool,
}

fn address(v: &Foo) -> usize {
v as *const _ as usize
}

let a = Foo {
field: true
};
address(&a)
```
We can also find the address of `a.field` in our `address` function using this code:
```rust
fn address(v: &Foo) -> usize {
&v.field as *const _ as usize
}
```
Thus we say that our borrow `&a` observes both `a` and `a.field`.
In general, borrowing a value observes all other values stored inside except for values which are reached using an indirection (for example a reference or a `Box`). If any elements of an array is observed, the entire array is also observed.

Whenever we are moving an value we emit an error if the type does not implement `Move` and the value could also have been observed. We do the same check for any values stored inside; again ignoring indirections.

## Immovable types contained in movable types

To allow immovable types to be contained in movable types, we introduce a `core::cell::MovableCell` wrapper which itself implements `Move`. It works similarly to `Cell` in that it disallows references to the value inside.
Copy link

@nbaraz nbaraz Jan 12, 2018

Choose a reason for hiding this comment

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

I can't think of a scenario where this is actually safe - If a type assumes that it doesn't move in memory because something depends on its address, moving it will always break something.

I think that it makes sense for immovable types to only live on the stack or behind pointers, which are always Move, and for instances to be created with placement-new.

Maybe add an ExplicitMove trait, which implemented for every struct whose members implement ExplicitMove (by default not implemented for immpvable types)? I think that this can be addressed in another RFC.

```rust
#[lang = "movable_cell"]
#[derive(Default)]
pub struct MovableCell<T: ?Move> {
value: T,
}

impl<T: ?Move> MovableCell<T> {
/// Creates a new MovableCell.
pub const fn new(value: T) -> Self {
MovableCell {
value: value,
}
}

/// Extracts the inner value.
pub fn into_inner(self) -> T {
self.value
}

/// Replaces the inner value.
pub fn replace(&mut self, new_value: T) -> T {
mem::replace(self, MovableCell::new(new_value)).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.

## Allowing immovable types in container types

`std::boxed::Box`, `std::rc::Rc`, `std::rc::Weak`, `std::sync::Arc`, `std::sync::Weak` will be changed to allow immovable types inside, but will themselves be movable. These can be used to overcome the limitations of immovable types at the cost of an allocation.

For `Rc` and `Arc` , the function `try_unwrap` would only be allowed on movable types.

In general, we can allow immovable types in an movable container if we either:
- disallow all methods of accessing the address of the contained immovable types, including references (possible for `Vec`, `HashMap`)
Copy link
Member

Choose a reason for hiding this comment

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

I'd call this "rvalue containers" and the other one "lvalue containers".

Copy link
Author

Choose a reason for hiding this comment

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

That seems confusing, since you can get the address of both l and r-values in Rust. Not that these are important enough to have names.

Copy link
Member

Choose a reason for hiding this comment

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

That's not what happens though, rvalues are converted to lvalues if necessary.

- prevent the type from actually moving once it's inside (the method suitable for `Box`, `Rc`, `Arc`)

# How We Teach This
[how-we-teach-this]: #how-we-teach-this

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 another checker pass. 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?