Ouroboros, a crate for making self-referential structs

Thanks for the feedback! That error handling looks really nice, I'll go ahead and implement that. I'm hesitant to change the lifetime to 'self because there is some code that uses 'this as an actual template parameter.

Speaking of that smooth segue, here's a quick explanation of what the expanded code does:

  1. The order of all the fields in the struct is reversed so that references are dropped before the fields they reference.
  2. 'this is replaced with 'static.
  3. Data can only be accessed through use_* functions. These functions accept a closure. In the example, use_float_reference accepts something like for<'this> FnOnce(&'outer_borrow &'this f32). Using the for<'this> part means that the closure has to work for any possible value of 'this, which prevents the closure from storing the reference somewhere else and breaking safety guarantees. The closure can also return a value that will then be returned by the use_* function. As far as I can tell this return value is bounded by how long self was borrowed for, so this code correctly refuses to compile:
let self_ref_struct = make_self_ref_struct();
let float_data_ref = self_ref_struct.use_float_reference(|float_reference| *float_reference);
drop(self_ref_struct);
println!("{:?}", float_data_ref);

Here's the full code that the example I gave expanded into. I don't have the best understanding of how lifetimes work in all the corner cases, so I would greatly appreciate any review of the code:

Expanded Code

This code uses a helper function which looks like this:

    pub unsafe fn stable_deref_and_strip_lifetime<T: StableDeref + 'static>(
        data: &T,
    ) -> &'static T::Target {
        &*((&**data) as *const _)
    }
