Ownership of the fields of a struct when borrowing with mutable references

I am following the Interactive Rust Book to learn about structs in Rust. I trying to understand how permissions work when only certain fields of a struct are borrowed. In this section, we see that when the x field of the struct p is mutably borrowed, the entire struct p loses it's R/W/O permissions.


Now, if we see the same example in the Quiz Question 2:

(coping the code for convenience)

struct Point {

  x: i32,

  y: i32,

}


fn main() {

  let mut p = Point { x: 1, y: 2 };

  let x = &mut p.x;  // Here both p and p.x lose their R/W/O permissions

  let y = &mut p.y;  // Then here, how can p.y be accessed if p itself has lost all permissions?

  *x += 1;

  *y += 1;

  println!("{} {}", p.x, p.y);

}

As stated in the answer, if Rust can understand that p.x refers to a different object than p.y or p why did even p lose the permissions in the first example? It seems inconsistent. What is the general rule for referencing fields and annotating permissions of a struct or elements of a container such as an array, vector, string etc when the entire container has lost permissions because one element was mutably borrowed?
Thanks!

1 Like

For those types you can't take mutable references to multiple elements at all (except with something like slice - Rust). This is because the elements aren't fields, so the compiler can't see this.

What i think they mean with p losing permissions is that you can't create a mutable reference to p, while the mutable reference to p.x exists.

Off topic label discussion

I think the tutorial category is meant for people posting their tutorials and not questions about tutorials, so you may want to change the category go help (if that's possible)

2 Likes

If we don't have Read permissions on p, how can we even reference p.y?

One way to look at it is that the permissions you have to p are just the intersection of the permissions you have to p.x and p.y. So, you have no permissions to p because p.x has been exclusively borrowed, but that doesn't mean you have no permissions to p.y.

5 Likes

That makes sense. Do you have any idea whether the borrow-checker internally maintains the permissions lists for p, p.xand p.y separately or just for p.x and p.y?

I don’t know about the actual implementation, but I would assume that it doesn't track p.x or p.y specifically until you borrow at least one of them, because doing so would waste potentially lots of time and memory (for each use of large or nested structs whose fields are not relevant). It doesn’t need to make a complete table up front. All it needs is: if and when you borrow p.y, then it has to check both p and p.y — all the places on the path to y — for other conflicting borrows.

Hello! I’ve moved this to the help category since you’re seeking help not sharing a tutorial

Note that[1] the permission system is a mental model of how the borrow checker works, and isn't a reference on how the compiler is actually implemented. Currently[2] it computes borrows on a control flow graph of the program, and checks if uses of places conflict with outstanding borrows.

The compiler understands slices and arrays via pattern matching. Slice and array indexing by usize are also a built-in operations, and could technically be tracked for const/literal indices, but are not. However you can emulate it with pattern matching like in the example.

In contrast, for library types like Vec<_>, indexing and obtaining a slice are done via operations / method calls which borrow the entire container.


  1. (despite what impressions the book may give) ↩︎

  2. and in the next gen checker, and for the foreseeable future ↩︎

3 Likes

Borrow checker works on "places".

When you write &mut p.y it's not "borrow p, then borrow y". It's p.y selecting which place you want, and then &mut borrowing that place. This is also why &*obj can work and reborrow a place without moving data, even where *obj isn't allowed and wouldn't make sense as an intermediate step.

3 Likes

Ah okay, that makes sense. Thanks a lot! I need to spend more time looking at ownership examples to understand this concept well.

Thinking about this; if the borrow checker works on places, then why does p lose permissions when p.x is borrowed?

One possible reason, for ownership, is:

fn main(){
struct A (u32,u32);
    let a = A(1,2);// u32 type is inferred
    let field_ref = &mut a.0;
    drop(a);
    field_ref;
}

Now field_ref would be a dangling pointer I think.


We can not have &mut p because it'd mean we can change any internal field through it, even &p is forbidden (ps: not so sure why in this case).

So I take the "permissions on p" to require at least the same access to all fields. If one does not have it, it fails.

So this fails as well:

fn main(){
    /* ...same as above */
    println!("{:?}",a); // this makes it fail as well
    field_ref;
}

So, in terms of something like a mental diagram; one could say p is a full red circle; and every-time you request a field, you are making a "hole" in p; then we can not request full p anymore. (But you can request disjoint places / places making disjoint holes.)

Rust has a concept of interior mutability, which allows &Mutex or &AtomicU32 mutate data through a shared reference.

Apart from that exception, &T requires the borrowed place to be strictly immutable. Not just forbidding you from mutating it (like const in C), but guarantees nobody else can (except the holes of interior mutability types).

So you can't have &p and &mut p.y at the same time, because &p could see its field mutated.

4 Likes

Hello,
Apologies for reviving this thread 21 days later, but I'm facing a difficulty in working with this mental model and also the 'red circle with a hole' mental model suggested by @anon1281670

So, this is again an example from Section 5.3 of the Interactive Rust Book, Q4 of the last quiz on the page.


Copy pasting the code for convenience:

Question 4

Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.

struct Point {
  x: i32,
  y: i32
}
impl Point {
  fn get_x(&mut self) -> &mut i32 {
    &mut self.x  // p.x loses RWO permissions here
  }
}

fn main() {
  let mut p = Point { x: 1, y: 2 };
  let x = p.get_x();  // x gains RWO permissions, permissions for p and p.y unchanged
  *x += 1;
  println!("{} {}", *x, p.y);
}

Now here, I understand that get_x returns a mutable reference to the x field of the struct. Now going by your mode, permissions are attached to places, so we need to separately check permissions for p, p.x and p.y. Based on this logic, once the function ends, the p should regain all it's lost permissions. p.y should have no change in permissions and p.x should lose RWO permissions, giving it up for x. This is the exact error the compiler throws:

error[E0502]: cannot borrow `p.y` as immutable because it is also borrowed as mutable
  --> temp.rs:15:25
   |
13 |   let x = p.get_x();  // x gains RWO permissions, permissions for p and p.y unchanged
   |           - mutable borrow occurs here
14 |   *x += 1;
15 |   println!("{} {}", *x, p.y);
   |   ----------------------^^^-
   |   |                     |
   |   |                     immutable borrow occurs here
   |   mutable borrow later used here
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

Where am I misunderstanding this? Thanks a lot!

We can test this:

fn main() {
  let mut p = Point { x: 1, y: 2 };
  let x = &mut p.x;//p.get_x();  // x gains RWO permissions, permissions for p and p.y unchanged
  *x += 1;
  println!("{} {}", *x, p.y);
}

I would conclude that the function keeps the borrow of p.
But why?

In my view the reason is that the compiler does not know whether you return a borrow to x or to y. So it retains of p. (The compiler only sees function signatures.)

Apart from that, your model is correct. Also, the issue disappears if everything is a shared reference, because as you will figure out (I think you got this very well, but am no expert!) this is primarily a problem for an exclusive borrow.

This signature, which can be made more explicit:

fn get_x<'this>(&'this mut self) -> &'this mut i32 {

