ouroboros

repository·main·Indexed 20 days ago

https://github.com/someguynamedjosh/ouroboros

A Rust library providing the #[self_referencing] macro to safely generate self-referential structs. It allows fields to hold references to other fields within the same struct using a special 'this lifetime. The library includes a generated builder pattern for instantiation, support for asynchronous initialization via MyStructAsyncBuilder, and no_std compatibility (requiring alloc). It provides safe access to internal fields through generated borrow_FIELD(), with_FIELD(), and with_mut() methods.

Tokens
2.9K
Snippets
8
Records
10
Agent score
22%

What's inside ouroboros

  1. Hide self-referential structs behind a friendly wrapper

    main

    The #[self_referencing] macro adds many public methods to a struct (like borrow_{field} and with_mut). To avoid exposing this cluttered API to users of your library, it is recommended to use an internal/external pattern:

    1. Define an Internal struct annotated with #[self_referencing].
    2. Define a Friendly public struct that contains the Internal struct as a private field.
    3. Implement your desired public API on the Friendly struct, wrapping the complexity of the internal self-referential logic.
    #[self_referencing]
    struct Internal {
        // ...
    }
    
    // This struct provides a clean interface for library users
    pub struct Friendly {
        internal: Internal,
    }
    
    impl Friendly {
        pub fn new() -> Self {
            // Implementation details...
            Friendly { internal: InternalBuilder { ... }.build() }
        }
        
        pub fn do_the_thing(&self) -> T {
            // Use self.internal.borrow_...() here
        }
    }
  2. Ouroboros requirements and compatibility

    main

    Runtime Requirements

    • no_std support: Ouroboros is no_std compatible.
    • alloc requirement: Even in no_std mode, the alloc crate is required.

    Version Compatibility

    • Rust Version:
      • Version 0.17.0 and later require Rust 1.60 or later.
      • Version 0.18.0 and later require Rust 1.63 or later.
    • Automatic Boxing: Since version 0.10.0, the library automatically boxes every field to prevent undefined behavior.
  3. Create self-referential structs with #[self_referencing]

    main

    Ouroboros provides the #[self_referencing] macro to allow Rust structs to hold references to their own fields.

    Implementation Steps:

    1. Annotate your struct with #[self_referencing].
    2. Define fields that will be owned (e.g., int_data: i32).
    3. Define fields that will be references using the #[borrows(field_name)] attribute.
    4. Use the special 'this lifetime for these reference fields. This lifetime is automatically created by the macro.

    Accessing Data:

    • Direct access is not possible: You cannot access fields directly.
    • Borrowing: The macro generates borrow_{field_name}() methods to access fields.
    • Mutation: Use with_mut(|fields| { ... }) to perform mutable operations on the internal fields.

    Lifetime Constraints:

    References obtained from the struct (e.g., via borrow_field()) are tied to the lifetime of the struct itself. If the struct is dropped, any held references become invalid and will cause compilation errors if used.

    use ouroboros::self_referencing;
    
    #[self_referencing]
    struct MyStruct {
        int_data: i32,
        float_data: f32,
        #[borrows(int_data)]
        int_reference: &'this i32,
        #[borrows(mut float_data)]
        float_reference: &'this mut f32,
    }
    
    fn main() {
        let mut my_value = MyStructBuilder {
            int_data: 42,
            float_data: 3.14,
            int_reference_builder: |int_data: &i32| int_data,
            float_reference_builder: |float_data: &mut f32| float_data,
        }.build();
    
        // Accessing data
        println!("{:?}", my_value.borrow_int_data());
        println!("{:?}", my_value.borrow_float_reference());
    
        // Mutating data
        my_value.with_mut(|fields| {
            **fields.float_reference = (**fields.int_reference as f32) * 2.0;
        });
    }
  4. Use the #[self_referencing] macro to create self-referencing structs

    main

    The #[self_referencing] macro transforms a standard Rust struct into a safe self-referencing struct. This allows fields to hold references to other fields within the same struct using the 'this lifetime.

    Key Concepts

    • 'this lifetime: A special lifetime created by the macro. Use it for any field that borrows from the struct.
    • #[borrows(...)]: Annotate self-referencing fields with this attribute. It specifies which fields are being borrowed. Use mut to indicate a mutable borrow (e.g., #[borrows(field_a, mut field_b)]).
    • Head fields: Fields that do not borrow anything. They are initialized with direct values.
    • Self-referencing fields: Fields that borrow other fields. They are initialized via closures in the builder.
    • Tail fields: Fields that are not borrowed by any other fields.

    Limitations

    • Fields must be declared before the first time they are borrowed.
    • Standard Rust borrowing rules apply (e.g., you cannot borrow a field mutably twice).
    • Fields using 'this must have a corresponding #[borrows] annotation.
    use ouroboros::self_referencing;
    
    #[self_referencing]
    struct MyStruct {
        int_data: i32,
        #[borrows(int_data)]
        int_reference: &'this i32,
    }
  5. Initialize self-referencing structs asynchronously

    main

    You can initialize structs asynchronously using MyStruct::new_async() or the MyStructAsyncBuilder.

    • Closures: When using a builder, the closures for self-referencing fields must return a Pin<Box<dyn Future<Output = T>>>.
    • Execution: All builder functions are called serially in the order they were declared.
    • Send Support: If you need the resulting struct to implement Send, use MyStructAsyncSendBuilder or MyStruct::new_async_send().
    let mut my_value = MyStructAsyncBuilder {
        int_data: 42,
        float_data: 3.14,
        int_reference_builder: |int_data: &i32| Box::pin(async move { int_data }),
        float_reference_builder: |float_data: &mut f32| Box::pin(async move { float_data }),
    }.build().await;
  6. Initialize self-referencing structs with MyStructBuilder

    main

    The preferred way to create an instance of a self-referencing struct is using the generated [StructName]Builder.

    • Head fields: Use the same name as defined in the struct.
    • Self-referencing fields: Use the name of the field suffixed with _builder (e.g., int_reference_builder). These fields require a closure that accepts references to the borrowed fields and returns the new value.
    • Empty borrows: Fields with #[borrows()] are initialized directly like head fields.

    Calling .build() on the builder executes the closures in the order they were declared.

    #[self_referencing]
    struct MyStruct {
        int_data: i32,
        #[borrows(int_data)]
        int_reference: &'this i32,
    }
    
    let my_value = MyStructBuilder {
        int_data: 42,
        int_reference_builder: |int_data: &i32| int_data,
    }.build();
  7. Use the MyStructBuilder to instantiate self-referential structs

    main

    When you apply #[self_referencing] to a struct, the macro automatically generates a companion builder struct named {StructName}Builder.

    Builder Configuration:

    • Owned Fields: Provide the value directly (e.g., int_data: 42).
    • Reference Fields: Provide a closure for fields marked with #[borrows]. The field name in the builder follows the pattern {field_name}_builder.
    • Closure Arguments: The closure passed to a _builder field receives a reference to the field(s) specified in the #[borrows] attribute.

    Call .build() on the builder to create the instance.

    let mut my_value = MyStructBuilder {
        int_data: 42,
        float_data: 3.14,
        // The builder field name is the struct field name + `_builder` 
        // The closure receives a reference to the borrowed field(s)
        int_reference_builder: |int_data: &i32| int_data,
        float_reference_builder: |float_data: &mut f32| float_data,
    }.build();
  8. Handle covariance with #[covariant] and #[not_covariant]

    main

    The macro may be unable to determine if a type is covariant. If you encounter compiler errors regarding uncertainty about covariance, use these annotations on the field to resolve it:

    • #[covariant]: Tells the macro the type is covariant, enabling the generation of the borrow_FIELD() method.
    • #[not_covariant]: Tells the macro the type is not covariant, preventing the generation of borrow_FIELD() (forcing the use of with_FIELD() instead).
    #[self_referencing]
    struct DataStorage {
        immutable: i32,
        mutable: i32,
        #[borrows(immutable, mut mutable)]
        #[not_covariant]
        complex_data: ComplexData<'this, 'this>,
    }
  9. Access fields in a self-referencing struct

    main

    Because the internal fields are private to ensure safety, the macro generates specific accessor methods:

    • borrow_FIELD(): Returns an immutable reference to a tail or immutably-borrowed field. Only generated for types known to be covariant (or if annotated with #[covariant]).
    • with_FIELD(|field| ...): Safely accesses a reference to a tail or immutably-borrowed field via a closure. This is the safer alternative to borrow_FIELD().
    • with_FIELD_mut(|field| ...): Safely accesses a mutable reference to a tail field via a closure.
    • with(|fields| ...): Borrows all tail and immutably-borrowed fields at once.
    • with_mut(|fields| ...): Borrows all tail fields mutably and all immutably-borrowed fields immutably at once.

    Warning: References obtained from these methods must not outlive the struct itself.

    // Immutable access
    let val = my_value.borrow_int_data();
    
    // Mutable access to a tail field
    my_value.with_mut(|fields| {
        *fields.some_tail_field = 10;
    });
  10. Handle errors during initialization with try_new

    main

    If your self-referencing field initialization closures can fail, use the try_new family of methods:

    • MyStruct::try_new(...): Returns Result<MyStruct, E>. Used with MyStructTryBuilder and .try_build().
    • MyStruct::try_new_async(...): The asynchronous version of try_new.
    • MyStruct::try_new_or_recover_async(...): If initialization fails, this returns both the error and the successfully initialized head fields, allowing you to recover data.