Rust containers and wrappers

Perhaps it's just me having bad luck, but whenever I find a method which does something that I need to understand and I click on the [src] link, I realize that user-facing code for containers in Rust is mostly this:

struct SomeContainer {
  /// you thought you were gonna see the secret sauce here lol
  inner: RawSomeContainer
}

impl SomeContainer {
  fn do_thing(&mut self) -> bool {
    /// not here lmao
    self.inner.do_thing()
  }
}

Randomly curious: Why is this? Why is there so often a wrapper around the actual implementation? I have done this in one of my crates (which isn't a collection), because I noticed it got a lot cleaner to separate the internals into an inner module, but technically I could just have continued to use pub and non-pub to maintain an inner/public separation. Are there any particular reasons that collections seem to do this more often?

It kind of makes me wish rustdoc had an #[rustdoc::actual_implementation_location = "crate::raw::do_thing"] feature, so the [src] link could be forced to point to what the user is likely actually interested in. .. or better yet, that the generated source code linked function/method calls, so one could just click again to reach the secret sauce.

1 Like

Some improvement at least is in the links that (nightly) rustdoc can insert into the code. E.g. the standard library docs hosted on stdrs.dev use this option. Click e.g. the source link of BTreeSet::insert, and you can click your way through BTreeMap::insert's implementation to the implementation of the entry-API methods that are being used underneath.

It's not perfect yet, e.g. DormantMutRef::new in the .entry(..) implementation doesn't have a link for whatever reason, and for HashMap, AFAICT you don't get links to the hashbrown crate, etc.. but with sufficient future improvements, this feature should make the pain of browsing these implementations a lot less while being generally super useful anyways, without any need for new fancy special-case features for "improving" (i.e. altering) the destination of the [source] button itself (which might also be super confusing for some people).

7 Likes

Normally, having all the secret sauce in the SomeContainer is the way to go because it's simpler and easier to maintain, but I can think of a couple more niche reasons to use this wrapping approach.

The top two reasons for using wrappers are to provide a stable, more user-friendly facade over some complex data structure, or in cross-platform code where you have different implementations under the hood (e.g. std::fs::File uses the libc APIs on Linux and WinAPI APIs on Windows) which will be selected using conditional compilation.

Introducing a facade also makes it easier to swap out the underlying implementation without breaking backwards compatibility. For example, the standard library used to have its own implementation of std::collections::HashMap but a couple years ago that was ripped out in favour of using a port of Google's SwissTable.

Another place I've seen this pattern used is when you've got one "main" type which shares all its state with child values.

struct Database {
  inner: Arc<RawDatabase>,
}

struct Transaction {
  db: Arc<RawDatabase>,
  ...
}

This comes up often in FFI because C libraries tend to shove all their shared state into one Context object that gets freed at the end of the application instead of needing to manage the individual lifetimes of each object it creates.

2 Likes

A big part is that it helps to separate responsibilities in many cases.

In Vec, for example, the RawVec is responsible for allocating and freeing the (conceptual) array of MaybeUninit<T>s, while the Vec itself is responsible for keeping track of which of those elements are initialized, and dropping them at the right time.

See RawVec - The Rustonomicon for a long discussion about this.

4 Likes