Means "so long as the return value is in use, keep *self exclusively borrowed". That means all of the Point: The borrow of the Point has duration 'this, the same as what is in the return value type.

The function body has "permission" to return x instead of y, or to logically rely on everything besides y remaining exclusively borrowed but inaccessible while the return value exists, etc. Due to how the API works, these would be non-breaking changes. If only p.x remained exclusively borrowed, they would be breaking changes.

As of yet, Rust has no function APIs that mean

  • Pass in some reference to Point but only exclusively borrow some fields, not others[1]
  • Pass in something exclusively borrowed, but downgrade some or all of the borrow to shared borrow once the call returns[2]
  • Variations on those themes

  1. search for "view structs" or "view types" for more about this idea ↩︎

  2. you can approximate this by passing back more references or an exclusive referenced and a shared view struct, etc ↩︎

1 Like

There are several noteworthy points here and despite going over your reply several times, a lot of them remain unclear.

What exactly is this? I assume you are referring to the this field of classes, similar to what one sees in C++ or Java. Isn't self the same thing, following the convention used in Python to refer to the class itself instead of calling it this. Or is 'this a Rust-specific keyword?

I do not understand what you mean by 'has duration 'this. Can you elaborate on this?

Well, just going by the signature, can't it return something totally different as well? Like create a new box by doing some operations on x and y and return a mutable reference to that box?

Why so? Or do you mean this is allowed to make the code given 'safe', so that we can access p.y in the print statement. If so, yes, that should be a non-breaking change.

This is the crux of my original question from yesterday… since permissions exist on paths, p.x remaining exclusively borrowed says nothing about the R permission on p.y.

So in this case, how do we model the permission changes across function calls? The answers to my original question (the first post in this thread) convinced me that if there are no function calls, permissions apply to paths, but in case there is a function call which takes a reference to a struct, then all permission downgrades apply to the path of the struct as well as all paths which contain it (like p.x, p.y, p.some_other_struct_object.some_other_attribute etc)

I don't understand this line. Can you give me an example, please?

Thanks a lot!

On a side note: I feel that the Rust book doesn't do justice to the intricacies of the borrowing and permissions mechanism in Rust. Could someone suggest a source which could help me understand it in terms of hard RULES, actual implementations and nuance with many more examples?

An arbitrary name for a generic lifetime, not a keyword at all.

It can, although the box would have to be leaked, otherwise reference to it will not live as long as required - it won't outlive the function body, even.

'a, 'b, 'c, 'this are arbitrary names for a lifetime, marked as '. Lifetimes are always associated with references. See link at the bottom.

The name this was given to help, but may confuse you if you are not used to lifetimes yet.

Many of the questions you will answer yourself if you read the book further than Chapter 4. Some will need other resources, but I would not rush it. Yes, the questions you read are Ch 4, but the paragraph still applies.

For example, there is one on lifetimes where lifetime elision rules are described. I linked the book but you can use the Brown one as well.

So, what does the given signature mean exactly?

Yeah, that's what I meant, returning a reference to a completely new heap location which outlives the function body.