Offsetof a field of a "C" struct (to implement an intrusive linked list)

This is only disallowed if one of those accesses is a write. Accessing the same value immutably from many threads at once is fine.

When it comes to the type that the reference points to, the rule is just the same as with raw pointers. E.g. it is valid to cast an &i8 to an &u8. It is also valid to cast &mut i8 to &mut u8.

Strictly speaking, the data just has to be valid at the time of access. The lifetime is just a tool for the compiler to stop you from accessing it when it is not valid.

I believe a correct way to phrase it is the following:

A mutable reference guarantees that between any two uses of the same mutable reference, no other pointer or reference has accessed the value. It also guarantees that if the mutable reference was used between any two uses of any other pointer/reference to the same value, then the mutable reference was created from that other pointer/reference (possibly recursively).

Note that an UnsafeCell does not turn off this guarantee.

2 Likes

I would be giving more than 1 <3 if I could.

This is tricky to get my head around.

1 Like

Yeah. The way to think of the second part is that, if you have exclusive access to the value, then it is ok for someone to have accessed the value before you, and it is also ok for someone to access the value after you, but if someone has access before, and still has it afterwards, then you didn't really have exclusive access, did you?

1 Like

I do feel like these rules end up having the "feel" of mathematics, where you have different statements (e.g. lifetimes vs. access order) and then prove they are equivalent.

Why would you not expect them to have the "feel" of mathematics? Making precise statements about when something is valid is right in the middle of the domain of mathematics.

They are not really equivalent since talk about lifetimes often only applies in codebases without unsafe code, but my statements apply equally in the presence of unsafe code (assuming I didn't make a mistake).

1 Like

Kind of: as long as they are no longer dereferenced up until the last usage of the &mut, or if they are derived from the &mut, in which case the tables turn: the &mut must not be used up until the last dereference of the raw pointer. See what follows for more info:

A good trick regarding raw pointers is to consider that a raw pointer *const/mut T acts as if it was a "&UnsafeCell< T >" borrow-checker-wise (let's not talk about the validity of T here).

So you have to consider in which case there may be other &UnsafeCell< T > coexisting with a &mut T without:

  • the borrow-checker complaining,

  • or the contract of UnsafeCell being broken.

Examples:

  • :white_check_mark: Either the &mut T reborrows one of these *const/mut T = &UnsafeCell< T >:

    let p: &UnsafeCell<i32> = …;
    let p2: &mut i32 = &mut *p.get(); // reborrow as Unique
    /* use `p2`, but do not use `p` here (not even for reading):
    …
    */
    last_usage_of(p2);
    // p can now be used again:
    let read_value: i32 = *p.get(); /* or */ p.get().write(written_value);
    
    • raw pointer variant / UnsafeCell goggles off
      let p: *mut i32 = …;
      let p2: &mut i32 = &mut *p; // reborrow as Unique
      /* Use `p2`, but do not use `p` here (not even for reading):
      …
      */
      last_usage_of(p2);
      // p can now be used again:
      let read_value: i32 = *p; /* or */ p.write(written_value);
      
  • :white_check_mark: Or the *const/mut T = &UnsafeCell<T> reborrows the &mut T:

    let p2: &mut i32 = …;
    let p: &UnsafeCell<i32> = UnsafeCell::from_mut(p2); // reborrow as Shared Read-Write
    /* use `p`, but do not use `p2` here (not even for reading!):
    …
    */
    last_usage_of(p);
    // p2 can now be used again:
    …
    
    • raw pointer variant / UnsafeCell goggles off
      let p2: &mut i32 = …;
      let p: *mut i32 = p2 as _; // reborrow as Shared Read-Write
      /* Use `p`, but do not use `p2` here (not even for reading!):
      …
      */
      last_usage_of(p);
      // p2 can now be used again:
      …
      
  • :x: Interleaved accesses is wrong:

    let p: &UnsafeCell<i32> = …;
    let p2: &mut i32 = &mut *p.get(); // reborrow as Unique
    // Using `p` puts the span of the `Unique` access to an end, invalidating `p2`:
    let read_value: i32 = *p.get(); /* or */ p.get().write(written_value);
    some_usage_of(p2); // UB, usage of invalidated pointer.
    
    • raw pointer variant / UnsafeCell goggles off
      let p: *mut i32 = …;
      let p2: &mut i32 = &mut *p; // reborrow as Unique
      // Using `p` puts the span of the `Unique` access to an end, invalidating `p2`:
      let read_value: i32 = *p; /* or */ p.write(written_value);
      some_usage_of(p2); // UB, usage of invalidated pointer.
      
    let p2: &mut i32 = …;
    let p: &UnsafeCell<i32> = UnsafeCell::from_mut(p2); // reborrow as Shared Read-Write
    // Using `p2` puts the span of the `Shared Read-Write` access to an end, invalidating `p`:
    let read_value: i32 = *p2; /* or */ *p2 = written_value;
    // UB, usage of invalidated pointer `p`:
    let read_value: i32 = *p.get(); /* or */ p.get().write(written_value);
    
    • raw pointer variant / UnsafeCell goggles off
      let p2: &mut i32 = …;
      let p: *mut i32 = p2 as _; // reborrow as Shared Read-Write
      // Using `p2` puts the span of the `Shared Read-Write` access to an end, invalidating `p`:
      let read_value: i32 = *p2; /* or */ *p2 = written_value;
      // UB, usage of invalidated pointer `p`:
      let read_value: i32 = *p; /* or */ p.write(written_value);
      

So provided you get a clear hierarchy of pointer usage whereby you can consider that your &mut is reborrowing from the raw pointers (which must thus not be used up during the whole span of the creation of the &mut up to the last usage of that &mut), or the other way around (the raw pointer is derived from an &mut, and between the point of creation and point of last usage of such raw pointer or copy of it, the &mut isn't used), then all is fine.

Otherwise, you are very likely to be doing UB somewhere and should not be using &muts. Generally, when doing this kind of FFI, or when doing stuff like doubly linked lists (and funnily, you are doing both!), you should stay away of &muts as much as possible. If no mutation happens during the existence (up until last usage) of a shared reference, you could be using a shared reference in some cases, but since some interleaved mutation would invalidated the "Shared Read-Only" assumption of a shared reference, it is also something that should be avoided, if possible.

When dealing with complex code, using "Shared Read-Write" pointers (e.g., raw pointers or &UnsafeCell references) is the way to go. In practice, you can use Cell instead of UnsafeCell to have some non-unsafe usability (.get() / .set(), by assuming lack of parallel accesses). Know that once something is wrapped in "may-be-concurrently-mutated" wrapper, you can "project" / split that wrapper onto each part of the bigger thing (e.g., Cell<[Thing; 2]> is the same as [Cell<Thing>; 2]; see cell_project - Rust for a helper macro to do that in the general case (structs)).

2 Likes

Do you think some of this could be cut/pasted into the rustonomicron. It's very useful!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.