This is... surprisingly reasonable, though a little roundabout. This applies equivalently to SingleElementStorage
, and is easier to explain, so I'm going to walk through with that.
Originally I though this would be unreasonable to support, as SingleElementAllocatorStorage<Alloc>
obviously would have a Handle
of ptr::NonNull<T>
, so that Box<T, SEAS<A>>
would decay back to just being { ptr: NonNull<T>, alloc: SEAS(A) }
, which is basically just the current allocator-generic box. However, the storage itself could store the pointer, and provide a Handle
of ()
, so Box<T, SEAS<S>>
would be { handle: (), storage: NonNull<T> }
.
Basically, SingleElementStorage
acts like Box<T>
, and MultiElementStorage
acts like Box<[T]>
(except maybe uninit (maybe), so RawBox
).
HOWEVER, I don't think this is the correct way to handle (no pun intended) it. The Storage
traits should solely care about acquiring/releasing memory when asked. Let (Raw
)Box<T, S>
/Box<[T], S>
be those types that do the dealloc on drop. This simplifies the Storage
s' job immensely, and reduces the cost of providing/implementing Storage
. Additionally, if SingleStorage
isn't in charge of releasing handles and assumes the user does, this allows MultiStorage
to be a simple marker trait extension of SingleStorage
that lifts the "only one live handle" restriction.
Specifically, (assuming there are no further impl restrictions, and I think this is just a slight reorganization of the existing POC traits), I think the right API is something along the lines of (modulo naming bikeshed)
// NB: Blank lines removed for compactness, also I just use usize for simplicity
/// A storage capable of storing single elements.
unsafe trait Storage {
/// The handle used to access stored elements.
type Handle<T: ?Sized + Pointee>: Copy;
/// Acquire a handle managed by this storage.
/// # Safety
/// Only one handle may be live unless this type is `MultiStorage`.
unsafe fn acquire<T: ?Sized + Pointee>(&mut self, meta: T::Metadata) -> Result<Self::Handle<T>, Error>;
/// Release a handle managed by this storage.
/// # Safety
/// This is an unreleased handle acquired from this storage.
/// Invalidates the handle.
unsafe fn release<T: ?Sized + Pointee>(&mut self, handle: Self::Handle<T>);
/// Resolve a handle managed by this storage.
/// # Safety
/// This is an unreleased handle acquired from this storage. The pointer is only valid
/// until the storage is moved or `acquire`/`release` is called (for any handle).
unsafe fn resolve<T: ?Sized + Pointee>(&self, handle: Self::Handle<T>) -> ptr::NonNull<T>;
// helpers and coerce things
}
/// This storage supports multiple live handles.
unsafe trait MultiStorage: Storage {}
/// A storage capable of storing contiguous ranges of elements.
unsafe trait RangeStorage {
/// The handle used to access stored elements.
/// Knows the provided capacity.
type Handle<T>: Copy;
/// Acquire a handle managed by this storage, capable of holding at least `capacity` elements.
/// # Safety
/// Only one handle may be live unless this type is `MultiRangeStorage`.
unsafe fn acquire<T>(&mut self, capacity: usize) -> Result<Self::Handle<T>, Error>;
/// Release a handle managed by this storage.
/// # Safety
/// This is an unreleased handle acquired from this storage.
/// Invalidates the handle.
unsafe fn release<T>(&mut self, handle: Self::Handle<T>);
/// Resolve a handle managed by this storage.
/// # Safety
/// This is an unreleased handle acquired from this storage. The pointer is only valid
/// until the storage is moved or `acquire`/`release` is called (for any handle).
unsafe fn resolve<T>(&self, handle: Self::Handle) -> ptr::NonNull<[T]>;
/// helpers, try_grow, try_shrink, max capacity
}
/// This storage supports multiple live handles.
unsafe trait MultiRangeStorage: MultiStorage {}
(Traits need to be unsafe
. Otherwise a valid impl is no-op acquire
/release
and ptr::null
for resolve
.)
I'm unsure about providing the T
s upfront to be honest, and could go either way. (The current POC requires the T
upfront, my sketch just requires the pointer metadata.) Not requiring the T
is probably better, as you maintain the ability to emplace dynamically sized types (for arbitrary storage). [std::alloc
example]
fn resolve_mut(&mut self, handle: Self::Handle<T>)
is probably required (under current stacked borrows rules), to give mutable provenance to the returned pointer even for inline storages. Either that, or any inline storage that wants to be mutable must use UnsafeCell
on its internals. Everywhere I marked to invalidate pointers is being conservative around SB; I'm not sure if conservative enough tbh. Ultimately, I'm not super confident how inline storages, pointer provenance, and stacked borrows interact, and would need to do further study to gain confidence. RustBelt proved our allocation primitives sound (with std::alloc
); we definitely don't want to accidentally lose that without a very clear way to gain it back.
One thing I'd be curious to know is if it's possible to collapse RangeStorage
of T
into just being a Storage
of [T]
. I'm not sure; this would require more design experimentation to see if it puts undue restrictions on storages to support both single and range allocation simultaneously (rather than, say, providing an impl of RangeStorage
based on {Storage
of [T]
}). IIUC, RangeStorage
handles are currently required to remember the capacity they provide (acquire
returns ptr::NonNull<[T]>
; I took this directly from the POC), rather than passing that responsibility on to the user (which would be a clear reason to split the traits; acquire
would just return ptr::NonNull<T>
).
Maybe it would look as simple as something like...
/// A storage that can more efficiently manage contiguous ranges of elements.
unsafe trait RangeStorage: Storage {
/// Attempt to grow the handle to cover at least `capacity` elements.
/// # Safety
/// Requested capacity is >= current capacity.
/// Invalidates resolved handle pointers.
/// Invalidates the input handle only on success.
unsafe fn try_grow<T>(&mut self, handle: Self::Handle<[T]>, capacity: usize) -> Result<Self::Handle<[T]>, Error>;
/// Shrink the handle to cover at least `capacity` elements.
/// # Safety
/// Requested capacity is <= current capacity.
/// Invalidates resolved handle pointers and the input handle.
/// Output handle may have any capacity between requested and prior capacity.
unsafe fn shrink<T>(&mut self, handle: Self::Handle<[T]>, capacity: usize) -> Self::Handle<[T]>;
}
This extension-style RangeStorage
sketch makes me think that an independent RangeStorage
that resolves to ptr::NonNull<T>
and requires (lets) the user remember the capacity of each handle separately is probably better. (E.g. [MaybeUninit<T>; N]
would just use Handle = ()
and resolve()
=> self.arr.as_mut_ptr()
.
Now I should probably stop discussing this in depth, since IP ownership of stuff I do is murky at best right now. (I will go to SMU legal and get an exception for OSS if I need to but... confrontation,, and the Guildhall people seem to think the agreement doesn't apply to non-coursework anyway,,)