Second borrow to mutate data

Hello. I know having multiple mut references is not allowed in Rust. I'm trying to model some actual firmware behavior to show that Rust can replace the code in C/C++.

This is the broken code, which I want to fix somehow: Rust Playground

The system has allocated objects in seq: Vec<Obj>. Instead of passing referenced in queues, I'm using index numbers from that Vec in queues.

The main idea of the algorithm is to "move numbers" from full Objects into empty. This loop must be fast and easy to read and understand.

Essentially, I need to execute the following:

let mut src = seq[q_full.pop()];
let mut dst = seq[q_empty.pop()];

dst.counter += src.counter;
src.counter = 0;

In the real system, counters can be changed only by 1 at once with some additional logic.

The system has a guarantee that every object is located only in one of the 2 queues or outside of the queues during data processing.

But I can't borrow data from Vec by index as mutable more than one time. But my system guarantees that there are no more than 1 reference to the seq array at any given time.

error[E0499]: cannot borrow `seq` as mutable more than once at a time
  --> src/main.rs:50:35
   |
41 |         let src_ref = &mut seq[src_idx];
   |                            --- first mutable borrow occurs here
...
50 |             let target_ref = &mut seq[target_idx];
   |                                   ^^^ second mutable borrow occurs here
...
53 |             let limit = min(src.counter, MAX_COUNTER);
   |                             ----------- first borrow later used here

Are there any existing data structures or crates to allow something like this? Any techniques or workarounds?
One of the requirements that I have is that we should avoid using seq as an array of pointers to heap allocated data. So, it must store data continuously without Box indirection.

If you want to do this in safe code (without raw pointers), you need to split the borrows in a way that ensures there's no violation of exclusive borrows.

For the example code, if you had a couple of helper functions...

fn split_slice<T>(slice: &mut [T], idx: usize) -> (&mut [T], &mut T, &mut [T]) {
    let (head, tail) = slice.split_at_mut(idx);
    let (item, tail) = tail.split_first_mut().unwrap();
    (head, item, tail)
}

fn get_second<'a, T>(head: &'a mut [T], tail: &'a mut [T], idx: usize) -> &'a mut T {
    if idx < head.len() {
        &mut head[idx]
    } else {
        let idx = idx.checked_sub(head.len() + 1).unwrap();
        &mut tail[idx]
    }
}

Then you could...

        let (head, src_ref, tail) = split_slice(&mut seq, src_idx);
        // ...
            let target_ref = get_second(head, tail, target_idx);

(And you could wrap this pattern up into a couple of custom types if you wanted.)

There may be ways to get multiple non-contiguous entries at once in the future, though I don't know how much it will help you since you're doing work you may not want to repeat between the two accesses.


Cavaet emptor: I typed this up in short order and spent no time verifying correctness.

Note that using references instead of indices would solve the problem, too, at least in your concrete example code…

Rust Playground

Also, in your concrete code, you don't have to have both references at once – just cache the counter in a temporary:

let src_idx = q_full.pop();
let counter = seq[src_idx].counter;
seq[q_empty.pop()].counter += counter;
seq[src_idx].counter = 0;

Thanks everyone for the suggestions! Indeed, In this simplified example, it can be just references.
Ok, now I've prepared a second version of the example, which is close to the real problem.

The same error but with a slightly different context:

error[E0499]: cannot borrow `self.all_objects[_]` as mutable more than once at a time
  --> src/main.rs:61:35
   |
49 |          let mut current_target = &mut self.all_objects[self.target];
   |                                   ---------------------------------- first mutable borrow occurs here
...
61 |                 current_cleaner = &mut self.all_objects[self.cleaner];
   |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second mutable borrow occurs here
...
73 |         current_target.counter += 1;
   |         --------------------------- first borrow later used here

The algorithm involves incrementing the counter in the Object. When we hit the limit, we must get another(or next) Object and continue to fill it. If only 1 free Object remains, I must clean and make the system with at least 2 free Objects.

This code is single-threaded and Sync. However, I'm trying to simulate an embedded system, and I have to follow some existing flows.

-> H2CO3 I "can't cache" the counter to avoid double borrowing. In the real world, it could be changed from different contexts.

-> @steffahn Yeah, thank you for the solution with references. However, this was my fault. I made an extremely simplified version of the actual system, so the above link has slightly updated code.

-> @quinedot This sounds interesting, but I have no idea how I should maintain intermediate split borrows in my updated example to achieve the similar flow of pushing and popping object ids from queues.

From the Splitting Borrows - The Rustonomicon article, I can see that I can achieve what I need without being unsafe, but I can't wrap around those things in my head to store every single bit for my algorithm.

Moreover, after several days of attempts, it's ok for me to have a small amount of unsafe code...

Can still work with references :slightly_smiling_face:

Something doesn't add up here. Concurrent mutation is prevented by the language when &mut references are used. Given that you have (and want to have) &mut references to both objects, this would prevent anything else from accessing their fields while the function is running.

Anyway, it doesn't look like your current_target and current_cleaner depend on each other in any way. You could just move the current_target.counter += 1; line before the loop, which makes it compile.

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.