Description
Proposal
Problem statement
The allocator API is currently unstable. Going through the global {re}alloc
functions is possible, but not as general. Still, all these methods are unsafe
and require careful use. Most of the complexity comes from providing a consistent layout to the different methods for the associated pointer representing the allocation, checking for nullptr and not causing accidental double-free or leakage.
Further, while there is a way to manage an allocation of typed memory through Box<T>
, there is no equivalent for the underlying untyped memory.
While the Allocator API is understandably still unstable and the exact contract with implementations is a bit of a moving target that needs to get stabilized, from a user persective only the unsatisfying alloc
, realloc
and dealloc
methods can be used, which will most likely get deprecated (to my understanding, in favor of the Allocator API) either way in the forseeable future. The biggest pain points on stable are that:
- You don't get notified if the allocator gives you more memory than you requested, potentially leading to superfluous calls to realloc
- Can't (re)allocate 0-sized, this is often a separate codepath in every manual usage of
alloc::allocate
realloc
can't change the alignment of the allocation- raw pointers and layout data are
Copy
and express no ownership, making it necessary to carefully argue about duplication and leaking in panic paths - There is no stable
realloc_zeroed
Most of these are solvable on unstable, though allocator_api
is and was a moving target for the past 8 years with no particular stabilization date in sight. Additionally, the API is still unsafe
. The proposed API is safe and fairly small, trading that for more explicitly tracked data in the added Allocation
type.
Motivating examples or use cases
This PR manually implements buffer management that can't be done with a Vec
due to working with heterogeneous datatypes with potentially different alignment. Similar datastructures are BoxBytes
in bytemuck
(which has conversion from a Box
/Vec
and deallocates but does not support resizing) and multiple questions on stackoverflow et al about over-aligned vectors.
Solution sketch
A new datatype Allocation
in alloc::alloc
that tracks all necessary information to safely use the allocation interface. While this often has a small overhead of some bits of information that is available externally (such as the datatype's alignment and capacity in the case of Vec
), the unsafety requirements can be tracked and checked making this worth it.
// Not Copy
struct Allocation<A: Allocator = Global> {
alloc: A,
ptr: NonNull<u8>,
layout: Layout, // layout that was used to allocate the above
}
impl Allocation {
// Forwards to alloc, handles layout.size() == 0 with a dangling ptr
fn new(layout: Layout) -> Self;
fn into_parts(self) -> (NonNull<u8>, Layout);
unsafe fn from_parts(ptr: NonNull<u8>, layout: Layout) -> Self;
fn layout(&self) -> Layout;
}
impl<A: Allocator> Allocation<A> {
// Guarantees: aligned to requested layout, len >= requested size, reflecting the actual allocation
// Pointer is valid for reads and writes until the next call to realloc or this Allocation being dropped
fn as_slice(&self) -> NonNull<[MaybeUninit<u8>]>;
// Calls either grow or shrink, compares against stored layout
fn realloc(&mut self, new_layout: Layout);
fn realloc_zeroed(&mut self, new_layout: Layout);
}
unsafe impl Sync<A: Allocator + Sync> for Allocation {}
unsafe impl Send<A: Allocator + Send> for Allocation {}
impl Drop for Allocation {
// handles deallocation
}
// Extensions depending on allocator API
#[cfg(allocator_api)]
impl Allocation {
fn try_new(layout: Layout) -> Result<Self, AllocError>;
}
#[cfg(allocator_api)]
impl<A: Allocator> Allocation<A> {
fn new_in(layout: Layout, alloc: A) -> Self;
fn try_new_in(layout: Layout, alloc: A) -> Result<Self, AllocError>;
fn into_parts_with_alloc(self) -> (NonNull<u8>, Layout, A);
unsafe fn from_parts_in(ptr: NonNull<u8>, layout: Layout, alloc: A) -> Self;
fn try_realloc(&mut self, new_layout: Layout) -> Option<AllocError>;
fn try_realloc_zeroed(&mut self, new_layout: Layout) -> Option<AllocError>;
}
Alternatives
All downsides mentioned in the motivation can in theory be worked around with existing API, but an unstable-enabled library like the std
could provide a better and more performant interface than a crate working against stable rust ever could by internally using the yet unstable allocator API. This ACP could be stabilized without commiting to the exact details of the underlying Allocator API.
Very little of the above would be unsafe
API, the alternative for consumers of std
at the moment is writing a lot of unsafe code (which is possible, albeit probably error prone).