Self-referential struct - request for a review

Hi Rustaceans :wave:

I'm trying to craft a self-referential struct on the heap with Boxes and Pins with a pinch of unsafe code and am looking for a little bit of a review. The code is as follows:

#![feature(new_uninit)]

use std::mem::MaybeUninit;
use std::pin::Pin;
use std::ptr;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Parent {
    name: String,
}

#[derive(Debug)]
struct Child<'a> {
    name: String,
    parent: &'a Parent,
}

#[derive(Debug)]
struct Family<'a> {
    parent: Parent,
    child: Child<'a>,
    _pin: PhantomPinned,
}

// Key points:
//  0. Result is Pin so that the structure cannot change location.
//  1. construct object with Box::new_uinit().
//  2. take pointer to storage with boxed.as_mut_ptr()
//  3. use this pointer to "write" fields.
//  4. use assume_init to turn Box<MaybeUninit<_>> into Box<_>
//  5. box.into() produces Pin<Box<_>> from Box<_>

impl<'a> Family<'a> {
    fn new<T>(parent_name: T, child_name: T) -> Pin<Box<Family<'a>>>
    where
        T: ToOwned<Owned = String>,
    {
        let mut boxed: Box<MaybeUninit<Family<'a>>> = Box::new_uninit();
        let ptr = boxed.as_mut_ptr();

        unsafe {
            ptr::addr_of_mut!((*ptr).parent).write(Parent {
                name: parent_name.to_owned(),
            });

            ptr::addr_of_mut!((*ptr).child).write(Child {
                parent: &(*ptr).parent,
                name: child_name.to_owned(),
            });

            return boxed.assume_init().into();
        }
    }
}

fn main() {
    let f = Family::new("Krzysztof".to_owned(), "Lenka".to_owned());
    println!("{:?}", f);
}

Is this design sound? Does it contain any undefined behavior that I cannot see? I saw that some crates providing macros for self-referential struct use Box for every field, but this seems to be an overkill to me and am not quite getting the point of doing so.

Is it possible to get rid of feature(new_uninit) to get it compiled with stable? If so, are there smaller chain-saws than std::mem::transmute?

If you'll try to run your code using miri (cargo miri or tools>miri in rust playground), you'll get Undefined Behavior: trying to reborrow for SharedReadOnly...

References don't mix well with self-referential structures. An example in documentation uses pointer: std::pin - Rust

2 Likes

Self-referential structs do not have lifetimes for the self-referential contents. You will have to use raw pointers.

4 Likes

@red75prime , @alice thanks for you answers… Let me add some clarifications:

As usually, the example I provided was a simplification of a real-life problem. The real-life problem is to provide a struct for reading a specific member of a ZIP file with zip crate.

Zip provides a ZipArchive (the "parent") and ZipFile<'a> (the "child") which is created out of the parent. Now image a factory method that takes a file name as input and returns Box<dyn std::io::Read> based on the extension of the file. In order make borrow checker happy I need to bundle ZipArchive together with ZipFile, but ZipFile is parametrized with a reference.

Can such a pattern be expressed with pointers? Would I need to make ZipFile lifetime 'static or something like that?

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.