0
votes

I am looking for a way, how to convince a borrow checker that following code is safe.

Situation

SliceHolder is a structure and I can not change it's interface (different library). I want to temporary set slice in the SliceHolder, call some method which will process data and then finish borrowing (stop borrowing of the slice). My problem is, that borrow checker don't allow me to do it. If I add to interface foo where 'a: 'b borrow checker will fail on self.slice = slice; because input slice can not be stored (even temporary) to the structure will longer lifetime (at least I think that this is the reason). If I change to where 'b: 'a borrow checker will fail on x[0] = 5 because it thinks, that slice is borrowed longer than lifetime of the SliceHolder.

I simply want to temporary use reference of the slice and somehow release the reference when function foo ends so the borrow checker will not consider reference as used. For that I used self.slice = &DEFAULT; which should convince borrow checker that slice is no longer stored inside SliceHolder, but this is not working.

I have found solution with unsafe and raw pointers (self.slice = unsafe {&*(slice as *const [u8])};) but I think it is not correct way how to solve this issue in the rust.

the code

struct SliceHolder<'a> {
    slice: &'a [u8],
}

const DEFAULT: [u8; 0] = [];

impl<'a> SliceHolder<'a> {
    fn foo<'b>(&mut self, slice: &'b [u8]) -> Vec<u8> {
        self.slice = slice; // FIRST POSITION WHERE BORROW CHECKER COMPLAINS
        let result = do_something_with_holder(&self);
        self.slice = &DEFAULT;
        result
    }
}

// blackbox function, do some computation based on the slice
fn do_something_with_holder(holder: &SliceHolder) -> Vec<u8> {
    Vec::from(holder.slice)
}


fn main() {
    let mut holder = SliceHolder {
        slice: &DEFAULT,
    };

    let mut x: [u8; 1] = [1];
    holder.foo(&x);
    x[0] = 5;
    holder.foo(&x); // SECOND POSITION WHERE BORROW CHECKER COMPLAINS
}

Tried with rust 1.40.

1
Please post a minimal reproducible example. In particular we need to know what Result is. See the rust tag for more information about Rust-specific MREs.mcarton
"How to convince borrow checker that borrowed reference is no longer used" but you use that reference. Please clarify your code.Stargateur
If do_something_with_holder were to panic, your holder would be left referencing x after x was dropped but before holder was dropped.loganfsmyth
@mcarton the example will fail, even when do_something_with_holder is not called (and foo has no result). From my point of view the function is a blackbox but for sake of the example you can think about it that clones the content of slice.Tomas
@Stargateur To be honest, I don't know how to formulate it better. Because slice has no compile time size, I need reference to slice (for sake of performance not a copy). And this reference is temporary used when foo is running and then forgotten to allow modification of the slice itself. And this is not working for me.Tomas

1 Answers

3
votes

When trying to fix lifetime issues like this, it's helpful to diagram all the lifetimes involved. There are 2 named lifetimes, but a minimum of 5 relevant lifetimes. This is just for the function SliceHolder::foo.

  • 'static. This is the lifetime that &DEFAULT will have (due to 'static promotion).
  • 'a. The lifetime attached to the type of self.
  • 'b. The lifetime of the slice.
  • Anonymous lifetime 1 (I'll call it 'c). The lifetime of the &mut borrow on self.
  • Anonymous lifetime 2 (I'll call it 'd). The lifetime corresponding to the body of the function.

Here's a diagram of their relationships (forgive the crappy ASCII art). Lifetimes higher on the diagram generally last longer, but a lifetime only outlives another lifetime if there's a sequence of arrows connecting them.

        'static
           ^
         /   \
       /       \
     /          \
    |            |
    V            V
   'a           'b
    |            |
    V            |
   'c            |
     \          /
       \      /
         \  /
          V
         'd

Adding the lifetime bound 'b: 'a amounts to adding an arrow 'a -> 'b on this diagram, saying that 'a outlives 'b, or that borrows with lifetime 'a are valid for the whole period 'b.

Let's take a look at how these lifetimes interact in main.

// holder: SliceHolder<'a>, where 'a: 'static
let mut holder = SliceHolder { slice: &DEFAULT };

let mut x: [u8; 1] = [1];

// Take a mutable borrow of holder (with lifetime 'c1)
// Also take a borrow of x with lifetime 'b1
holder.foo(&x);
// mutate x. This forces 'b1 to end no later than here.
x[0] = 5;
// Take another mutable borrow of holder ('c2)
// and another borrow of x ('b2)
holder.foo(&x);

This shows that we can't have 'b: 'a, since that would mean that 'a has to end no later than when 'b ends. That would force holder to stop existing before x is mutated. But holder is used later than that.


So we've demonstrated that we can't have 'b: 'a. What's left then? When we perform the assignment self.slice = slice;, we're implicitly casting from &'b [u8] to &'a [u8]. This requires that 'b: 'a, which we've just ruled out. self always has to have type SliceHolder<'a>, so we can't just cut down its lifetime.

Aside: If we didn't uphold that that the slice in self always has lifetime (at least) 'a, we could panic at some point (e.g. in do_something_with_holder) and avoid the control path where self.slice gets reassigned to something with a longer lifetime. 'b would end (invalidating slice: &'b [u8]), but self would still exist and be holding an invalid reference. These are the sorts of things you need to consider if you ever use unsafe code.

However, we could have a second variable with a shorter lifetime whose value (i.e. the internal slice) is the same as self. We'd need this second variable to have type SliceHolder<'_> with some lifetime that's no longer than 'a (so that we can use self's reference) and no longer than 'b (so we can assign slice to its slice). We look on the diagram see that there is indeed a lifetime shorter than both 'a and 'b, namely 'd.

Fortunately, we don't need to worry about naming this lifetime. All that matters is that it exists and the compiler will figure out the rest. So how do we get this second variable? We need to somehow move self into a new variable. But remember, we can't move out of mutable references, so we have to leave something valid inside.

We were already planning on switching out self's reference for &DEFAULT, so why not just do that? The relevant command is std::mem::replace and can be used like let second_variable = std::mem::replace(self, SliceHolder {slice: &DEFAULT}).

There are some slightly more ergonomic ways to do this, but it would involve adding some traits for SliceHolder. If SliceHolder really only contains a reference, it can implement Copy, which makes everything in this situation easier. Barring that, implementing Default for it (with &DEFAULT as the default slice) would allow you to use the newly stabilized std::mem::take instead of std::mem::replace. Then you wouldn't need to construct that default value inline.

There are probably some other things to consider, but without a Minimal Reproducible Example, it's hard to say what. I'll leave you with some working code (playground).

struct SliceHolder<'a> {
    slice: &'a [u8],
}

const DEFAULT: [u8; 0] = [];

struct Result;

fn do_something_with_holder(_: &SliceHolder) -> Result {
    Result
}

impl<'a> SliceHolder<'a> {
    fn foo<'b>(&mut self, slice: &'b [u8]) -> Result {
        // new_holder has the exact value that self would have had before
        let mut new_holder: SliceHolder<'_> =
            std::mem::replace(self, SliceHolder { slice: &DEFAULT });

        new_holder.slice = slice;
        let result = do_something_with_holder(&new_holder);
        // self.slice = &DEFAULT; // no longer needed - we've already taken care of that
        result
    }
}

fn main() {
    let mut holder = SliceHolder { slice: &DEFAULT };

    let mut x: [u8; 1] = [1];
    holder.foo(&x);
    x[0] = 5;
    holder.foo(&x);
}