Borrowing from a nested RefCell

Hello. I have a structure with a RefCell within a RefCell, and I want to make an accessor function that can borrow the inner value from the outer struct. In my example it's called borrow_inner_value()

use std::cell::{RefCell, Ref};

pub struct InnerStruct {
    pub the_value : RefCell<u8>,
}

pub struct OuterStruct {
    inner_struct : RefCell<InnerStruct>,
}

impl OuterStruct {
    pub fn borrow_inner_value(&self) -> Ref<u8> {

        //Doesn't compile because of temporary Ref from first borrow
        self.inner_struct.borrow().the_value.borrow()

        //Doesn't compile because a Ref isn't really an &
        Ref::map(self.inner_struct.borrow(), |inner_struct| inner_struct.the_value.borrow())
    }

}

fn main() {

    let my_outer_struct = OuterStruct{inner_struct : RefCell::new(InnerStruct{the_value : RefCell::new(5)})};

    //This works
    let _the_value = *my_outer_struct.inner_struct.borrow().the_value.borrow();

    //This doesn't
    let the_value = *my_outer_struct.borrow_inner_value();
}

Obviously in the example, I could just return the u8 rather than Ref, but in real life my structures aren't as simple as the example.

Can anyone suggest the cleanest approach to doing this?

Thank you everyone.

This may not be doable without unsafe. Ref is only valid while it is in scope, and you can't return it with the inner borrow without creating a self-referential struct.

The cleanest way to get this working would be to invert the problem with a map-like API, where the user passes in a function to operate on the inner value. If you need a double-borrow to return the inner ref, the caller is unlikely to be able to use that ref in a more complicated manner anyway, because it's only valid for a very narrow scope.

If that doesn't work, my next thought would be to use try_borrow_unguarded:

pub fn borrow_inner_value(&self) -> Ref<'_, u8> {
    unsafe { self.inner_struct.try_borrow_unguarded()}.unwrap().the_value.borrow()
}

This will avoid creating the outer borrow's Ref.

Is it possible to return a Ref<Ref<T>>? if you can somehow create one of those, you can get rid of the ugly type signature by returning impl Deref<Target = T>

I don't think you want to reach for unsafe here. There's a good chance you'll lead to data races with try_borrow_unguarded().

This is unsound. Calling this function right before self.inner_struct.borrow_mut() is insta-UB, allowing compiler to generate garbage code.

Please don't recommends unsound solutions. The reason why the Rust shines is that the language is memory-safe by default, but unsound functions without marked as unsafe like this would makes it false promise thus harm the entire ecosystem.

2 Likes

That is an excellent suggestion but unfortunately it won't work in my case. The code that needs to touch the inner_value has too many places it needs to touch the outside world.

I think this is probably the best solution; Somehow returning both Refs, so the internal reference guarding can track when the references are dropped. I'm fumbling a little bit trying to make it work though. I hope I'll figure it out if I smash my head on it long enough.

Thanks everyone for the suggestions.

Is it unsound? Who would be calling borrow_mut, in this case?

It is syntactically valid, and may pass borrow checker. So it's compilable.

In Rust, safe code cannot trigger UB, even by intension. But with this function it is possible to trigger UB only with safe code, so it's unsound.

1 Like

Thank you for the replies! But my head is getting sore from smashing it trying to compose a Ref<Ref<T>>. I don't see any way to compose or manipulate Refs except through Ref::map, but I need help understanding how to correctly specify the lifetimes so that the result is a Ref<Ref<T>>

Ref::map(self.inner_struct.borrow(), |inner_struct| &inner_struct.the_value.borrow())

Thanks again to everyone for the help!

If it were possible for you to implement this function:

fn borrow_inner_value(&self) -> Ref<Ref<u8>>

then you would be able to do this:

let r: Ref<Ref<u8>> = o.borrow_inner_value();
let r2: Ref<u8> = Ref::clone(&*r);
drop(r);
println!("{}", r2);

However now you're accessing the insides of a RefCell without any active Ref<...> to that RefCell, which would be unsound.

playground

Thanks for the reply! Thinking out loud... In essence, the borrow checker operates within a scope but not across scopes. So this:

let r1: Ref<InnerStruct> = o.inner_struct.borrow();
let r2: Ref<u8> = r1.the_value.borrow();
drop(r1);

Would cause a borrow-checker violation. But in the code you posted, there is no way for the borrow checker to know about the inherent dependency.

That pretty much squishes the whole category of solutions involving returning two Refs, nested or otherwise. :frowning: Am I understanding correctly?

Is there a better Rust pattern for accomplishing this type of thing, at a high level?

Thank you!

You can make your own ref type that allows you to ask for a Ref<u8> like this:

impl OuterStruct {
    pub fn borrow_inner_value<'a>(&'a self) -> MyDoubleRef<'a> {
        let r = self.inner_struct.borrow();
        MyDoubleRef {
            r: Ref::map(r, |r| &r.the_value)
        }
    }
}

