Modelling interconnected structures

Hi,

I am trying to parse a file that contains serialized C++ objects, and represent them within Rust. The file starts with a list of every object, then the objects themselves can also refer to one another. Ideally, I think I'd like something like this:

struct A {
    // Only simple types
    foo: u32,
    bar: f32,
}

struct B<'a> {
    // Mix of simple types and references
    baz: u32,
    boom: &'a RefCell<A>,
}

enum Classes {
    A(RefCell<A>),
    B(RefCell<B>),
}

struct MyFile {
    objects: Vex<Classes>,
}

That is:

  • The structure representing the file owns the 'real' list of deserialized objects
  • Object types that reference other objects have references to the real version in the file's structure, with RefCells for runtime borrow-checking enforcement (let's say this is all single-threaded, or switch the RefCell for a Mutex)
  • Therefore any modifications to an object, either through the master list in the file's structure, or through a different object within the file by accessing its members, modifies the object stored in the file's structure.

I am really struggling with this however. I can't work out:

  • How to initially construct the file structure. If B needs a reference to A, as we're looping through the objects maybe A hasn't been made yet.
  • How to have B hold a reference to the concrete type, which we know, rather than the enum type.

The enum is not required, but I don't know how else to have a big collection of different object types. I don't think a trait is appropriate, because I want users to be able to access the unique fields of each object.

Thanks for any help! This feels like something that'd be better to do in say C++, but so much of the rest of the project is working really well in Rust (macros to generate the class definitions, compiling to wasm) that it'd be a real shame to have to give up on it now

The difficulty with having Classes own both the As and the Bs when the Bs are pointing at the As is that the data structure has become self-referencial -- it contains a reference pointing at something else it owns. Rust doesn't have meaningful native support for that when using actual references (&_).[1]

You could use Rc<_> instead of references (&_), or use some sort of key you can look things up with instead of references. This is probably what I'd recommend from what you've said so far.

Option<RcOrKey<..>> or Weak instead of Rc or a dummy sentinel Rc are possibilities for this part in particular. You could also perhaps have a building type and a finalized type (without the Options or Weaks).


Alternatively, you could split up your ownership so the owner of Bs is not the owner of As, so that things are no longer self-referencial. But you may get a crash course in Rust lifetimes and borrow-checking even if that way is workable.

There are also some third-party crates like yoke and ourorboros that aim to provide sound methods to create self-referencial structs.[2] I haven't tried them out with large self-referencial structures,[3] so I'm not completely sure if they'd apply or not.


  1. If you own something, you generally have the ability to move it and to create exclusive references (&mut _) to all the parts of it, which conflicts with various fundamental guarantees: no dangling references, &mut _ is exclusive, etc. ↩︎

  2. I haven't checked lately to see what outstanding soundness problems they have. ↩︎

  3. just things like "one field is a reference pointing at another field" ↩︎

Here's a quick example of what I meant by that.

It's probably worth elaborating on avoiding shared ownership since it's the generally preferred approach in Rust. One simple option for that here is:

struct File {
  a_data: Vec<A>,
  b_data: Vec<B>,
  objects: Vec<Object>,
}

enum Object {
  A(usize),
  B(usize),
}

then use indices instead of references.

You can tidy this up a bit by wrapping the indices in new types, etc., but this is often what it ends up looking like (perhaps using a HashMap if the persisted format has native IDs already)