[Pre-RFC] Allow moves on objects with active borrows


#1

#Motivation

struct Foo<'r> {
    i: Box<Baz>,
    b: Bar<'r>,
}
struct Baz;

struct Bar<'r> {
    i: &'r Baz,
}

let boxed_baz = Box::new(Baz{});
let boxed_ref: &Baz = boxed_baz.as_ref();
let f = Foo{i: boxed_baz, b: Bar{i: boxed_ref}};

Which results in:

error[E0505]: cannot move out of `boxed_baz` because it is borrowed
  --> src/main.rs:51:20
   |
50 |     let boxed_ref: &Baz = boxed_baz.as_ref();
   |                           --------- borrow of `boxed_baz` occurs here
51 |     let f = Foo{i: boxed_baz, b: Bar{i: boxed_ref}};
   |                    ^^^^^^^^^ move out of `boxed_baz` occurs here

A more real example would look like this

struct Device;

struct CommandBuffer {
    device: &Device,
    // ...
}

// Does not work because Rust does not allow internal pointers
struct Something {
    device: Device,
    command_buffer: CommandBuffer,
}

The workaround for this problem is to fallback to Arc

struct Device;

struct CommandBuffer {
    device: Arc<Device>,
    // ...
}

struct Something {
    device: Arc<Device>,
    command_buffer: CommandBuffer,
}

Arc imposes the following drawback

  • Atomic counter
  • Ignores Rust’s lifetime system

#Solution

Add a builtin trait to allow moves even with active borrows.

unsafe impl<T> BorrowMove<&T> for Box<T> {}

If Rust encounters a borrow of type &T that is linked to a Box<T>, it will allow the box to be moved to a equal or larger lifetime.

Rust will still refuse to move Box<T> for every other active borrow, this includes &Box<T>.

#Problems

Unsure how this will interact with custom alloactors.

#Closing thoughts

This is not really a well crated RFC. I encountered the problem recently while designing vulkan wrapper.

The proposed solution is probably too optmistic and will have some problems. The main goal for the pre-rfc is to see if moving should be allowed for some types that have active borrows.


#2

How does that not create a dangling pointer?


#3

The Box's heap pointer shouldn’t be affected by moving the Box itself, so there’s no real dangling.


#4
let a = Box::new(0);
let b = &a;
let c = ('x', a, 'x');
let d = **b;

Unless you’re going to actually teach Rust the difference between borrowing a thing and a borrow of a thing owned (but not contained in) a thing, this is never going to work.


#5

If arbitrary moves of the Box are permitted, then so is this:

let b = Box::new(..);
let r = &*b;
drop(b):

Additionally, since presumably one still needs mutable access to at least the Box itself, if not its contents since otherwise a type containing a Box becomes effectively immutable (or needs to fall back to pervasive interior mutability). Leaving aside the question of how one can restrict “derived” borrows, this too can lead to dangling pointers, e.g.:

// structs as above

let boxed_baz = Box::new(Baz{});
let boxed_ref: &Baz = boxed_baz.as_ref();
let f = Foo{i: boxed_baz, b: Bar{i: boxed_ref}};
 // write a new boxed Baz to f.i, drop the old one
::std::mem::replace(&mut f.i, Box::new(Baz{}));

#6

Yeah that seems to be a problem that I haven’t thought about.

In general the move onto the stack can’t be allowed. Allow to move Box<T> to Box<T>, but disallow a move to something else like Box<T> to T.

This should probably also be user specified.


#7

This will only work if Box doesn’t contain any other field than the heap pointer - which seems to be case by looking into the implementation - but to know this difference you have to look deeply into the implementation, so this kind of borrowing seems like a bad idea, because most likely the user will not have this deep knowledge and will have a hard time to understand why it works sometimes and in other cases not.

The other thing is, that this might introduce breaking changes to user code if the author of a library adds a private field and suddenly the user can’t borrow the struct in the same way as before.

For some people borrowing is now already too much to bother with, so making the whole system even harder to comprehend IMHO won’t help.


#8

I think you’ve misunderstood. I’m not moving the contents of the box, I’m moving the box. &a is a pointer to the Box on the stack, then using the proposed change, I move the Box itself to somewhere else on the stack, then dereference the Box (which is no longer where the pointer is pointing).


#9

This is one of the main challenges with today’s borrow system. There are a number of challenges with integrating support for this feature into Rust today. One of the biggest is something that your proposal hinted at: in order for this to be even remotely safe, we need to know that the data was borrowed from something with a stable address. So e.g. if you borrow the interior of a box, then even if we move the box, that address won’t change. But this is not true for many containers, because they store things internally (e.g., Option). For other containers, like Vec, it is parly true. One can imagine adding some kind of traits to try and express that (e.g.) the “deref of this ptr type is stable”, which seems to be what you are proposing.

But another big problem is that the borrow checker just doesn’t track information in the way that you propose. For example, if I do let x = &foo.bar, the borrow checker records two independent facts: (1), that the reference x has lifetime 'x, and (2) that foo.bar is borrowed for the lifetime 'x. You can see that these facts are linked (both reference the lifetime 'x), but there is no direct path from the reference x to the fact that it is derived from foo.bar. There may in fact be many references with lifetime 'x, so there is no 1-to-1 correlation between them. This means that we can’t (for example) determine that when foo.bar is moved, x remains alive – at least not without a deep change to the borrow checker.

(There are some other challenges – some of which were highlighted by others on this thread – but I’ll leave it at that for now. =)


#10

Well, ok, I won’t leave it at that. To reiterate what @rkruppe wrote:

Right now, if I have some fn like fn foo(x: Box<T>), I know that x and all data owned by x are unaliased. Lots of code and patterns rely on this. So if we are going to allow some borrows to outlast a move, we have to still forbid calling foo with an outstanding borrow (as foo is within its rights to free x or move it to another thread).