pub struct MyDoubleRef<'a> {
    r: Ref<'a, RefCell<u8>>,
}
impl<'a> MyDoubleRef<'a> {
    pub fn borrow<'b>(&'b self) -> Ref<'b, u8> {
        self.r.borrow()
    }
}

playground

The reason that a Ref<'a, Ref<'a, u8>> is not possible is that the lifetime annotation simply says to the compiler "This value cannot outlive the lifetime 'a". In particular, it does not say that "This value must live exactly as long as 'a". It is allowed to not live as long as that lifetime.

So if you compare Ref<'a, Ref<'a, u8>> to Ref<'a, u8>, there's nothing here that says that the second must live shorter than the first. They have a shared deadline they can't outlive, but which one lives for the longest is not determined by those lifetimes.

The reason these other approaches work is that the inner Ref<'b, u8> you can obtain does not have the same lifetime annotation as the outer Ref. The lifetime annotation on Ref<'b, u8> is the lifetime of a borrow of the outer Ref, so this Ref<'b, u8> cannot outlive that borrow of the outer Ref, which is what makes it sound.

3 Likes

Awesome! I never considered referencing the RefCell.

Your explanation of lifetimes is also wonderful.

Thank you again!

1 Like

FWIW, here are other approaches that can be useful:

The .with_...(|it| { ... }) pattern (CPS)

    pub
    fn with_borrow_inner_value<R> (self: &'_ Self, f: impl FnOnce(&'_ u8) -> R)
      -> R
    {
        f(&*self.inner_struct.borrow().the_value.borrow())
    }
}

fn main ()
{
    let my_outer_struct = OuterStruct {
        inner_struct: RefCell::new(InnerStruct {
            the_value: RefCell::new(5),
        }),
    };

    // This works
    let _the_value = *(
        my_outer_struct
            .inner_struct
            .borrow()
            .the_value
            .borrow()
    );

    // And this too! :)
    let the_value =
        my_outer_struct
            .with_borrow_inner_value(|it| *it)
    ;
}

Basically instead of doing let var = my_outer_struct.thingy(); <use var here>, you do

my_outer_struct.with_thingy(|var| {
    <use var here>
});

It's a bit more cumbersome but it does solve most "cannot return a local" problems :wink:


.borrow_mut()

This, of course, is not really what you asked, but know that if in your code using .borrow_mut() is fine, then a .borrow_mut() on the outer RefCell removes any need to .borrow the inner ones!

  • The reason for that is simple: RefCell is a construct to yield, out of a shared access, exclusive (&mut) or shared (&) access to the wrappee using runtime checks (w.r.t other concurrent accesses that may be happening on this shared value). But out of an already exclusive access, there cannot possibly be such concurrent accesses, so there is no need to perform any runtime checks whatsoever: if the outer .borrow_mut() succeeds, there is no other access on the innermost RefCell that could make the borrows on it race with anything.
impl OuterStruct {
    pub
    fn borrow_inner_value_mut (self: &'_ Self)
      -> RefMut<'_, u8>
    {
        RefMut::map(
            self.inner_struct.borrow_mut(),
            |it| it.the_value.get_mut(),
        )
    }
}

fn main ()
{
    let my_outer_struct = OuterStruct {
        inner_struct: RefCell::new(InnerStruct {
            the_value: RefCell::new(5),
        }),
    };

    // This works
    let _the_value = *(
        my_outer_struct
            .inner_struct
            .borrow()
            .the_value
            .borrow()
    );

    // This works as well
    let _the_value = *(
        my_outer_struct
            .borrow_inner_value_mut()
    );
}

OwningHandle (:warning:with unsafe :warning:)

As @alice pointed out, the issue here is that the return type of your borrow_inner must contain two Ref instances (each one with its .drop() to clear the runtime guards), but the inner object refers to the outer one, so this leads to a self-referential situation that Rust abhors. For this kind of things, OwningHandle can save us when we know the items are not directly self-referential, i.e., that there is a layer of indirection that makes it so objects can be moved around without invalidating the pointers.

impl OuterStruct {
    pub
    fn borrow_inner_value<'a> (self: &'a Self)
      -> impl ::core::ops::Deref<Target = u8> + 'a
    {
        OwningHandle::new_with_fn(
            // Owner: the `Ref<'a, InnerStruct>`
            self.inner_struct.borrow(),
            // Borrowing handle: the `Ref<'a, u8>`
            |it: *const InnerStruct| -> Ref<'a, u8> {
                let it: &'a InnerStruct = unsafe {
                    // Safety: the pointee will live as long as the owner,
                    // so the `'a` lifetime is sound.
                    &*it
                }; 
                it.the_value.borrow()
            },
        )
    }
}

fn main ()
{
    let my_outer_struct = OuterStruct {
        inner_struct: RefCell::new(InnerStruct {
            the_value: RefCell::new(5),
        }),
    };

    // This works
    let _the_value = *(
        my_outer_struct
            .inner_struct
            .borrow()
            .the_value
            .borrow()
    );

    // This works as well
    let _the_value = *(
        my_outer_struct
            .borrow_inner_value()
    );
}
3 Likes

This is great info, and provides some good additional options. Thanks for the reply!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.