Child contains reference data from parent

Hello,
I'm trying to put a reference of a Vec inside a struct that is contained inside another struct that the own the Vec:

    struct Foo<'a> {
        data: &'a [String],
    }

    impl<'a> Foo<'a> {
        fn new(data: &'a [String]) -> Self {
            Self { data }
        }
    }

    struct Bar<'a> {
        data: Vec<String>,
        foo: Foo<'a>,
    }

    impl<'a> Bar<'a> {
        fn new(data: Vec<String>) -> Self {
            let foo = Foo::new(&data);
            Self { data, foo }
        }
    }

    fn main() {
        let v = vec!["s".to_owned()];
        let bar = Bar::new(v);
    }

Unfortunately it doesn't work as I expected. Where is my mistake?

Thank you!

Your mistake is that you're trying to create a self-referential struct. See e.g. this.

What is possible in principle is to replace the references by the indices used to store the owned values. That way borrowck won't complain, and indices are easier to pass around than borrows, too.

Thank you for the answers I didn't think that this is in the end a self-referential struct. After reading the link I found this solution, and because the data is static in memory should work.

use std::ptr::NonNull;

struct Foo {
    data: NonNull<Vec<String>>,
}

impl Foo {
    fn new(data: NonNull<Vec<String>>) -> Self {
        Self { data }
    }
}

struct Bar {
    data: Vec<String>,
    foo: Foo,
}

impl Bar {
    fn new(data: Vec<String>) -> Self {
        let p = NonNull::from(&data);
        Self {
            data,
            foo: Foo::new(p),
        }
    }
}

fn main() {
    let v = vec!["s".to_owned()];
    let bar = Bar::new(v);
    unsafe {
        println!("{:#?}", bar.foo.data.as_ref());
    }
}

This is incorrect. You're creating a pointer to the location on the stack that the vector is stored, not its heap allocation, then you move the vector by returning Bar. This invalidates the pointer. If this works at all its because of an optimization that inlined the call to new, optimizing the move out as a side effect.

Try putting an #[inline(never)] on your new function, or move bar after creation by putting it in a box.

Even if you point to the heap allocation, that pointer would be invalidated if the vector is ever modified.

1 Like

Using the #[inline(never)] seems to work fine and doing this

fn main() {
    let v = vec!["s".to_owned()];
    let bar = Bar::new(v);
    let boxed = Box::new(bar);
    unsafe {
        println!("{:#?}", boxed.foo.data.as_ref());
    }
}

seems to work without any problem. Should I use Pin for a self-referential struct?

You're just being really lucky that the memory location of the thing you moved still has the data from before it got moved. Try a loop:

fn main() {
    let mut bars = vec![];
    for i in 0..5 {
        let v = vec![format!("{}", i)];
        let bar = Bar::new(v);
        bars.push(bar);
    }
    for bar in bars {
        unsafe {
            println!("{:?} {:?}", bar.data, bar.foo.data.as_ref());
        }
    }
}

playground

You should also try running it in Miri, which you can do by selecting it under tools in the playground. It will detect the undefined behaviour you're relying on.

1 Like

As for pin, that would be one approach. However be aware that you can't create the pointer until your struct is called with a pinned reference — so it needs to happen after returning from the constructor.

1 Like

I was playing with Pin, following the official documentation, and I found that this seems to work:

use std::{marker::PhantomPinned, pin::Pin, ptr::NonNull};

struct Foo {
    data: NonNull<Vec<String>>,
    _pin: PhantomPinned,
}

impl Foo {
    fn new(data: NonNull<Vec<String>>) -> Self {
        Self {
            data,
            _pin: PhantomPinned,
        }
    }
}

struct Bar {
    data: Vec<String>,
    foo: Foo,
}

impl Bar {
    fn new(data: Vec<String>) -> Pin<Box<Self>> {
        let res = Self {
            foo: Foo::new(NonNull::dangling()),
            data: data,
        };

        let mut boxed = Box::pin(res);
        let slice = NonNull::from(&boxed.data);

        unsafe {
            let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).foo.data = slice;
        }

        boxed
    }
}

fn main() {
    let mut bars = vec![];
    for i in 0..5 {
        let v = vec![format!("{}", i)];
        let bar = Bar::new(v);
        bars.push(bar);
    }
    for bar in bars {
        unsafe {
            println!("{:p} {:p}", &bar.data, bar.foo.data.as_ref());
        }
    }
}

unfortunately when I run this in miri I get this error:

   Compiling playground v0.0.1 (/playground)
error: Miri evaluation error: trying to reborrow for SharedReadWrite, but parent tag <1887> does not have an appropriate item in the borrow stack
   --> /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ptr/mod.rs:174:1
    |
174 | / pub unsafe fn drop_in_place<T: ?Sized>(to_drop: *mut T) {
175 | |     // Code here does not matter - this is replaced by the
176 | |     // real drop glue by the compiler.
177 | |     drop_in_place(to_drop)
178 | | }
    | |_^ trying to reborrow for SharedReadWrite, but parent tag <1887> does not have an appropriate item in the borrow stack
    |
    = note: inside call to `std::intrinsics::drop_in_place::<Bar> - shim(Some(Bar))` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/ptr/mod.rs:174:1
note: inside call to `std::intrinsics::drop_in_place::<std::pin::Pin<std::boxed::Box<Bar>>> - shim(Some(std::pin::Pin<std::boxed::Box<Bar>>))` at src/main.rs:52:5
   --> src/main.rs:52:5

but I cannot understand the problem very well, probably lack of knowledge :frowning:

What is the actual problem you're trying to solve? It's extemely rare that you can't get around the need for a self-referential stuct by restructuring your program - if you provide more info of what you're trying to do, people will hopefully be able to give you better advice :slight_smile:

I'd definitely also consider @jjpe's advice:

Swapping references for indexes is a pretty common trick to get around self-referencing issues.

What I need is a struct that contains a list of strings and a string generator inside that struct that can generate a string from that list but I cannot move the list strings inside the child generator because I would like to read that list directly on the parent struct, and I cannot move the algorithm to the parent struct because I use that in different places and passing the list is much easier. Furthermore that list is coming from a RON file sederialized with serde so I don't want to move the list because could be useful when I need to resave it. This is why, if I understood correctly that solution, I cannot send indixes, every time the entire list is needed.

First off, I don't mean to offend, but extremely long sentences like in your response are really difficult for others to read and parse. For example took me 4(!) tries just to understand what you meant with your first sentence alone, while I'm used to essentially understanding a sentence while I'm still reading it the first time. What would help tremendously is cutting up the sentences into smaller ones that can easily be read and understood on their own.

Secondly, why not have the generator consume string slices (i.e. &str) instead? If the generator is actually constructing new String instances, the original strings (except the first) can't be kept and thus have to be dropped, because concatenation (in which I include .push_str() and the like) uses string slices for its arguments anyway.
That is likely to keep borrowck happy, too.

Sorry for my last reply I didn't re-read it very well :frowning:

The generator needs to store the entire list because on that list is based the algorithm, so I still need at least the reference to a slice.
However this will end up in the previous case where I'm not able to get a reference to the list, unless I'll split the two structs. I prefer to keep them together because makes more sense considering their behavior.

Thank you again.

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