mod ouroboros_impl_documentation_example {
    use super::*;
    /// The example in the readme and the documentation.
    pub struct DocumentationExample {
        #[doc(hidden)]
        float_reference: &'static mut f32,
        #[doc(hidden)]
        int_reference: &'static i32,
        #[doc(hidden)]
        float_data: Box<f32>,
        #[doc(hidden)]
        int_data: Box<i32>,
    }
    ///A more verbose but stable way to construct self-referencing structs. It is comparable to using `StructName { field1: value1, field2: value2 }` rather than `StructName::new(value1, value2)`. This has the dual benefit of making your code both easier to refactor and more readable. Call [`build()`](Self::build) to construct the actual struct. The fields of this struct should be used as follows:
    ///
    ///| Field | Suggested Use |
    ///| --- | --- |
    ///| `int_data` | Directly pass in the value this field should contain |
    ///| `float_data` | Directly pass in the value this field should contain |
    ///| `int_reference_builder` | Use a function or closure: `(int_data: &_) -> int_reference: _` |
    ///| `float_reference_builder` | Use a function or closure: `(float_data: &mut _) -> float_reference: _` |
    pub struct DocumentationExampleBuilder<
        IntReferenceBuilder_: for<'this> FnOnce(&'this <Box<i32> as ::std::ops::Deref>::Target) -> &'this i32,
        FloatReferenceBuilder_: for<'this> FnOnce(&'this mut <Box<f32> as ::std::ops::Deref>::Target) -> &'this mut f32,
    > {
        pub int_data: Box<i32>,
        pub float_data: Box<f32>,
        pub int_reference_builder: IntReferenceBuilder_,
        pub float_reference_builder: FloatReferenceBuilder_,
    }
    impl<
            IntReferenceBuilder_: for<'this> FnOnce(&'this <Box<i32> as ::std::ops::Deref>::Target) -> &'this i32,
            FloatReferenceBuilder_: for<'this> FnOnce(&'this mut <Box<f32> as ::std::ops::Deref>::Target) -> &'this mut f32,
        > DocumentationExampleBuilder<IntReferenceBuilder_, FloatReferenceBuilder_>
    {
        ///Calls [`DocumentationExample::new()`](DocumentationExample::new) using the provided values. This is preferrable over calling `new()` directly for the reasons listed above.
        pub fn build(self) -> DocumentationExample {
            DocumentationExample::new(
                self.int_data,
                self.float_data,
                self.int_reference_builder,
                self.float_reference_builder,
            )
        }
    }
    ///A more verbose but stable way to construct self-referencing structs. It is comparable to using `StructName { field1: value1, field2: value2 }` rather than `StructName::new(value1, value2)`. This has the dual benefit of makin your code both easier to refactor and more readable. Call [`try_build()`](Self::try_build) or [`try_build_or_recover()`](Self::try_build_or_recover) to construct the actual struct. The fields of this struct should be used as follows:
    ///
    ///| Field | Suggested Use |
    ///| --- | --- |
    ///| `int_data` | Directly pass in the value this field should contain |
    ///| `float_data` | Directly pass in the value this field should contain |
    ///| `int_reference_builder` | Use a function or closure: `(int_data: &_) -> Result<int_reference: _, Error_>` |
    ///| `float_reference_builder` | Use a function or closure: `(float_data: &mut _) -> Result<float_reference: _, Error_>` |
    pub struct DocumentationExampleTryBuilder<
        IntReferenceBuilder_: for<'this> FnOnce(
            &'this <Box<i32> as ::std::ops::Deref>::Target,
        ) -> Result<&'this i32, Error_>,
        FloatReferenceBuilder_: for<'this> FnOnce(
            &'this mut <Box<f32> as ::std::ops::Deref>::Target,
        ) -> Result<&'this mut f32, Error_>,
        Error_,
    > {
        pub int_data: Box<i32>,
        pub float_data: Box<f32>,
        pub int_reference_builder: IntReferenceBuilder_,
        pub float_reference_builder: FloatReferenceBuilder_,
    }
    impl<
            IntReferenceBuilder_: for<'this> FnOnce(
                &'this <Box<i32> as ::std::ops::Deref>::Target,
            ) -> Result<&'this i32, Error_>,
            FloatReferenceBuilder_: for<'this> FnOnce(
                &'this mut <Box<f32> as ::std::ops::Deref>::Target,
            ) -> Result<&'this mut f32, Error_>,
            Error_,
        > DocumentationExampleTryBuilder<IntReferenceBuilder_, FloatReferenceBuilder_, Error_>
    {
        ///Calls [`DocumentationExample::try_new()`](DocumentationExample::try_new) using the provided values. This is preferrable over calling `try_new()` directly for the reasons listed above.
        pub fn try_build(self) -> Result<DocumentationExample, Error_> {
            DocumentationExample::try_new(
                self.int_data,
                self.float_data,
                self.int_reference_builder,
                self.float_reference_builder,
            )
        }
        ///Calls [`DocumentationExample::try_new_or_recover()`](DocumentationExample::try_new_or_recover) using the provided values. This is preferrable over calling `try_new_or_recover()` directly for the reasons listed above.
        pub fn try_build_or_recover(self) -> Result<DocumentationExample, (Error_, Heads)> {
            DocumentationExample::try_new_or_recover(
                self.int_data,
                self.float_data,
                self.int_reference_builder,
                self.float_reference_builder,
            )
        }
    }
    ///A struct for holding immutable references to all [tail and immutably borrowed fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions) in an instance of [`DocumentationExample`](DocumentationExample).
    pub struct BorrowedFields<'outer_borrow, 'this> {
        pub float_reference: &'outer_borrow &'this mut f32,
        pub int_reference: &'outer_borrow &'this i32,
        pub int_data_contents: &'outer_borrow <Box<i32> as ::std::ops::Deref>::Target,
    }
    ///A struct for holding mutable references to all [tail fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions) in an instance of [`DocumentationExample`](DocumentationExample).
    pub struct BorrowedMutFields<'outer_borrow, 'this> {
        pub float_reference: &'outer_borrow mut &'this mut f32,
        pub int_reference: &'outer_borrow mut &'this i32,
    }
    ///A struct which contains only the [head fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions) of [`DocumentationExample`](DocumentationExample).
    pub struct Heads {
        pub float_data: Box<f32>,
        pub int_data: Box<i32>,
    }
    impl DocumentationExample {
        ///Constructs a new instance of this self-referential struct. (See also [`DocumentationExampleBuilder::build()`](DocumentationExampleBuilder::build)). Each argument is a field of the new struct. Fields that refer to other fields inside the struct are initialized using functions instead of directly passing their value. The arguments are as follows:
        ///
        ///| Argument | Suggested Use |
        ///| --- | --- |
        ///| `int_data` | Directly pass in the value this field should contain |
        ///| `float_data` | Directly pass in the value this field should contain |
        ///| `int_reference_builder` | Use a function or closure: `(int_data: &_) -> int_reference: _` |
        ///| `float_reference_builder` | Use a function or closure: `(float_data: &mut _) -> float_reference: _` |
        pub fn new(
            int_data: Box<i32>,
            mut float_data: Box<f32>,
            int_reference_builder: impl for<'this> FnOnce(
                &'this <Box<i32> as ::std::ops::Deref>::Target,
            ) -> &'this i32,
            float_reference_builder: impl for<'this> FnOnce(
                &'this mut <Box<f32> as ::std::ops::Deref>::Target,
            ) -> &'this mut f32,
        ) -> Self {
            let int_data_illegal_static_reference =
                unsafe { ::ouroboros::macro_help::stable_deref_and_strip_lifetime(&int_data) };
            let float_data_illegal_static_reference = unsafe {
                ::ouroboros::macro_help::stable_deref_and_strip_lifetime_mut(&mut float_data)
            };
            let int_reference = int_reference_builder(int_data_illegal_static_reference);
            let float_reference = float_reference_builder(float_data_illegal_static_reference);
            Self {
                int_data,
                float_data,
                int_reference,
                float_reference,
            }
        }
        ///(See also [`DocumentationExampleTryBuilder::try_build()`](DocumentationExampleTryBuilder::try_build).) Like [`new`](Self::new), but builders for [self-referencing fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions) can return results. If any of them fail, `Err` is returned. If all of them succeed, `Ok` is returned. The arguments are as follows:
        ///
        ///| Argument | Suggested Use |
        ///| --- | --- |
        ///| `int_data` | Directly pass in the value this field should contain |
        ///| `float_data` | Directly pass in the value this field should contain |
        ///| `int_reference_builder` | Use a function or closure: `(int_data: &_) -> Result<int_reference: _, Error_>` |
        ///| `float_reference_builder` | Use a function or closure: `(float_data: &mut _) -> Result<float_reference: _, Error_>` |
        pub fn try_new<Error_>(
            int_data: Box<i32>,
            mut float_data: Box<f32>,
            int_reference_builder: impl for<'this> FnOnce(
                &'this <Box<i32> as ::std::ops::Deref>::Target,
            ) -> Result<&'this i32, Error_>,
            float_reference_builder: impl for<'this> FnOnce(
                &'this mut <Box<f32> as ::std::ops::Deref>::Target,
            )
                -> Result<&'this mut f32, Error_>,
        ) -> ::std::result::Result<Self, Error_> {
            let int_data_illegal_static_reference =
                unsafe { ::ouroboros::macro_help::stable_deref_and_strip_lifetime(&int_data) };
            let float_data_illegal_static_reference = unsafe {
                ::ouroboros::macro_help::stable_deref_and_strip_lifetime_mut(&mut float_data)
            };
            let int_reference = int_reference_builder(int_data_illegal_static_reference)?;
            let float_reference = float_reference_builder(float_data_illegal_static_reference)?;
            ::std::result::Result::Ok(Self {
                int_data,
                float_data,
                int_reference,
                float_reference,
            })
        }
        ///(See also [`DocumentationExampleTryBuilder::try_build_or_recover()`](DocumentationExampleTryBuilder::try_build_or_recover).) Like [`try_new`](Self::try_new), but all [head fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions) are returned in the case of an error. The arguments are as follows:
        ///
        ///| Argument | Suggested Use |
        ///| --- | --- |
        ///| `int_data` | Directly pass in the value this field should contain |
        ///| `float_data` | Directly pass in the value this field should contain |
        ///| `int_reference_builder` | Use a function or closure: `(int_data: &_) -> Result<int_reference: _, Error_>` |
        ///| `float_reference_builder` | Use a function or closure: `(float_data: &mut _) -> Result<float_reference: _, Error_>` |
        pub fn try_new_or_recover<Error_>(
            int_data: Box<i32>,
            mut float_data: Box<f32>,
            int_reference_builder: impl for<'this> FnOnce(
                &'this <Box<i32> as ::std::ops::Deref>::Target,
            ) -> Result<&'this i32, Error_>,
            float_reference_builder: impl for<'this> FnOnce(
                &'this mut <Box<f32> as ::std::ops::Deref>::Target,
            )
                -> Result<&'this mut f32, Error_>,
        ) -> ::std::result::Result<Self, (Error_, Heads)> {
            let int_data_illegal_static_reference =
                unsafe { ::ouroboros::macro_help::stable_deref_and_strip_lifetime(&int_data) };
            let float_data_illegal_static_reference = unsafe {
                ::ouroboros::macro_help::stable_deref_and_strip_lifetime_mut(&mut float_data)
            };
            let int_reference = match int_reference_builder(int_data_illegal_static_reference) {
                ::std::result::Result::Ok(value) => value,
                ::std::result::Result::Err(err) => {
                    return ::std::result::Result::Err((
                        err,
                        Heads {
                            int_data,
                            float_data,
                        },
                    ))
                }
            };
            let float_reference = match float_reference_builder(float_data_illegal_static_reference)
            {
                ::std::result::Result::Ok(value) => value,
                ::std::result::Result::Err(err) => {
                    return ::std::result::Result::Err((
                        err,
                        Heads {
                            int_data,
                            float_data,
                        },
                    ))
                }
            };
            ::std::result::Result::Ok(Self {
                int_data,
                float_data,
                int_reference,
                float_reference,
            })
        }
        ///Provides limited immutable access to the contents of `int_data`. This method was generated because `int_data` is immutably borrowed by other fields.
        pub fn use_int_data_contents<'outer_borrow, ReturnType>(
            &'outer_borrow self,
            user: impl for<'this> FnOnce(
                &'outer_borrow <Box<i32> as ::std::ops::Deref>::Target,
            ) -> ReturnType,
        ) -> ReturnType {
            user(&*self.int_data)
        }
        ///Provides an immutable reference to `int_reference`. This method was generated because `int_reference` is a [tail field](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions).
        pub fn use_int_reference<'outer_borrow, ReturnType>(
            &'outer_borrow self,
            user: impl for<'this> FnOnce(&'outer_borrow &'this i32) -> ReturnType,
        ) -> ReturnType {
            user(&self.int_reference)
        }
        ///Provides a mutable reference to `int_reference`. This method was generated because `int_reference` is a [tail field](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions).
        pub fn use_int_reference_mut<'outer_borrow, ReturnType>(
            &'outer_borrow mut self,
            user: impl for<'this> FnOnce(&'outer_borrow mut &'this i32) -> ReturnType,
        ) -> ReturnType {
            user(&mut self.int_reference)
        }
        ///Provides an immutable reference to `float_reference`. This method was generated because `float_reference` is a [tail field](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions).
        pub fn use_float_reference<'outer_borrow, ReturnType>(
            &'outer_borrow self,
            user: impl for<'this> FnOnce(&'outer_borrow &'this mut f32) -> ReturnType,
        ) -> ReturnType {
            user(&self.float_reference)
        }
        ///Provides a mutable reference to `float_reference`. This method was generated because `float_reference` is a [tail field](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions).
        pub fn use_float_reference_mut<'outer_borrow, ReturnType>(
            &'outer_borrow mut self,
            user: impl for<'this> FnOnce(&'outer_borrow mut &'this mut f32) -> ReturnType,
        ) -> ReturnType {
            user(&mut self.float_reference)
        }
        ///This method provides immutable references to all [tail and immutably borrowed fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions).
        pub fn use_all_fields<'outer_borrow, ReturnType>(
            &'outer_borrow self,
            user: impl for<'this> FnOnce(BorrowedFields<'outer_borrow, 'this>) -> ReturnType,
        ) -> ReturnType {
            user(BorrowedFields {
                float_reference: &self.float_reference,
                int_reference: &self.int_reference,
                int_data_contents: &*self.int_data,
            })
        }
        ///This method provides mutable references to all [tail fields](https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html#definitions).
        pub fn use_all_fields_mut<'outer_borrow, ReturnType>(
            &'outer_borrow mut self,
            user: impl for<'this> FnOnce(BorrowedMutFields<'outer_borrow, 'this>) -> ReturnType,
        ) -> ReturnType {
            user(BorrowedMutFields {
                float_reference: &mut self.float_reference,
                int_reference: &mut self.int_reference,
            })
        }
        pub fn into_heads(self) -> Heads {
            drop(self.float_reference);
            drop(self.int_reference);
            let float_data = self.float_data;
            let int_data = self.int_data;
            Heads {
                float_data,
                int_data,
            }
        }
    }
}
pub use ouroboros_impl_documentation_example::DocumentationExample;
pub use ouroboros_impl_documentation_example::DocumentationExampleBuilder;
pub use ouroboros_impl_documentation_example::DocumentationExampleTryBuilder;

While I've only shown examples where the self-referencing part is a literal reference, I designed the code so that it can work on anything that is self-referencing. For example, I am using a library which has a Context struct which can generate a Module<'ctx> struct. The builder for that would look like |context: &'this Context| context.build_module("name"). In that case it is not clear whether the reference needs to be mutable or immutable, so I opted to leave it explicit.

Finally, I wanted to mention I did encounter an issue with my implementation. It does not seem to work with chains, I.E. you cannot have a struct where C references B references A. I don't understand what is causing the issue so I've opened another thread to get some help on it, I would appreciate any help you all could give.

**quick edit: I wanted to point out that the generated code provides no way to access float_data directly, since it is already mutably borrowed by float_reference. In contrast, int_data can be borrowed immutably with borrow_int_data_contents since it has only been borrowed immutably by int_reference.

1 Like