Self-seferencing struct problem because of a "hollow" reference

Hello.

I have two types: Pool and Item. Pool allocates objects of Item, and Pool must never be dropped while any of the Items it allocated is still alive.

To make borrow checker ensure that the requirement is satisfied, Items are allocated with the lifetime of its allocator Pool:

use std::marker::PhantomData;

pub struct Pool {
    // some fields..
}

impl Pool {
    pub fn new() -> Self {
        Self {}
    }

    pub fn allocate(&self) -> Item<'_> {
        Item::new(
            // pass some data, but no references to self
        )
    }
}

// The lifetime specifier is not mandatory since Item doesn't actually reference Pool.
// The lifetime specifier is used to enable borrow checker to ensure that Pool
// is never dropped while there are alive Item objects allocated from the Pool.
pub struct Item<'a> {
    // some fields..
    _marker: PhantomData<&'a ()>,
}

impl<'a> Item<'a> {
    pub fn new() -> Self {
        Self {
            _marker: PhantomData,
        }
    }

    // some methods..

    pub fn hello(&self) {
        println!("Hello, Mom!");
    }
}

Item type doesn't actually reference Pool, but the lifetime of the Pool is used during allocation to ensure that the allocated Item does not outlive its allocator.

Example:

fn main() {
    let pool = Pool::new();

    let item1 = pool.allocate();

    // As desired, we cannot drop the pool while item1 is alive.
    // Error: cannot move out of `pool` because it is borrowed
    // core::mem::drop(pool);

    item1.hello();
}

It worked for a while.
Until a new requirement arrived.

Now I have a type User. User needs to own one (potentially more, but fixed amount, known at compile time) Item. User must not reference any external Pool, so it must have its own.

That's where the problem arises. Item behaves like it references Pool. Because of this the borrow checker will not allow both Item and its Pool to be in the same struct.

This code is what I want to achieve:

pub struct User {
    pool: Pool,
    item: Item<'something>,
}

impl User {
    pub fn new() -> Self {
        let pool = Pool::new();
        let item = pool.allocate();

        Self { pool, item }
    }

    pub fn item_hello(&self) {
        self.item.hello();
    }
}

Since Item doesn't actually have a reference to Pool, the User object would be safe to move.

However, I see there are some problems:

  1. Order of fields destruction in a struct, as per rust documentation, is in the order of their declaration. That means item must be declaratied after pool so that pool is not dropped first, unless there is a feature in the language that would enforce the compiler to drop pool field only after item under any circumstances. I could not find such a feature.
  2. pool could simply be replaced with another pool, and then dropped.

Isn't there some feature in the language that would allow to create "hollow" references used only to enfore the order of objects destruction? Such a reference could enforce the correct order of destruction in the struct (pool is always dropped after item independent of their declaration order), and prevent pool from being replaced (because it is borrowed).

If there is not, maybe there is some standard design pattern in rust that solves such a problem?

No, the only support for such patterns is

  • don’t use it, but
    • always keep the Pool in a separate struct from the Items
    • reach for dynamically checked alternatives; e.g. there could be owned versions of that Item type that keep some Arc<Pool> to make sure the Pool is simply kept alive that way
  • use unsafe (and likely fail to spot some soundness issues)
  • use crates that features safe-to-use macros for self-referencing structs such as self_cell - Rust / yoke - Rust / ouroboros - Rust. (Drawback in this case: They will conservatively force an (with some, implicitly added) additional layer of indirection & allocation to your data, akin to working with a Box<Pool>).

The distinction about “"hollow" references” isn’t particularly special; at least as far as I understand your take here. Many borrows don’t borrow the object directly, but (almost) only care about drop order. E.g. for Box<T>, if you borrow the &T, then the Box itself is not really borrowed directly, and in principle all you matter about is that it’s not dropped before your &T (and that it isn’t ever used mutably anymore… so maybe perhaps Box isn’t the optimal example … maybe… imagine a ReadOnlyBox that only offers read-only access to its target, then drop-order is the only concern that’s left). Memory isn’t unique for this “Foo must be dropped before Bar” kind of dependency and lifetime are an accurate model IMO.

2 Likes

If this is unacceptable because of potential pool poisoning (using the same pool between items or such)...

struct User<'a> {
    pool: &'a Pool,
    item: Item<'a>,
}

impl<'a> User<'a> {
    fn new(pool: &'a Pool) -> Self {
        let item = pool.allocate();
        Self { pool, item }
    }
}

...a GhostCell-like approach may be possible that preserves the invariants (Item<'_> associated with a specific Pool).

// Use module privacy or whatever so that only a `Pool` can
// create an `Item<'_>`

struct UserDataExceptPoolAndItem { /* ... */ }
struct User<'a> {
    data: &'a UserDataExceptPoolAndItem,
    item: Item<'a>,
    // pool: &'a Pool, // if you need it
}

type UDEPAI = UserDataExceptPoolAndItem;

impl Pool {
    fn with_user<F>(self, data: &UDEPAI, task: F)
    where
        F: FnOnce(User<'_>)
    {
        let item = self.allocate();
        let user = User { item, data, /* pool: &self */ };
        f(user);
    }
}
2 Likes

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.