Issue double buffering mutable references

#1

In a real project, I have a structure that I need to double buffer, since I can’t drop it until the next iteration of a loop. This is complicated by the fact that I can’t move it since it’s self-referential. The example below shows the exact problem without the complicated self-referential structs.

I have 2 “immovable” objects a_1 and a_2. I take fat mutable references to them with the Ar type. I want to create and operate on the Ar type for the current iteration before Drop-ing the Ar from the previous iteration. In the below example, I try to wrap the Ar in an Option that I save from iteration to iteration, _defer_drop. This extends the lifetime of the Ar to 2 iterations, which is what I want, but it prevents the borrow checker from seeing that the borrows of a_1 and a_2 last 2 iterations, yet don’t overlap with themselves since they are only borrowed once every 2 iterations.

Has anyone else experienced an issue like this and have any tips?

// I want this to print:
//
// Iteration 1
//   Create 1
// Iteration 2
//   Create 2
//   Drop 1 1
// Iteration 3
//   Create 1
//   Drop 2 1
// Done
//   Drop 1 2

struct A{x: u32, count: u32}
struct Ar<'a>{a: &'a mut A}
impl<'a> Drop for Ar<'a> {
    fn drop(&mut self) {
        // Example to mutate A on Drop
        self.a.count += 1;
        println!("  Drop {}: {}", &self.a.x, &self.a.count);
    }
}
fn main() {
    let mut a_1 = A{x: 1, count: 0};
    let mut a_2 = A{x: 2, count: 0};
    let mut _defer_drop: Option<Ar> = None;
    let mut one = true;
    for i in 1..3 {
        println!("Iteration {}", i);
        if one {
            let ar = Ar{a: &mut a_1};
            println!("  Create {}", &ar.a.x);
            _defer_drop = Some(ar);
        } else {
            let ar = Ar{a: &mut a_2};
            println!("  Create {}", &ar.a.x);
            _defer_drop = Some(ar);
        }
        one = !one;
    }
    println!("Done");
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `a_1` as mutable more than once at a time
  --> src/main.rs:31:28
   |
31 |             let ar = Ar{a: &mut a_1};
   |                            ^^^^^^^^ mutable borrow starts here in previous iteration of loop

error[E0499]: cannot borrow `a_2` as mutable more than once at a time
  --> src/main.rs:35:28
   |
35 |             let ar = Ar{a: &mut a_2};
   |                            ^^^^^^^^ mutable borrow starts here in previous iteration of loop

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0499`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.

0 Likes

#2

I don’t think it’s possible for the compiler to determine the lifetimes don’t overlap by just doing static analysis. To understand that the _defer_drop is using one = !one to alternate between pointing to a_1 and a_2 it’d need to interpret the actually run the program or use runtime checks (which defeats the point of lifetimes being a purely compile-time concept).

It might be that things are too complicated and the problem will solve itself with a small re-design. For example, most implementations of double buffering I’ve seen will use a struct that contains both buffers and a flag to tell you which buffer is the current one. That _defer_drop also complicates things, but that’s almost like an if statement which does some work when switching buffers.

pub struct DoubleBuffer {
  buffer_a: Buffer,
  buffer_b: Buffer,
  count: usize,
}

impl DoubleBuffer {
  fn current_buffer(&mut self) -> &mut Buffer {
    if self.count % 2 == 0 { &mut self.buffer_a } else { &mut self.buffer_b }
  }

  fn switch_buffers(&mut self) {
    self.count += 1;
    if self.count > 1 {
      // do defer_drop stuff for the previous buffer
    }
  }
}
0 Likes

#3

This may be one of those cases where RefCell is appropriate: playground

0 Likes

#4

This could be a scenario where rcu-clean could help. It enables you to modify a copy of an object and then free the old copy (or copies) when convenient. You can keep references to old versions and they’ll remain valid. However, you can’t free until you can prove to the compiler that there are no references left, so on second thought it won’t help you unless you’re either okay leaking memory or you can restructure your loop…

0 Likes

#5

Thanks for the help everyone. RefCell definitely works and I haven’t used it much yet, so I hadn’t considered it. I also cooked up a version using unsafe to transmute the lifetimes (playground). I think that implementation is safe, if maybe overly paranoid about ambiguities in reference aliasing.

0 Likes