generic-container 0.2.1

Abstract over "containers" that hold a T, such as a T itself, Box<T>, or Arc<Mutex<T>>
Documentation

Latest version Documentation Apache 2.0 or MIT license.

Abstract over how a T is stored and accessed by using generic "containers", bounded by the container traits here. A container owns a T and can provide references to it, or be consumed to return the inner T (if T is Sized).

Some containers are fallible, or can only provide immutable references and not mutable references. The container traits are meant to be specific enough that a generic container can be bound more tightly by the interface it needs, to support more container implementations.

Skip to here for a list of the provided container implementations.

Motivation

Someone who only needs to use your type from a single thread, will never clone it, and will only use a single concrete type for a generic or dyn will likely have very different preferences from someone who wants to use your type from multiple threads and instantiate a bulky generic type with many combinations of generic parameters.

This concern is likely not even applicable for Clone + Send + Sync types that have no generics, and in the other direction, the two use cases may simply be far too different to cleanly unify.

But when "how will this T be stored?" is a major blocker against flexibly supporting both sorts of uses in a performant way, this crate can help. The traits here allow a type to abstract over how something is held and accessed by using a generic "container", and blanket implementations for Trait can implement Trait for any container wrapping dyn Trait, or effectively T: Trait in general with the help of a container wrapper type [^blanket-container-t].

Choices like these can be deferred to the consumer of your types (or traits):

  • Is the tradeoff between atomic refcounts and Send or Sync worth it?
  • Will the consumer never clone your type, or is it necessary to be able to cheaply clone something with a copyable or refcounted container (which could be signaled by the container implementing Copy or dupe::Dupe, or perhaps by the container implementing Clone even when the contained type is not Clone)?
  • Will the impact of monomorphized generics on the binary size outweigh their better optimization compared to dyn, or are some of the concrete types involved not known until runtime? If so, a user may prefer or need a dyn trait object, and from the first two bullets, maybe a user would need Rc<dyn Trait> rather than Box<dyn Trait>, and in general may want something like Container<dyn Trait>[^container-dyn-trait].

Particular traits worth abstracting over are Clone (or a cheap clone like dupe::Dupe in particular), Send, and Sync.

Additionally, trait authors may wish to allow any generics bound by your traits to optionally use dyn instead of monomorphizing everything; a standard way to allow for this is to implement YourTrait for Box<dyn YourTrait> and perhaps a few other smart pointers, but even better is providing a blanket implementation for YourTrait for any C: ?Sized + Container<dyn YourTrait> [^container-dyn-trait].

Example

use generic_container::FragileContainer;

trait MyTrait {
    fn calculate_something(&self) -> u32;
}

// Whenever something is generic over `MyTrait`, thanks to this blanket impl, we could opt into
// using `Box<dyn MyTrait>`, `Arc<dyn MyTrait>`, and so on. This way, being generic over
// `MyTrait` gives the user a strict superset of the abilities they'd have if we only used
// `dyn MyTrait` internally.
impl<C: ?Sized + FragileContainer<dyn MyTrait>> MyTrait for C {
    // You should warn users like this when depending on `FragileContainer`.
    // Note that someone solely using the `MyTrait` interface could not encounter this problem:
    // any `FragileContainer<dyn MyTrait>` is a perfect drop-in for a `MyTrait`.
    //
    /// Calculate something using the internal `MyTrait` implementation.
    ///
    /// # Fragility: Potential Panics or Deadlocks
    /// If `C` does not also implement `Container` and there is an existing borrow from `C`
    /// at the time `calculate_something` is called, a panic or deadlock may occur.
    ///
    /// See [`FragileContainer::get_ref`].
    #[inline]
    fn calculate_something(&self) -> u32 {
        self.get_ref().calculate_something()
    }
}

struct NeedsToCalculateSomething<T: MyTrait>(T);

// This is effectively the same thing you'd get if you used a `dyn` object instead of a generic
// above; the user of your struct loses nothing.
type NeedsToCalculateSomethingDyn = NeedsToCalculateSomething<Box<dyn MyTrait>>;

Container Traits

