Skip to content

Proposal: Storage-backed Allocator #93

Open
@DecoyFish

Description

@DecoyFish

Here's another take on enabling a storage-backed Allocator trait with minimal changes to the current implementation. This is inspired by @matthieu-m's awesome storage-poc (see also #79).

Locally, I've prototyped a rough Box and Vec implementation along with several Allocator impls to gain a base level of assurance. However, I'm somewhat new to both Rust and this feature so I might have overlooked key issues. Please also let me know if I have or if this (or a similar) proposal has already been considered and rejected/deferred.

Objectives

  • Enable building the following:
    • static_box::Box - inline-only allocation of unsized boxed items
    • SmallVec - inline/heap fallback allocation (both union and enum variants)
    • ThinArc - Pointee::Metadata (and potentially other items) stored at the start of the allocation (with only a thin pointer exposed)
  • Do not leak pointers to unstable memory locations
  • Do not depend on GAT stabilization
  • Zero cost abstraction (pay only for what you use)
  • Require minimal changes to the current proposal
  • Keep Allocator trait object safe (this was only partially successful - Allocator<Buffer = NonNull<u8>> is object safe, which appears to still meet the spirit of the request in Runtime allocators #83)

Issues

Currently it faces the same limitation as @matthieu-m's storage-poc: the Box implementation is not coercible because a type can't effectively implement CoerceUnsized while containing <T as Pointee>::Metadata. This issue is being discussed in rust-lang's #81513.

While not blocking, it would reduce the required constraints in data structure implementations if Pointee::Metadata required Metadata<Self>.

Proposal

// There are a few key changes:
//   - Associated type Buffer.
//   - Because Self::Buffer has no direct concept of its own layout, each (re-)allocation also
//     returns a Layout. Callers can leverage this to learn how much extra space (if any) was
//     allocated.
//   - Because Self::Buffer is uniquely-owned, it consume it on (de-)reallocation.  Relatedly, it
//     is also returned in the Err on reallocation failure.
//
// SAFETY concerns for this trait should match those of the current trait.
pub trait Allocator {
    type Buffer: Buffer;

    fn allocate(&self, layout: Layout) -> Result<(Self::Buffer, Layout), AllocError>;

    unsafe fn deallocate(&self, buffer: Self::Buffer, layout: Layout);

    unsafe fn grow(
        &self,
        buffer: Self::Buffer,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<(Self::Buffer, Layout), Self::Buffer>;

    unsafe fn shrink(
        &self,
        buffer: Self::Buffer,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<(Self::Buffer, Layout), Self::Buffer>;

    // OMIT: other methods like *_zeroed, as_ref...
}

// From initial prototyping (and some godbolt usage), this appears to be inlined quite successfully
// - in keeping with the goal of zero cost abstractions.  Let me know if anyone foresees issues
// with this, though.
pub trait Buffer {
    // SAFETY: Proven<T, M>::layout() must 'fit' the buffer (see Allocator trait for 'memory fit').
    unsafe fn as_mut<T: ?Sized, M: Metadata<T>>(&mut self, metadata: Proven<T, M>) -> &mut T;

    // SAFETY: Proven<T, M>::layout() must 'fit' the buffer (see Allocator trait for 'memory fit').
    unsafe fn as_ref<T: ?Sized, M: Metadata<T>>(&self, metadata: Proven<T, M>) -> &T;

    // Used in Allocator implementations - particularly for copying from one buffer to another
    // during reallocation in an intermediate/fallback Allocator.
    //
    // SAFETY: The pointer is only valid during F's invocation.
    unsafe fn with_ptr<F: FnOnce(NonNull<u8>)>(&mut self, layout: Layout, apply: F);
}

// This trait is strictly additive and can be added later (or not at all).  It is used to build
// things like ThinArc in which the Metadata is stored at the start of the allocation.  Using this
// trait we can (1) constrain allocators to have ThinBuffers and (2) read the sized "head"/metadata
// out of the buffer without knowing the layout of the buffer.
//
// These methods are not included in Buffer because not all Buffer implementations will support
// them.  Consider a union-based Buffer (like the one used in SmallVec's union feature).  In order
// to know which union variant is valid, we must first know the capacity (metadata) of the Buffer.
// That said, most Buffer implementations will likely be capable of implementing this trait.
pub trait ThinBuffer: Buffer {
    unsafe fn as_mut<T>(&mut self) -> &mut T;

    unsafe fn as_ref<T>(&self) -> &T;
}

mod metadata {
// Provides layout and metadata information for a given type.  Note the following:
//  - Might not contain a valid layout (this can happen if size overflows on slice metadata).
//  - Might be a type other than T::Metadata (e.g., a Vec with max 256 elements might store
//    allocation's size/capacity in a u8).
pub trait Metadata<T: ?Sized + Pointee>: Copy {
    fn try_layout(self) -> Result<Layout, LayoutError>;

    fn metadata(self) -> T::Metadata;
}

// Wraps Metadata<T> which have a known valid Layout.  Consequently, subsequent layout checks can
// be unchecked.  The key is that this type can only be constructed via conversion from valid-
// Layout metadata sources.
pub struct Proven<T: ?Sized, M: Copy>(M, PhantomData<fn(T) -> T>);

impl<T: ?Sized, M: Metadata<T>> Proven<T, M> {
    pub fn fatten(self, ptr: NonNull<()>) -> NonNull<T> {
        NonNull::from_raw_parts(ptr, self.0.metadata())
    }

    pub fn layout(self) -> Layout {
        unsafe { self.0.try_layout().unwrap_unchecked() }
    }
}

// OMIT: From impls for Proven to convert from &<T as Pointee> and Layout where
//       T: Sized + Pointee<Metadata = usize>
}

PS: I am not good at naming. My dog's fairly lucky that he didn't end up named "Dog". Please let me know if you have any suggestions regarding better names.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions