Why this code is meant to borrow as mutable more than once?

struct DcdWriter<'a>(&'a mut [u8]);
impl<'a> DcdWriter<'a> {
   fn write_with_len(&'a mut self, value: u32, len: usize) -> &'a mut DcdWriter<'a> {
       dcd_encode(value, &mut self.0[0..len]);
       self.0 = &mut self.0[len..];
       self
   }

   fn write_exact<T: num_traits::ToPrimitive>(&'a mut self, value: &T) -> &'a mut DcdWriter<'a> {
       let len = std::mem::size_of_val(&value);
       self.write_with_len((*value).to_u32().unwrap(), len)
   }

   fn write_simple<'b:  'a>(&'b mut self, value: u32, len: usize) {
       dcd_encode(value, &mut self.0[0..len]);
       self.0 = &mut self.0[len..];
   }
}

impl BeQueryData {
   pub fn to_packet(&self, packet: &mut PckStruct) {
       packet.p_param.resize(48, 0x00);
       let mut writer = DcdWriter(&mut packet.p_param);
       //let writer = writer
       //    .write(self.status_flags, 6)
       //    .write(
       //        self.vcharge_max_pos.to_u32() as u32,
       //        std::mem::size_of_val(&self.vcharge_max_pos) * 2,
       //    )
       //    .write(self.vcharge_max_neg as u32, 4)
       //    .write(self.icharge_max_pos as u32, 4);
       writer.write_simple(self.status_flags, 6);
       writer.write_simple(self.vcharge_max_pos as u32, 6);
   }
}

This secondo call to writer.write_simple generates an error ("cannot borrow writer as mutable more than once at a time").
The commented part works instead.
Why is that? Where is writer borrowed?
By trial and error i found that the by removing the lifetime annotation ('b) for &mut self the error disappers but then i'm not able to modify self.0 anymore.
I could just uuse the other methods but still i would like to understand the reason of this behaviour.

Thanks

When using mutable references, you need to keep separate the lifetimes for “a Self is borrowed” (&'a mut self) and “Self contains borrowed data” (Self = DcdWriter<'a>). Creating an &'a mut DcdWriter<'a>, with the same lifetime in both positions, is nearly always wrong, because it implies that the DcdWriter is now exclusively borrowed for the rest of its existence, and therefore it can no longer be used at all.

Remove all of your extra lifetime annotations. Once you do, you'll have a problem with your assignment to self.0. The spcific trick to solve this problem is that you need to take (move) the existing self.0 mutable reference, so that you can slice it and get back one with the original 'a lifetime instead of a reborrow through the &mut self reference, which necessarily has a shorter lifetime.

impl<'a> DcdWriter<'a> {
    fn write_with_len(&mut self, value: u32, len: usize) -> &mut DcdWriter<'a> {
        dcd_encode(value, &mut self.0[0..len]);

        let slice_taken: &'a mut [u8] = std::mem::take(&mut self.0);
        self.0 = &mut slice_taken[len..];

        self
    }
    ...

Notice the taken reference has lifetime 'a, but we did not add any lifetime annotations (that would constrain 'a further) to the function signature.

Since both of your write methods will need this, it might be useful to write a helper function that does this “consume some elements” operation.

7 Likes

&'a mut self is such a frequent footgun. I've just answered another one:

3 Likes

Now it is more clear, thanks.
However, couldn't the compiler understand by itself that the lifetime of self.0 is already 'a and so allow me to do

self.0 = &self.0[len..];

how could that possibly fail?

With shared loans & it doesn't fail.

However, &mut is not just mutable. It's a special kind of loan that is very very strictly exclusive, meaning there can't be any other &mut to the same data anywhere, under any circumstances, even for a split second.

self.0 = &mut self.0[len..];

but here self.0 and &mut self.0[len..] coexist for a brief moment (it calls self.0.index_mut() -> &mut under the hood).

There is a silly workaround for this:

// self.0 is replaced with an empty `&mut []` to nowhere
let old_ref = std::mem::take(&mut self.0); 
self.0 = &mut old_ref[len..]; // or old_ref.split_at_mut(len).0
2 Likes

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.