Eight main container traits are provided here, with every combination of the following aspects:

  • Mutability: whether a container can provide mutable references to the value stored in the container, in addition to immutable references.
  • Fallibility: a container is fallible if try_get_ref or try_get_mut can fail, and get_ref or get_mut cannot be provided.
  • Fragility: a fragile container is not guaranteed to support reentrancy, and may panic or deadlock if try_get_ref (or similar) is called by a thread that already has a live reference to the value stored in the container.

Mutability is indicated by the prefix Mut, fallibility by the prefix Try, and fragility by the prefix Fragile. They are combined in the order "FragileTryMutContainer".

There are also two de facto trait aliases which replace "FragileTry" with "Base" in the two FragileTry*Container traits.

Except for the Base*Container traits intended as aliases, implementors of the traits must manually implement each of them; there are no blanket [Container] implementations for TryContainer types whose error types are uninhabited, for instance.

When bounding a generic by a container trait, you should generally bound by the minimum container interface you need, plus any other marker trait needed, like Clone (or a cheap clone like dupe::Dupe), Send, and Sync. Conversely, you should bound the T which the container is required to store as tightly as you can, especially by Sized, Send, and Sync.

Fragility: Potential Panics or Deadlocks

The Ref associated types of some container implementations (and RefMut for mutable containers) may have nontrivial Drop impls that interfere with other attempts to borrow from the container, and possibly from other clones of the container referencing the same inner data. Such a container might be "fragile", unless it implements additional container traits indicating that it is not.

Being "fragile" means that a single thread should not attempt to get multiple live references to the T in the container, whether from the same container instance or clones referencing the same inner T. Doing so risks a panic or deadlock (such as in the case of Arc<Mutex<T>>). In other words, the fragile container traits do not guarantee that the container handles reentrancy gracefully. A thread should drop any borrow obtained from the fragile container's methods accessing its inner T before a new reference to the inner T can be obtained without any risk of panic or deadlock.

Containers

Common examples of containers include T itself, Box<T>, Rc<T>, Rc<RefCell<T>>, Arc<T>, Arc<RwLock<T>>, and Arc<Mutex<T>>.

Additionally, container traits are implemented for [CheckedRcRefCell<T>] (from this crate) and Arc<ThreadCheckedMutex<T>> (when the thread-checked-lock feature is enabled).

Note that CheckedRcRefCell<T> and Arc<ThreadCheckedMutex<T>> essentially shift how the runtime invariants of a RefCell or Mutex are enforced; with a fragile implementation, the user is required to enforce them (on pain of panics or deadlocks), while with a fallible implementation, such usage will still fail, but not fatally. Defaulting to the fragile implementations is likely the best choice.

Some container implementations choose to panic if a poison error is encountered, as a poison error can only occur if another thread has already panicked.

Other crates may implement container traits for their own types.

Provided Container Implementations

This crate provides the following container implementations:

  • For MutContainer<T> (and its supertraits):

    • T itself
    • Box<T>
  • For Container<T> (and its supertraits):

    • Rc<T>
    • Arc<T>
  • For FragileMutContainer<T> (and its supertraits):

    • Rc<RefCell<T>>
    • Arc<RwLock<T>> (implementation may panic on poison)
    • Arc<Mutex<T>> (implementation may panic on poison)
  • For TryMutContainer<T> (and its supertraits):

    • CheckedRcRefCell<T>
    • Arc<ThreadCheckedMutex<T>> (only if the thread-checked-lock feature is enabled)

Container Kind Traits

Currently, Rust doesn't allow bounds like where C: for<T: Send> Container<T> + Clone + Send + Sync. The solution is to define an extra trait with whatever you need as the bounds of a GAT (generic associated type):

use generic_container::FragileMutContainer;
use dupe::Dupe;

pub trait NeededContainerStuff {
   // Implementors should use `T: ?Sized` when possible. But right now, the only way to create,
   // for example, `Arc<Mutex<[T]>>` is via unsizing coercion from `Arc<Mutex<[T; N]>>`;
   // as such, a `T: ?Sized` bound would be somewhat useless without also requiring the
   // container to support unsizing coercion, which currently requires nightly-only traits.
   type MutableContainer<T: Send>: FragileMutContainer<T> + Dupe + Send + Sync;
}

