Description
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.