If some data needs thread-safe mutability, but you don't want to pay the cost of a lock for read-only data, you can use multiple GATs:

use generic_container::{FragileMutContainer, Container};
use dupe::Dupe;

pub trait NeededContainerStuff {
   // E.g.: `Arc<Mutex<T>>`, or something `parking_lot`-based
   type MutableContainer<T: Send>: FragileMutContainer<T> + Dupe + Send + Sync;
   // E.g.: `Arc<T>`
   type ReadOnlyContainer<T: Send + Sync>: Container<T> + Dupe + Send + Sync;
}

Such a trait is called a "container kind trait" (with implementations being "container kinds", just as implementations of the container traits are "containers"). Relevant characteristics for a container kind's container include the eight container traits, Send + Sync bounds (possibly only when T is Send + Sync, or just Send), Dupe, whether the GAT allows T to be unsized, and Debug bounds.

Not every conceivably-useful container kind trait is provided, as there would be exponentially-many such traits. Note also that creating simple container kind traits that can be combined into more complicated bounds does work, but not well. A container kind should set the GAT of each container kind trait it implements to the same container type; this can be asserted or required with Rust's type system, but the trait solver doesn't understand it very well. Such container kind traits would likely not be pleasant to use.

When the kinds feature is enabled, container kinds and container kind traits for common sorts of containers are provided. They do not use Dupe bounds, and do not mess with type-equality shenanigans that confuse the trait solver.

Features

  • std: enables support for Arc<Mutex<T>> and Arc<RwLock<T>>. Enabled by default. Implies the alloc feature.
  • alloc: enables container implementations based on Box, Rc, Arc, and RefCell, including CheckedRcRefCell. Without alloc, the container traits and GenericContainer are still available, and T is a container for itself. Enabled by default.
  • kinds: provides several container kinds and container kind traits (see above).
  • thread-checked-lock: if enabled, TryMutContainer<T> is implemented for Arc<ThreadCheckedMutex<T>>. Implies the std feature.
  • serde: derives Serialize and Deserialize for GenericContainer and, if alloc is enabled, CheckedRcRefCell.

MSRV

Rust 1.85, the earliest version of the 2024 edition, is supported.

License

Licensed under either of

at your option.

[^blanket-container-t]: To satisfy the trait solver and avoid conflicting trait implementations, a GenericContainer<T, C> struct is provided. It is not necessary for blanket implementations of YourTrait for any container holding dyn YourTrait, but should be used for blanket implementations for GenericContainer<T, C> for any container C holding a T: ?Sized + YourTrait, or similar. [^container-dyn-trait]: Container<dyn Trait> might not be the best choice; [FragileContainer] is preferred if possible. Just as functions are encouraged to take FnOnce or FnMut callbacks rather than Fn (if possible), it would be best to accept a fragile container, if the Trait's methods don't expose some potential for reentrancy with the container holding the dyn Trait. For example, if any of a ReentrantTrait's methods take a &self parameter and take inputs that could, potentially, have some way of getting a reference to the wrapping [FragileContainer], then implementing such a method of ReentrantTrait for FragileContainer<dyn ReentrantTrait> would likely begin by calling get_ref on the container. Then, the other inputs of the method could call get_ref on their reference to the container. Containers which are actually fragile (and don't implement TryContainer) are probably refcounted and cloneable, so changing &self to &mut self doesn't help: if ReentrantTrait provides a user an opportunity to run arbitrary code inside one of its &self or &mut self methods, there's a potential problem. As such, while FragileContainer<dyn ReentrantTrait> could be used if you are careful, it would not work in every situation that the ReentrantTrait interface would normally require; therefore, a blanket implementation of ReentrantTrait for anything implementing Container<dyn ReentrantTrait> could be provided, but doing so for FragileContainer<dyn ReentrantTrait> would make it easy to hand a fragile container to something expecting a normal ReentrantTrait, leading to potential panics or deadlocks.