Introduce the Store API for great good. - #3446
matthieu-m wants to merge 11 commits into
Conversation
The Store offers a more flexible allocation API, suitable for in-line memory store, shared memory store, compact "pointers", const/static use, and more. Adoption of this API, and its use in standard collections, would render a number of specialized crates obsolete in full or in part, such as StackFuture.
CAD97
left a comment
There was a problem hiding this comment.
I'm glad to see this move forward, and apologize that I wasn't able to drive it like I said I was planning on doing. I'm mostly on board with the current direction, though I have a bunch of notes/thoughts I've put inline.
cc @rust-lang/wg-allocators
| - Add a separate `SharingStore` trait -- see future possibilities. | ||
|
|
||
| It should be noted that `dyn` usage of `Allocator` and `Store` suffers from the requirement of using unrelated traits as | ||
| it is not possible to have a `dyn Allocator + Clone + PartialEq` trait today, though those traits can be implemented for |
There was a problem hiding this comment.
Multiple trait vtables are coming "soon," along with vtable upcasting.
I am now thinking we might potentially want a separate trait ErasedStore from the trait Store for the purpose of dynamic storages, to shim ErasedHandle in and potentially to provide is_sharing_with and clone functionality. Making Store deliberately not object safe initially might thus be desirable, until we figure out how dynamic storages "should" work.
| Since those extra capabilities can be brought in by user traits for now, I would favor adopting a wait-and-see approach | ||
| here. |
There was a problem hiding this comment.
It's not exactly trivial to add post facto, since all storages would need to provide the "potentially limited storage" API for it to be meaningfully useful. But combined with pattern refined types, I think adding to Store something like:
pub trait Store {
const MAX_STORAGE: usize = isize::MAX as usize;
const MIN_STORAGE: usize = 0;
}should get the job done, since you can have e.g.
struct Vec<T, S> {
handle: S::Handle,
len: usize @ 0..={S::MAX_STORAGE / size_of::<T>()},
cap: usize @ {S::MIN_STORAGE / size_of::<T>()}..={S::MAX_STORAGE / size_of::<T>()},
store: S,
marker: PhantomData<Box<[T]>>,
}and that should hopefully theoretically be able to at least optimize for single-valued cap and cap/len which are always less than uNN... though also maybe not, depending on coercions (e.g. (&usize @ PAT) as &usize); pattern restricted types might just give you niches, not size compression. Interim solutions that use other solutions should definitely be left to 3rd party, since that type jujitzu is definitely beyond anything else std has exposed. (The Store trait hierarchy is already close to if not the most involved API in std, even after the massive simplifications v3 gives over v2 and v2 gives over v1.)
There was a problem hiding this comment.
I'm still not convinced that this would the right point of customization, to be honest.
For example, I regularly use u32 for length (and u8 for capacity, as the exponent of a power-of-2) even with the Allocator API not because the Allocator cannot provide more, but because I don't care about supporting more.
In fact, my implementation of InlineVec uses u16 regardless, since I have uses for more than 255, but not use over 64K elements.
Requiring to wrap the store into an adapter that limits the sizes every time you wish to save up some bytes seem more complicated than directly plugging the types you want to use in the first place.
This comment was marked as off-topic.
This comment was marked as off-topic.
|
I think CAD has already covered most of my thoughts on the API, so I just want to say: I'm very happy to see this initiative moving forwards! |
| type Handle: Copy; | ||
|
|
||
| /// Returns a dangling handle, always invalid. | ||
| fn dangling() -> Self::Handle; |
There was a problem hiding this comment.
Would it make sense to take a Layout or ptr::Alignment in dangling, so that Vec::new can use dangling to get the 0-length slice pointer?
There was a problem hiding this comment.
That's a good question.
My intent was for dangling handles to always be invalid, and therefore to be UB to do anything with it, including resolving it. For example, I hoped that an index could use a giant value, which resolve would be able to detect was out of bounds in Debug.
Needless to say, this doesn't mesh well with Vec::as_ptr, which is then used for 0-length slices. It requires Vec to introduce a branch in as_ptr to choose between resolving or returning a dangling pointer, and that branch may not be well-optimized at all... it's all around sad.
This leaves two choices, as far as I can see:
- Alter the invalidity of dangling handle, and state that they must be resolvable, though the resolved pointer itself is then invalid. It would still be invalid to attempt to grow/shrink/deallocate a dangling handle, however.
- Require
Vecto use a 0-sized allocation, and implementers to handle those.
I believe (1) is clearly the superior alternative here, so I'll go ahead and amend the RFC in that direction in the next batch of fixes, and mention why in the alternatives section.
There was a problem hiding this comment.
So... I went ahead and tried having dangling take a ptr::Alignment argument, and there was an unforeseen circumstance: it became fallible.
I created a branch on the companion repository which I invite you to look at. The problem I face there is that inline stores cannot provide an allocation with an alignment greater than their own as allocations are always offsets within the store itself. In turn, this makes Vec::new() fallible, or in the companion repository branch it makes LinkedList::new() and SkipList::new() fallible.
This result, in turn, in making dangling fallible since one may (accidentally, I suppose) ask for an alignment greater than what the Store may be able to provide, even in the absence of actual allocation.
I am quite unsure of how to move forward on this.
There was a problem hiding this comment.
In #3446 (comment) @CAD97 raised the possibility of an allocate_dangling method.
It would make the interface more complex, but we could have:
Store::invalid: an invalid handle, it should never be resolved, deallocated, grown, nor shrunk.Store::allocate_zero_sized: a specialized allocation method for ZSTs. The handle is valid, and I would argue should be treated like any other handle: it may be resolved, deallocated, grown, etc... (it can't be shrunk). Multiple independent ZST handles may resolve to the same memory address. Unlike CAD97's proposal I'd argue it must be deallocated ultimately.
This would help making LinkedList::new and SkipList::new infallible again, as they could use Store::invalid. It would not, unfortunately, help Vec::new be infallible as Store::allocate_zero_sized would remain fallible due to the alignment argument.
There was a problem hiding this comment.
Unlike CAD97's proposal I'd argue it must be deallocated ultimately.
This is a problem for implementations. It also leads to the situations we see where both sides of the allocation interface end up checking for ZSTs and having to special case them (See my links to the ZST allocation issues with the current API elsewhere in the thread). While that's less detrimental for Stores (Store is probably not going to be frequently, if at all, used via virtual dispatch), it's still a bad sign IMO.
There was a problem hiding this comment.
an unforeseen circumstance:
danglingbecame fallible.
Fallibility wasn't unforseen on my end; my allocate_dangling did return Result to allow failure to allocate if the alignment request cannot be fulfilled. It's fine if Vec::new can fail and call handle_alloc_error, imho; the entire collection interface is built around assuming that allocation is essentially infallible. A try_new_in (or whatever solution is used for fallible allocation in collections) would be necessary instead to avoid termination on exhausting the storage.
The ability to skip deallocating an allocate_dangling handle is key to it being any amount of realistically useful. Without that guarantee, it's unambiguously better to just never use the allocator to create zero-sized allocations.
Note that my spitball still allowed zero sized allocations in the standard allocate path, requiring deallocation to prevent leaks, with the expectation that this allocation would be permitted to use the exact same path as non-zero-sized allocation and be wasteful for zero-sized allocation which can conjure a fresh allocation at any nonzero aligned address.
Storeis probably not going to be frequently, if at all, used via virtual dispatch
It'd be used dynamically essentially exactly as much as Allocator would, since it's replacing Allocator as the storage generic axis for collections. FWIW, I fully expect dyn GlobalAlloc will always remain the interface used for #[global_allocator], and changing it to be dyn Allocator to be unnecessary and of no real benefit. It makes sense that the API tradeoff calculus would be different for the globally available static allocation interface. (Though automatically bridging from Allocator to GlobalAlloc is both reasonable and desirable.)
There was a problem hiding this comment.
It's fine if
Vec::newcan fail and callhandle_alloc_error, imho; the entire collection interface is built around assuming that allocation is essentially infallible.
I am coming around to thinking this may be fine.
By defining type InlineVec<T, const N: usize> = Vec<T, InlineSingleStore<[T; N]>>; the type is guaranteed to have an infallible dangling, and thus users of the abstraction will never see a panicking new.
Performance-wise, this would rely on dangling being inlined in new, so the optimizer can see the error path is never taken and optimize out any check/call, but that can be tamed.
There was a problem hiding this comment.
It's fine if Vec::new can fail and call handle_alloc_error, imho; the entire collection interface is built around assuming that allocation is essentially infallible
I'm not sure this is fine, it seems quite unfortunate to me. Note that Vec::new is currently const. Obviously that only applies to the global allocator (or whatever default store is used for Vec), but it seems like quite a drawback to prevent things like Store-based smallvec/arrayvec/etc from having const fn news (which I can say very concretely is something desirable).
There was a problem hiding this comment.
but it seems like quite a drawback to prevent things like
Store-based smallvec/arrayvec/etc from havingconst fn news.
Note that handle_alloc_error is (unstably) const, so it won't prevent InlineVec::new from being const.
On the other hand, Rust doesn't support marking individual trait functions const for now, so we may need to lobby the compiler team or otherwise find a clever work-around for Store::dangling.
|
What does the indirection of having Handle types give us that we don't get from just using raw pointers? Is this mostly to support remote allocation (e.g. GPU resources?) I'm not sure that this is a great way to support such scenarios, and it seems to significantly complicate the API surface. |
Everything! :) It enables in-line stores, to start with, by which I mean a store which returns pointer in the memory range spanned by The same self-referential problem appears in Handles also allow, roughly in order of importance (for me!):
Or in short, yes there's an inconvenience, which I find slight in front of all the I note that all the usecases mentioned here are already part of the Motivation section of this RFC: do you have any suggestion to tweak/rewrite the Motivation section to make it clearer? |
|
Hm, that's fairly compelling, thanks. |
|
I'm cautiously optimistic about this API. I like the way it works, I like the flexibility of it, and I'm hopeful it'll really improve things. Initially, I was worried about the amount of unsafe code still required to make simple array-based storage work, but then I remembered that Honestly, I wonder if there's any merit to talking about "levels of unsafety" considering how UB is itself defined to be maximum unsafety regardless. Either way, I wouldn't be upset to see this implemented as-is. |
CAD97
left a comment
There was a problem hiding this comment.
What I currently see as blocking concerns (discussed in above reviews, I'll hopefully edit in direct links later):
- A resolution for
dangling/Vec::newgetting a handle resolvable to a zero-sized allocation, while still guaranteeing that no storage is actually reserved yet - Either a known way to ensure
Boxretains its retag behavior or signoff that this meansBoxdoesn't cause retags / doesn't getnoalias - Further investigation showing that the cost of
&mutmethods outweighs the opportunity cost of requiring all storages with inline state to be!Freeze(and thus making the container using them!Freezeeven if it doesn't utilize the shared mutability) - Added discussion of what happens with the
Allocatortrait; integrating it as part of theStoretrait hierarchy seems preferable to making it essentially an "impl alias."
|
One note about the Allocator trait is that |
Nothing else does right now. I specifically switched to taking
There would be overhead, yes, however it wouldn't be a "surprise", and therefore it's something that can be coded against. For example, if you have a Another possibility is to introduce a further trait which eschew calling the store when the handle is self-resolvable ( trait StoreHandle: Copy {
default fn resolve<S>(self, store: &Store) -> NonNull<u8>
where
S: Store<Handle = Self>,
{
store.resolve(self)
}
}
impl<H> StoreHandle for H
where
H: TryInto<NonNull<u8>> + Copy,
{
default fn resolve<S>(self, store: &Store) -> NonNull<u8>
where
S: Store<Handle = Self>,
{
self.try_into().unwrap_or_else(|| store.resolve(self))
}
} |
This works for a vector/string but not something like a map. It also only works with read-only interfaces. Note that with allocator you can add things to and remove things from the vector with no cost unless you trigger reallocation. |
|
This specific reply wanders pretty far off topic, but I don't think the resolve overhead for The key feature is (unproposed) final trait methods where default method bodies are currently allowed. If a trait method's default is final, it would not be allowed to be replaced by the concrete Pair that with a How practical getting those features are, though, as well as if it's desirable to block stabilization of (at least the Even if it can't be done automatically, a wrapping |
Isn't this the main purpose of I would imagine that using dynamic dispatch for something else would be ill-advised precisely for the performance issues you mention. I can't see why something like an array-based |
|
Right, but I couldn't use that in that way with Vec, could I? At that point, you're back to implementing data structures by hand yourself. |
* Changes: - Rename all XxxStore traits to StoreXxx. * Motivation: As proposed by @CAD97, this establishing a naming convention in which: - StoreXxx are the traits of the API. - XxxStore are the types implementing the API.
It will depend how efficient you want things to be. Firstly, if you use Then, it's just a matter of implementing an adapter struct DynStore<'a, H>(&'a dyn Store<Handle = H>);
unsafe impl<'a, H> const StoreDangling for DynStore<'a, H>
where
H: Copy + From<NonNull<u8>>,
{
type Handle = H;
fn dangling(&self, alignment: Alignment) -> Result<Self::Handle, AllocError> {
let handle: NonNull<u8> = /* magic */;
Ok(handle.into())
}
}
unsafe impl<'a, H> Store for DynStore<'a, H>
where
H: Copy + From<NonNull<u8>> + Into<NonNull<u8>>,
{
unsafe fn resolve(&self, handle: H) -> NonNull<u8> { handle.into() }
fn allocate(&self, layout: Layout) -> Result<Self::Handle, AllocError> { self.0.allocate(layout) }
// ... forward all other methods ...
}And that's it! Just parameterize your If really you somehow got yourself stuck with a If the local increase of memory usage is a problem, it's also possible to use a non-Send store adapter which stores the mapping from handle to pointer (and back) into a thread-local store, then returns the pointer as a handle directly. The mapping only would have to be consulted on allocation/deallocation, so its overhead should be relatively minimal. Anyway, before I start pulling any more hare-brained schemes, I think I've made it clear that there are many solutions that do NOT involve re-implementing collections: the whole point of the Store API is to avoid having to do so, after all. |
* Changes: - Introduce StoreDangling, as super-trait of Store. - Make dangling take an alignment, it becomes fallible. * Motivation: A separate trait is necessary for `dangling` to be usable in const contexts even when the entire `Store` implementation cannot be const. An alignment is necessary for efficient use in `Vec`, which takes advantage of NonNull::dangling being aligned today.
I added the The reasoning for a separate trait is laid out in the RFC, but in short, it's not possible to have a
Given that you used |
|
I don't think I saw anything about being able to use There is a very interesting experiment going on with the /// Defaults to current allocator style of panic on OOM
trait Allocator {
type Result<T> = T;
fn convert_result<T, E>(res: Result<T, E>) -> Self::Result<T> {
match res {
Ok(val) -> val,
Err(e) -> handle_alloc_error(e),
}
}
}
impl Alloc for FallibleAllocator {
type Result<T> = Result<T, AllocError>;
fn map_result .// ..
}
// Resolves to `()` for infallible, `Result<(), AllocError>` for fallible
impl<T, A:Alloc> Vec<T, A> { fn push(val: T) -> A::Result<()>; }
impl<T, A: Alloc> Box<T, A> { fn push(val: T) -> A::Result<Box<T,A>>; }I think support for some sort of behavior like this may be fairly crucial for this proposal since it is intended to work in a lot of places where allocation can fail, but I am unsure what the best solution is that would be. Or if I might have just missed something in the RFC. |
|
I should slightly revise what I said: really So there's potentially a non-interior-mutable API that works, but it'd probably involve pinning. Which makes sense I think; if your storage type stores data in-line then there is actual aliasing that needs to be handled, but if it stores data behind a pointer indirection then things become a lot less critical. Whether it's worth using
Right, that seems to be the main point... specifically this is about whether the |
Indeed, the issue is for The "multi allocations" collections (BTree{Map|Set}, LinkedList, ...} will necessarily require The "single allocation" collections (Box, Vec, VecDeque, Hash{Map|Set}, ...) could benefit from a special Hopefully I'll get some time next week-end to switch from Thank you very much for your clarifications. |
|
Hey @matthieu-m! Thanks for the lightning-fast reply. I took some time to investigate most of the things further, so sorry for the delay. Error handling & ResizingFair enough. I just wanted to give alternatives to things that were talked about. Maybe I'll see if other people proposed similar things and help there :) Lifetimes of pointed memoryI think I didn't explain myself well enough for this section. Let's imagine that you retrieve a pointer using a handle from a store. This pointer may be invalidated by any reason outlined in this RFC. We could say that it has a lifetime IMHO this is not be possible: some stores may share the handle and the same pointer may be retrieved in one thread and invalidated by another. Because of that, it's impossible to know the exact lifetime since even IO could change it. Thus, I believe the guarantees should be the responsibility of the abstractions on top of the store. But now let's define a lifetime We can build a type
'a ─────────┐
'b ───────────────────┐
'static ─────────────────────────────┐
┌────────┬─────────┬─────────┐
Pointer │ unsafe unsafe unsafe │
└────────────────────────────┘
┌────────┬─────────┬─────────┐
Reference │ valid │ invalid invalid │
└────────┴───────────────────┘
┌────────┬─────────┬─────────┐
AccessGuard │ unsafe unsafe │ invalid │
└──────────────────┴─────────┘Note that Mutability, sharing, and multithreadingThere are 3 orthogonal problems we are trying to tackle:
|
|
Another round of well-articulated feedback, I feel spoiled :)
I can see the advantage of having a maximum bound, indeed. For example, for a stack-pinned store, it's clear that all pointers will be invalidated when the stack frame containing the store is unwound. What is less clear is whether this additional safeguard can be built on top of For example, in the future possibilities, you can see that it's possible to build Ideally, I would prefer if the Would it be possible to build
I'm afraid I'm still not quite understanding why you insist on separating front and back, and why mutability of the front matters so much to you. In the end, the core data-structure is always the "back", the actual memory pool with both memory and associated metadata for tracking what is available (or not), and this memory pool will be aliased -- and thus require I don't understand what the "store front" you propose is supposed to be. It seems that it's either also the back (in-line stores) or just a reference to the back (your example) and never has any (mutable) state of its own. What role is this "front" supposed to play that a
You're close, though a bit off yet as far as I understand from my (very recent) conversation with Ralf Jung on this very thread:
Therefore, you cannot, soundly, form a mutable reference to And thus the code you linked is unsound despite the use of (And I'll freely admit I had not quite anticipated that when I launched myself in this quest, I knew aliasing mattered, but had not realized the depth of it... I hope that Ralf comments above and the
Another issue with Hence, for most stores and usecases, a As noted in my prior comment, I'll try to investigate switching from ... and at this stage I'm afraid it's about the best we can do. Whenever a store must make multiple allocations, there must be an |
|
I'm going to make simpler and more focused comments (probably a single topic) so it doesn't take so long to make them. Lifetimes of pointed memoryThe lifetime depends on the store being used. That means that if we want an abstraction to know this lifetime we need this API to share it publicly. We would like to do something like: trait Store {
lifetime 'limit;
}
impl Store for Example {
lifetime 'limit = 'static;
}So then we could use Is this what you had in mind? |
I didn't have much in mind, to be honest. I would, however, develop it much more in-line with the With that in mind: pub trait StoreUpperBound<'a>: Store {
// # Safety: see `Store::resolve`.
unsafe fn resolve_bounded(&self, handle: Self::Handle) -> AccessGuard<'a> {
// Safety:
// - Per pre-conditions.
AccessGuard::new(unsafe { self.resolve(handle) })
}
}
impl<'a> StoreUpperBound<'a> ExampleStore<'a> {}And then anyone wanting to use an And if people need to adapt existing implementation, you can simply provide an adapter struct which wraps an existing store and forwards existing traits implementations + implements I would think that's all that's necessary, no? |
|
@mangelats I updated the companion repository with a |
* Changes: - Remove StoreMultiple, blending its guarantees into Store. - Introduce parallel trait StoreSingle, specifically for single allocations. - Review APIs, examples, guarantee descriptions, and justifications. * Motivation: An in-line Store must use UnsafeCell internally, which runs afoul of LLVM coarse-grained attributes (noalias, readonly, writeonly, etc...). A specialized StoreSingle can however be used for single-allocations, powering the most commonly used collections (by a wide margin), as it side-steps the woes of UnsafeCell.
|
@CAD97 I think you will particularly appreciate this new revision, as you were the one most prominently decrying the issues that Thanks to the discussions with @mangelats (once again, thanks for your feedback!), I got the idea of moving away from a single "do-it-all"
While the "duplication" of API bothered me, at first, I think it's fine because:
This means there's essentially a single API to learn, it just so happens to be expressed as two different traits. The one thing that bothers me is that I couldn't manage to provide a default implementation of |
Avoid infinite recursion in `MaxPick::clear`. Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>
|
Any update on this? |
|
Well, it's embarrassing... but no, not really. And it's mostly because nobody (not even I) has been pushing forward. Ideally, we would need to move this to an e-RFC. A bunch of the small API questions can be solved a posteriori -- they're unsolved for There's one nagging API issue which may be better solved prior to an e-RFC, though, and that's moving away from Even with
If you care to see this move forward, then there's two ways you can help:
|
|
I personally am not enthusiastic about the Store API because its main use cases (
|
Yes, everybody has written those two (and their String variants, incredibly common too). I can't even count how many times I have, and it's actually because I was tired of subtle behavior/feature differences between my variants that I created the first Storage API in C++ (since VecDeque is only slightly harder to write, and I've written Inline & Small variants of it too. In C++, where it's a real pain. I haven't seen many versions of The first advantage of
Specialized collections on crates.io may always have some advantage indeed. They may be leaner (data-wise and code-wise), they may have extra functionality/guarantees, etc... I wouldn't necessarily say they're better though. They may not be as feature-rich, as optimized, as sound, and regardless they require depending on a 3rd-party which introduces its own risks. So, yes, if one wishes to save 8 bytes one can use I'd recommend people don't unless they have a very good reason too: Inline & Small collections -- by virtue of inlining the data -- are fat already. In most cases 8 bytes won't make or break the usecase, and the quality/safety/security assurances you get by using the standard ones are not worth trading for such a small saving.
Not with the current The more pressing issue may be fallibility. An Inline version of
Similarly, I have an Yes, a specialized container can pull heroics. With trade-offs. Whether a user value those heroics or not will depend on their usecases. Most users probably won't care:
If a user is looking for such savings, though, maybe they'll want to use Once again, though, are the heroics worth it? For most usecases, no. Rewriting a container from scratch to save up 8 bytes is rarely, if ever, worth it. All the more reasons for a generic Store API.
Are those even the main usecases, though? I mean, I'm quite a fan of And even if those are the main usecases, I don't see many people attacking the issue of rewriting core data-structures for shared-memory usage (inter-process communication OR persistence across restarts). I'd love me a |
|
Sorry if this is off topic, but could you elaborate on what problems the current allocator API has with shared memory? We've been using allocator2 with a shared memory allocator for a little while now & haven't hit any issues yet, and this has me concerned we just haven't exercised the API enough. |
The Allocator API is perfectly suited to allocating shared memory, there is no concern. The problem of the Allocator API is that it returns a pointer. On modern hardware and OSes, a pointer is an address in virtual memory which is, by default, private to the process, which in turn means said pointer is, by default, meaningless outside the process which allocated it. This means a pointer cannot be shared across processes, by default, which in turns means a pointer should not be written to (or read from) shared memory. Specific precautions can be taken: disabling ASLR may allow sharing pointers to virtual-tables or other static data, shared memory can be mapped at the same address in all processes allowing sharing "inner" pointers, etc... but by default, pointers should not be shared. |
To this point, I just caught up on this RFC because I'm trying to provide a local inline network heap for some userspace networking experiments on top of // briefly: this is a packet forwarder/router that operates over a set of links that
// may be added or removed dynamically by the user
struct NetNode<S> {
// node owns a fixed-size heap that it uses for all of its allocations:
// tx/rx buffers, queues, trait objects like below
allocator: MyHeap<S>,
network_links: Vec<Box<dyn Link, MyHeap<S>>, MyHeap<S>>,
}Where But in accepting that constraint, I'm forced to use macro_rules! static_netnode {
($size:n) => {{
static ref STORAGE: ::static_cell::ConstStaticCell<[u8; $size]> =
::static_cell::ConstStaticCell::new([0u8; $size]);
NetNode::with_storage(STORAGE.take())
}};
}The point of frustration is that in principle Also, beyond |
|
Any updates or ways to help push this ? As a gamedev this is something very important for us, as allocator_api is still unstable I hope Store API could get us closer to a stable allocator API, and IMO may be better than the allocator_api. In Unreal Engine you can build a inline vec by writing In general I am a big fan of APIs that helps more people to write performant code, the Store API lowers the barrier for developers to tune the allocator of theirs containers versus having to use another type which may not have a 100% compatible front-facing API or may not be well battle-tested. I saw devs falling back to a more simple solution vs performant a lot of time due to this. But I want to reiterate how having a stable allocator API is extremely important for high-performance and/or real time systems use cases such as video games. We can get away with unstable but that means that can't use other crates since they may globally allocate at unpredictable times => big no-no. It fragments the ecosystem like it is the case in C++: every major game engines ends up reimplementing the whole standard library, which cause friction when using libs or middleware, as these also reimplements theirs parts of the std lib 😢. |
I would personally prefer to wait for As far as I can tell, there are a few blockers:
Additional ComplexityThe ability to use String, Vec, HashMap, etc... with a Store API to get an inline version is indubitably very cool, yet there are costs:
Which somewhat offset the benefit of a single implementation. I of course believe the benefit overweigh the costs, but I am not the one you need to convince. Gathering usecases like you mention -- such as Unexpected LimitationsI'm relatively convinced, through my experiments, that the Store API is well-suited for the inline collections usecase. Similarly, it can be used to have intricate structures in shared memory... but regular collections do not fit there, since shared memory imply concurrency and storing any "process-local" pointer (not handle) in the shared memory is fraught with peril. There has no feedback of suitability, or lack thereof, in other usecases. I feel like it could be useful for exotic requirements -- compressed memory, distant/paginated memory, etc... -- but I have no experience to speak of with those, and there may be unexpected limitations resulting from this blind spot, which such experience could reveal, and possibly fix. Note: consider that the Ideal APIBeyond that, the Store API mostly "echoes" the Allocator API, and therefore inherits all its uncertainties. To name but a few:
Thom CC raised a lot more issue in his 2 years old post. Nothing has changed. Experience & AppetiteThe first requirement to push this over the line is appetite: the motivation to take the time to tackle the problems at hand. I don't think there's much appetite from the libs team -- there's a reason The second requirement is experience, and feedback. It's a bit of a catch-22. People complain that this is a nightly API (for I've demonstrated the traits work. I'm convinced they're pretty good. But that's ONE data point. It's quite insufficient. Which may partially explain the lack of appetite from the libs team: ONE data point to overhaul all the collections in Rust is a wee bit lacking. |
alloc: stabilise `Allocator` # Allocator stabilisation report Reference PR: - rust-lang/reference#2364 This is the stabilisation report for a subset of the feature `allocator_api`, with tracking issue [rust-lang#32838](rust-lang#32838) under the purview of wg-allocators, initially proposeed by [RFC rust-lang#1398](https://rust-lang.github.io/rfcs/1398-kinds-of-allocators.html). The remainder of the feature will be renamed to `allocator_ext`. This was a collaborative effort of t-libs, wg-allocators, members of t-types, t-lang, and t-opsem, alongside interested parties in the ecosystem and contributors to the initial attempt at stabilisation on GitHub. See also the new [wg-allocators roadmap](rust-lang/wg-allocators#150) on the matter. ## Summary The following is a proposal following several conversations, [in-person](rust-lang/all-hands-2026#48) and [online](https://rust-lang.zulipchat.com/#narrow/channel/197181-t-libs.2Fwg-allocators), with libs team members and interested ecosystem participants and represents an attempt at stabilising an MVP for the `Allocator` trait and its implementation safety requirements, alongside minimal functionality to make its use possible in the standard library. While an effort was made to align with the stated positions of the team, the opinions and rationale stated are **the author's own**, and should **not** be seen as representative of the libs(-api) team as a whole except insofar as individual members therein choose to endorse the contents of report. Any mention of "we", "us", etc. should be understood to refer to the author alongside those who have explicitly expressed agreement. ## API & considerations The stabilised API surface consists of: ```rust unsafe trait Allocator { // Required methods fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>; unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout); // Provided methods fn allocate_zeroed( &self, layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn grow( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn grow_zeroed( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn shrink( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } } // N.B.: This particular point is lang-relevant since `Box` // would be stabilised without being fundamental over `A`. struct Box<T, #[stable(...)] A: Allocator>(...) impl<T, A: Allocator> Box<T, A> { fn new_in(x: T, alloc: A) -> Box<T, A>; } impl<T: ?Sized, A: Allocator> Box<T, A> { unsafe fn from_raw_in(raw: *mut T, alloc: A) -> Self; unsafe fn from_non_null_in(raw: NonNull<T>, alloc: A) -> Self; fn into_raw_with_allocator(b: Self) -> (*mut T, A); fn into_non_null_with_allocator(b: Self) -> (NonNull<T>, A); fn allocator(b: &Self) -> &A; } struct Vec<T, #[stable(...)] A: Allocator> { ... } impl<T, A: Allocator> Vec<T, A> { fn new_in(alloc: A) -> Vec<T, A>; fn with_capacity_in(capacity: usize, alloc: A) -> Self; unsafe fn from_raw_parts_in( ptr: *mut T, length: usize, capacity: usize, alloc: A, ) -> Self; unsafe fn from_parts_in( ptr: NonNull<T>, length: usize, capacity: usize, alloc: A, ) -> Self; fn into_raw_parts_with_allocator(self) -> (*mut T, usize, usize, A); fn into_parts_with_allocator(self) -> (NonNull<T>, usize, usize, A); fn allocator(&self) -> &A; } struct Global; // implementor of Allocator struct System; // implementor of Allocator unsafe impl<A: Allocator + ?Sized> Allocator for &A { ... } unsafe impl<A: Allocator + ?Sized> Allocator for &mut A { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Box<A, _> { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Rc<A, _> { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Arc<A, _> { ... } ``` The `by_ref` method on `Allocator` was removed, as it was only a postfix syntax convenience (equivalent to writing `(&alloc)`). The safety requirements on implementors of `Allocator` were tightened to the most restrictive sound form we expect to possibly want, in order to enable us to iterate on the design in the future and relax these bounds if it is deemed possible. Notable changes from the form assumed before the stabilisation effort started: - the implicit requirement that the standard library's `Clone for Arc<T, A>` implementation relied on but was improperly documented, for implementors not to invalidate allocated memory on drop or mutable access was made explicit; - implementors are now required not unwind from any of the methods on the trait or from drop; - the safety invariants implementors of `Allocator + Clone` must uphold were moved to their own unstable marker subtrait as the requirement was deemed unjustifiable. Additionally, [it was decided](rust-lang#156906) that `Allocator` will be `dyn`-compatible, as the resulting constraints on the design were deemed acceptable given the significant increased flexibility for users. ## Soundness developments The process of attempting stabilisation resulted in several soundness issues arising, especially with regard to the interaction between custom allocators and `Box`es. Thus, some points had to be adjusted: - there existed a requirement for trait implementors to obey certain semantics if an implementor of `Allocator` is also `Clone`, which constituted possible UB if broken. These have been dropped, as unsafe implementors [cannot guard against possible unsoundness](rust-lang#156920) from incorrect implementations in downstream safe code, but the equivalent functionality may be added backwards-compatibly with an unsafe marker trait or language mechanism; - `Box::into_pin` will not yet be possible with custom allocators. This is because of a [soundness bug](rust-lang#157089) relating to an interaction between the possibility of manually implementing `Clone for Box<T, A>` and `Box` being covariant over `A`, allowing for a pinned box to be cloned with a non-`'static` allocator from one with a correct `'static` allocator subtyped to a non-static one. Making `Box` invariant over the allocator was considered, but was deemed far too limiting and would technically be a [breaking change](rust-lang#153607) to reverse later. Thus, for now, an unstable and unsafe marker trait `StaticAllocator` will be introduced to mark an allocator as guaranteeing that its allocations live for `'static` (i.e. will never be lost unless explicitly de/reallocated). This will be implemented for the `Global` and `System` allocators; - due to `Pin`'s preexisting implementation of a safe `Pin::new` for any pointer type where `<Ptr as Deref>::Target: Unpin`, it *will* be stably possible to call `Pin::new()` on a box with a custom allocator as changing this would require significant special-casing in trait resolution. Experiments in this direction surfaced a [soundness bug](rust-lang#159445 (comment)), addressed by tightening the requirements of `impl PinSafePointer for Box` to necessitate a pin-safe `StaticAllocator`; - further discussion revealed that these same semantics are necessary for integrating custom allocators into LLVM's proposed semantics for allocator intrinsics, as below; - a preexisting hack whereby `Box` had `noalias` semantics for its pointer if and only if the allocator is `Global` - alongside a similar hack to make ["unleaking"](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak) work - is to be moved to an unstable wrapper type `NativeAllocator<A: StaticAllocator>`, which enables us to make use of LLVM's new [allocator intrinsics](https://rust-lang.zulipchat.com/#narrow/channel/136281-t-opsem/topic/Enabling.20compiler.20magic.20on.20custom.20allocators). `Global` would then be equivalent to `NativeAllocator<A>` with the concrete allocator substituted in. Per a conversation with members of opsem, this appeared to be a reasonable way forward; - unwinding out of many allocation-related methods was found to be a pervasive source of unsoundness. Thus, language on allocator cloning and allocation methods was expanded so as to ensure unwinds never come out of methods on an `Allocator`, clones, or drops. ## Backwards-compatible changes Several designs were considered to extend or modify the trait's semantics. We have opted to defer full consideration of many of these for later, as we have determined they can be added backwards-compatibly to the existing API. A list of these is present below, alongside rationale for their postponement. ### `Store` API This is an alternative, more complex proposal for custom allocators (see the [draft RFC](rust-lang/rfcs#3446)). Per a conversation in-person with one of the authors of the `Store` proposal, we have established that it could be added backwards-compatibly (in `Store` terminology, the stabilised `Allocator` trait is effectively a storage with pointer handles). The details were thus deferred for potential post-stabilisation changes. ### Split `Deallocator` trait [Supertrait item shadowing](rust-lang#89151) alongside a blanket `impl<A: Allocator + ?Sized> Deallocator for A` will allow us to add a `Deallocator` supertrait backwards-compatibly, and to relax the requirements for collection types to insted hold a `Deallocator`. Conversations with those involved in the above issue suggest it is likely for a PR implementing this to be merged in the near future. ### `fn reallocate()` The current design uses dedicated `grow`, `grow_zeroed`, and `shrink` methods instead of a way to reallocate between arbitrary sizes. However, such a function could be added with a defaulted body in the future, forwarding to the extant `grow`/`shrink` implementations. ### Conditional reentrancy in `std` Not all allocators will be [reentrant in `std`](rust-lang/libs-team#743), and thus the standard library may want to be able to conditionally call the global allocator in areas it has otherwise promised not to. Thus, the proposed unstable `GlobalAllocator: Allocator` marker trait could be extended with a defaulted associated constant `REENTRANT_IN_STD: bool = true` wherein implementors could promise that a certain allocator never calls *any* part of `std`. ## Possible but less clean additions Several options appeared to signal compelling usecases, but were sufficiently niche that we did not consider them to be blocking for an MVP stabilisation so long as it was realistically possible to express their semantics. ### Associated constants Several usecases would be facilitated by having certain associated items on the `Allocator` trait; notably, `const MIN_ALIGN: usize` for the minimum alignment an allocator is always guaranteed to return. Adding the semantics of these backwards-compatibly would rely on maybe trait bounds being stabilised, which per conversations with the lang & types teams we believe is feasible in the near future. Alternatively, much of the same functionality could be added with defaulted `const` methods, which are also on the stabilisation path. ### `grow_in_place()` There is currently no obvious way to signal through the API whether a move of the data is acceptable when reallocating memory. Though messy, a way to express these semantics with the current design does exist, even if non-obvious: ```rust struct A; // `grow`/`grow_zeroed` have non-in-place semantics unsafe impl Allocator for A { /* ... */ } struct B(A); // in-place-grow semantics unsafe impl Allocator for B { /* ... */ } impl A { fn as_pinning(self) -> B { B(self) } } impl B { fn as_nonpinning(self) -> A { self.0 } } ``` We have decided that this is acceptable, given that it is "only" a point of design and not underlying functionality. A cleaner way to signal such semantics would be of interest for future extensions to the trait. Notably, in-place growing and/or shrinking without invalidating preexisting pointers (i.e. actually changing the size of the allocation in the abstract machine) needs proper support from LLVM which may not happen in the near future. ### Allocation flags A similar transmute-based mechanism as for the above can be used to reference a local inside of the allocator, though this could be UB-prone. Alternatively, and much more nicely, argument splatting could allow us to backwards-compatibly extend the trait (assuming implementors as well as callers may ignore optional fields). However, this would depend on the details of such a proposal. The main stakeholder who approached us with concerns on this topic - Rust for Linux - signalled willingness to maintain a downstream extension trait for such functionality for the time being. ## Rejected alternative proposals The following changes were explicitly not made to the API pre-stabilisation, despite it being unlikely that their semantics could be nicely expressed in the (near) future. In all cases, notable arguments existed to make the requested change, but we decided they were not sufficiently compelling. Should a way to express these semantics emerge in the future backwards-compatibly, we would be open to re-reviewing them. ### `NonZeroLayout` arguments An idea had been proposed to change the signature of the allocating/freeing methods to take a `Layout` that is guaranteed to have a nonzero size. We determined that API cleanliness and potential simplification of library code (once `const Trait`s are stable, collection types could drop special-case logic when using a `const Allocator` at zero capacity) outweigh the arguments for not allowing zero-sized allocations. As we see it, in the cases where it would genuinely be problematic, this will only move the branch on zero-sized allocation to the other side of the call. At worst, it would put marginally more pressure on a branch predictor. Though the possibility of zero-size allocations being probematic is often mentioned, we have not seen sufficiently convincing concrete cases where this is the case. One pointed-to example was that of highly performance-sensitive allocators (e.g. bump allocators); however, it appears most of these cases can trivially support zero-size allocations (e.g. bumping by zero). Consequently, we have decided to keep the nicer logic for downstream users of the trait. An argument had also been made around `jemalloc` being unable to correctly handle zero-sized allocations, but this appears to only apply to internal APIs. A similar idea wherein `allocate` was an unsafe method and support for zero-sized allocations was implementation-defined was rejected on similar usability grounds. Several members of the libs team expressed their opinion that the design of `GlobalAlloc` (featuring a similar unsafe allocating method wherein the caller must guarantee the size is nonzero) was not desirable in hindsight. ### `NonNull<u8>` return type Lacking a better way to signal returned vs. requested capacity, and not wishing to duplicate all of the allocating methods, we have decided that we would prefer to keep the wide-pointer return value and potentially use that logic to determine capacity in returned allocations. This will never be an issue with regard to performance, as no architecture allocates expects fewer than two registers to be clobbered by a function call, and so there is no cost to returning the wide pointer. Some callers will elect to ignore extra capacity; similarly, some implementors will elect not to offer it. The language around implementation safety ensures that these cases are supported, and recommends that implementors not signal extra capacity if it would be expensive to do so. That is to say, both the caller and implementor must cooperate for the excess to be meaningfully usable; otherwise there is no performance impact in a correct implementation. ### Associated types Having an associated type, especially for the returned error on allocation failure, had been mentioned as a possible addition; however, doing so would add significant complexity to the trait while also making `dyn`-compatibility impossible. Few concrete usecases came up where the allocator itself has meaningful error information that would be actionable to callers, and therefore it was elected to keep the current ZST `AllocError`. ## Future work A large part of the standard library will need review as we determine what the correct way is for various collection and pointer types to work with custom allocators. Notably, there are multiple outstanding proposals for integrating fallible allocation APIs into the standard library, and a stable mechanism needs to be decided on for exposing the `Allocator` + `Clone` interaction. ## Outlined potential extensions The following is a possible future outline of what the `Allocator` trait and related might look like under this proposal, assuming both of supertrait item shadowing and defaulted associated items being added: ```rust unsafe trait Allocator: Deallocator { fn allocate( &self, layout: Layout, ) -> Result<NonNull<[u8]>, AllocError>; unsafe fn deallocate( &self, ptr: NonNull<u8>, layout: Layout, ); // Provided methods unsafe fn reallocate( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } /// Minimum alignment that will always be returned, regardless /// of what alignment is requested. const fn min_align(&self) -> usize { 1 } } unsafe trait Deallocator { unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout); } // Enabled by supertrait item shadowing. impl<A: Allocator> Deallocator for A { unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) { <Self as Allocator>::deallocate(self, ptr, layout) } } /// The allocator is suitable for use as the global allocator. /// /// # Safety /// /// `reentrant_in_std` must never be `false` incorrectly, and /// if `true`, the allocator may only use those parts of `std` /// which explicitly allow themselves to be called from the global /// allocator (such as thread-locals). unsafe trait GlobalAllocator: Allocator + Sync + 'static { const REENTRANT_IN_STD: bool; } /// `Clone` will create an equivalent (de)allocator (i.e. both /// can deallocate the same memory), and `Copy` either is /// the same as clone (i.e. `Clone` is a memcpy) or impossible /// to implement. unsafe trait AllocatorClone: Deallocator + Clone {} /// Polls allocator equivalence. unsafe trait AllocatorEq<Other: AllocatorEq = Self>: Deallocator { /// If `other` can free something, so can `self`. /// Implementors must never incorrectly return `true`, /// and equality must be transitive and reflexive. fn is_equivalent(&self, other: &Other) -> bool; } /// The allocator in question will not break `Pin` guarantees /// even if subtyped with a shorter lifetime; that is, memory /// is never deallocated except via an explicit call to `deallocate` /// (and not via dropping the allocator, etc.). unsafe trait StaticAllocator: Allocator {} impl<T, A, D> Box<T, D> where A: Allocator + AllocatorEq<D>, D: AllocatorEq<A>, { // bikeshed better names fn new_in_with(x: T, alloc: A, dealloc: D) -> Self { if dealloc.is_equivalent(&alloc) { unsafe { Box::new_in_with_unchecked(...) } } } fn with_dealloc(boxed: Box<T, A>, dealloc: D) -> Self { ... } } impl<T, A: StaticAllocator> Box<T, A> { fn into_pin(boxed: Box<T, A>) -> Pin<Box<T, A>> { ... } } /// Calls to this allocator are not considered part of program /// behaviour, and thus may be elided or created by the optimiser. /// Additionally, such an allocator will never return an excess /// and makes no promises about alignment beyond what is requested. #[lang = "native_allocator"] struct NativeAllocator<A: StaticAllocator>(A); impl<A: StaticAllocator> Allocator for NativeAllocator<A> { ... } impl<A: StaticAllocator> StaticAllocator for NativeAllocator<A> {} ``` cc @rust-lang/libs @rust-lang/libs-api @rust-lang/opsem r? libs
Rollup merge of #156882 - nia-e:stable-allocator, r=clarfonthey alloc: stabilise `Allocator` # Allocator stabilisation report Reference PR: - rust-lang/reference#2364 This is the stabilisation report for a subset of the feature `allocator_api`, with tracking issue [#32838](#32838) under the purview of wg-allocators, initially proposeed by [RFC #1398](https://rust-lang.github.io/rfcs/1398-kinds-of-allocators.html). The remainder of the feature will be renamed to `allocator_ext`. This was a collaborative effort of t-libs, wg-allocators, members of t-types, t-lang, and t-opsem, alongside interested parties in the ecosystem and contributors to the initial attempt at stabilisation on GitHub. See also the new [wg-allocators roadmap](rust-lang/wg-allocators#150) on the matter. ## Summary The following is a proposal following several conversations, [in-person](rust-lang/all-hands-2026#48) and [online](https://rust-lang.zulipchat.com/#narrow/channel/197181-t-libs.2Fwg-allocators), with libs team members and interested ecosystem participants and represents an attempt at stabilising an MVP for the `Allocator` trait and its implementation safety requirements, alongside minimal functionality to make its use possible in the standard library. While an effort was made to align with the stated positions of the team, the opinions and rationale stated are **the author's own**, and should **not** be seen as representative of the libs(-api) team as a whole except insofar as individual members therein choose to endorse the contents of report. Any mention of "we", "us", etc. should be understood to refer to the author alongside those who have explicitly expressed agreement. ## API & considerations The stabilised API surface consists of: ```rust unsafe trait Allocator { // Required methods fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>; unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout); // Provided methods fn allocate_zeroed( &self, layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn grow( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn grow_zeroed( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn shrink( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } } // N.B.: This particular point is lang-relevant since `Box` // would be stabilised without being fundamental over `A`. struct Box<T, #[stable(...)] A: Allocator>(...) impl<T, A: Allocator> Box<T, A> { fn new_in(x: T, alloc: A) -> Box<T, A>; } impl<T: ?Sized, A: Allocator> Box<T, A> { unsafe fn from_raw_in(raw: *mut T, alloc: A) -> Self; unsafe fn from_non_null_in(raw: NonNull<T>, alloc: A) -> Self; fn into_raw_with_allocator(b: Self) -> (*mut T, A); fn into_non_null_with_allocator(b: Self) -> (NonNull<T>, A); fn allocator(b: &Self) -> &A; } struct Vec<T, #[stable(...)] A: Allocator> { ... } impl<T, A: Allocator> Vec<T, A> { fn new_in(alloc: A) -> Vec<T, A>; fn with_capacity_in(capacity: usize, alloc: A) -> Self; unsafe fn from_raw_parts_in( ptr: *mut T, length: usize, capacity: usize, alloc: A, ) -> Self; unsafe fn from_parts_in( ptr: NonNull<T>, length: usize, capacity: usize, alloc: A, ) -> Self; fn into_raw_parts_with_allocator(self) -> (*mut T, usize, usize, A); fn into_parts_with_allocator(self) -> (NonNull<T>, usize, usize, A); fn allocator(&self) -> &A; } struct Global; // implementor of Allocator struct System; // implementor of Allocator unsafe impl<A: Allocator + ?Sized> Allocator for &A { ... } unsafe impl<A: Allocator + ?Sized> Allocator for &mut A { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Box<A, _> { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Rc<A, _> { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Arc<A, _> { ... } ``` The `by_ref` method on `Allocator` was removed, as it was only a postfix syntax convenience (equivalent to writing `(&alloc)`). The safety requirements on implementors of `Allocator` were tightened to the most restrictive sound form we expect to possibly want, in order to enable us to iterate on the design in the future and relax these bounds if it is deemed possible. Notable changes from the form assumed before the stabilisation effort started: - the implicit requirement that the standard library's `Clone for Arc<T, A>` implementation relied on but was improperly documented, for implementors not to invalidate allocated memory on drop or mutable access was made explicit; - implementors are now required not unwind from any of the methods on the trait or from drop; - the safety invariants implementors of `Allocator + Clone` must uphold were moved to their own unstable marker subtrait as the requirement was deemed unjustifiable. Additionally, [it was decided](#156906) that `Allocator` will be `dyn`-compatible, as the resulting constraints on the design were deemed acceptable given the significant increased flexibility for users. ## Soundness developments The process of attempting stabilisation resulted in several soundness issues arising, especially with regard to the interaction between custom allocators and `Box`es. Thus, some points had to be adjusted: - there existed a requirement for trait implementors to obey certain semantics if an implementor of `Allocator` is also `Clone`, which constituted possible UB if broken. These have been dropped, as unsafe implementors [cannot guard against possible unsoundness](#156920) from incorrect implementations in downstream safe code, but the equivalent functionality may be added backwards-compatibly with an unsafe marker trait or language mechanism; - `Box::into_pin` will not yet be possible with custom allocators. This is because of a [soundness bug](#157089) relating to an interaction between the possibility of manually implementing `Clone for Box<T, A>` and `Box` being covariant over `A`, allowing for a pinned box to be cloned with a non-`'static` allocator from one with a correct `'static` allocator subtyped to a non-static one. Making `Box` invariant over the allocator was considered, but was deemed far too limiting and would technically be a [breaking change](#153607) to reverse later. Thus, for now, an unstable and unsafe marker trait `StaticAllocator` will be introduced to mark an allocator as guaranteeing that its allocations live for `'static` (i.e. will never be lost unless explicitly de/reallocated). This will be implemented for the `Global` and `System` allocators; - due to `Pin`'s preexisting implementation of a safe `Pin::new` for any pointer type where `<Ptr as Deref>::Target: Unpin`, it *will* be stably possible to call `Pin::new()` on a box with a custom allocator as changing this would require significant special-casing in trait resolution. Experiments in this direction surfaced a [soundness bug](#159445 (comment)), addressed by tightening the requirements of `impl PinSafePointer for Box` to necessitate a pin-safe `StaticAllocator`; - further discussion revealed that these same semantics are necessary for integrating custom allocators into LLVM's proposed semantics for allocator intrinsics, as below; - a preexisting hack whereby `Box` had `noalias` semantics for its pointer if and only if the allocator is `Global` - alongside a similar hack to make ["unleaking"](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak) work - is to be moved to an unstable wrapper type `NativeAllocator<A: StaticAllocator>`, which enables us to make use of LLVM's new [allocator intrinsics](https://rust-lang.zulipchat.com/#narrow/channel/136281-t-opsem/topic/Enabling.20compiler.20magic.20on.20custom.20allocators). `Global` would then be equivalent to `NativeAllocator<A>` with the concrete allocator substituted in. Per a conversation with members of opsem, this appeared to be a reasonable way forward; - unwinding out of many allocation-related methods was found to be a pervasive source of unsoundness. Thus, language on allocator cloning and allocation methods was expanded so as to ensure unwinds never come out of methods on an `Allocator`, clones, or drops. ## Backwards-compatible changes Several designs were considered to extend or modify the trait's semantics. We have opted to defer full consideration of many of these for later, as we have determined they can be added backwards-compatibly to the existing API. A list of these is present below, alongside rationale for their postponement. ### `Store` API This is an alternative, more complex proposal for custom allocators (see the [draft RFC](rust-lang/rfcs#3446)). Per a conversation in-person with one of the authors of the `Store` proposal, we have established that it could be added backwards-compatibly (in `Store` terminology, the stabilised `Allocator` trait is effectively a storage with pointer handles). The details were thus deferred for potential post-stabilisation changes. ### Split `Deallocator` trait [Supertrait item shadowing](#89151) alongside a blanket `impl<A: Allocator + ?Sized> Deallocator for A` will allow us to add a `Deallocator` supertrait backwards-compatibly, and to relax the requirements for collection types to insted hold a `Deallocator`. Conversations with those involved in the above issue suggest it is likely for a PR implementing this to be merged in the near future. ### `fn reallocate()` The current design uses dedicated `grow`, `grow_zeroed`, and `shrink` methods instead of a way to reallocate between arbitrary sizes. However, such a function could be added with a defaulted body in the future, forwarding to the extant `grow`/`shrink` implementations. ### Conditional reentrancy in `std` Not all allocators will be [reentrant in `std`](rust-lang/libs-team#743), and thus the standard library may want to be able to conditionally call the global allocator in areas it has otherwise promised not to. Thus, the proposed unstable `GlobalAllocator: Allocator` marker trait could be extended with a defaulted associated constant `REENTRANT_IN_STD: bool = true` wherein implementors could promise that a certain allocator never calls *any* part of `std`. ## Possible but less clean additions Several options appeared to signal compelling usecases, but were sufficiently niche that we did not consider them to be blocking for an MVP stabilisation so long as it was realistically possible to express their semantics. ### Associated constants Several usecases would be facilitated by having certain associated items on the `Allocator` trait; notably, `const MIN_ALIGN: usize` for the minimum alignment an allocator is always guaranteed to return. Adding the semantics of these backwards-compatibly would rely on maybe trait bounds being stabilised, which per conversations with the lang & types teams we believe is feasible in the near future. Alternatively, much of the same functionality could be added with defaulted `const` methods, which are also on the stabilisation path. ### `grow_in_place()` There is currently no obvious way to signal through the API whether a move of the data is acceptable when reallocating memory. Though messy, a way to express these semantics with the current design does exist, even if non-obvious: ```rust struct A; // `grow`/`grow_zeroed` have non-in-place semantics unsafe impl Allocator for A { /* ... */ } struct B(A); // in-place-grow semantics unsafe impl Allocator for B { /* ... */ } impl A { fn as_pinning(self) -> B { B(self) } } impl B { fn as_nonpinning(self) -> A { self.0 } } ``` We have decided that this is acceptable, given that it is "only" a point of design and not underlying functionality. A cleaner way to signal such semantics would be of interest for future extensions to the trait. Notably, in-place growing and/or shrinking without invalidating preexisting pointers (i.e. actually changing the size of the allocation in the abstract machine) needs proper support from LLVM which may not happen in the near future. ### Allocation flags A similar transmute-based mechanism as for the above can be used to reference a local inside of the allocator, though this could be UB-prone. Alternatively, and much more nicely, argument splatting could allow us to backwards-compatibly extend the trait (assuming implementors as well as callers may ignore optional fields). However, this would depend on the details of such a proposal. The main stakeholder who approached us with concerns on this topic - Rust for Linux - signalled willingness to maintain a downstream extension trait for such functionality for the time being. ## Rejected alternative proposals The following changes were explicitly not made to the API pre-stabilisation, despite it being unlikely that their semantics could be nicely expressed in the (near) future. In all cases, notable arguments existed to make the requested change, but we decided they were not sufficiently compelling. Should a way to express these semantics emerge in the future backwards-compatibly, we would be open to re-reviewing them. ### `NonZeroLayout` arguments An idea had been proposed to change the signature of the allocating/freeing methods to take a `Layout` that is guaranteed to have a nonzero size. We determined that API cleanliness and potential simplification of library code (once `const Trait`s are stable, collection types could drop special-case logic when using a `const Allocator` at zero capacity) outweigh the arguments for not allowing zero-sized allocations. As we see it, in the cases where it would genuinely be problematic, this will only move the branch on zero-sized allocation to the other side of the call. At worst, it would put marginally more pressure on a branch predictor. Though the possibility of zero-size allocations being probematic is often mentioned, we have not seen sufficiently convincing concrete cases where this is the case. One pointed-to example was that of highly performance-sensitive allocators (e.g. bump allocators); however, it appears most of these cases can trivially support zero-size allocations (e.g. bumping by zero). Consequently, we have decided to keep the nicer logic for downstream users of the trait. An argument had also been made around `jemalloc` being unable to correctly handle zero-sized allocations, but this appears to only apply to internal APIs. A similar idea wherein `allocate` was an unsafe method and support for zero-sized allocations was implementation-defined was rejected on similar usability grounds. Several members of the libs team expressed their opinion that the design of `GlobalAlloc` (featuring a similar unsafe allocating method wherein the caller must guarantee the size is nonzero) was not desirable in hindsight. ### `NonNull<u8>` return type Lacking a better way to signal returned vs. requested capacity, and not wishing to duplicate all of the allocating methods, we have decided that we would prefer to keep the wide-pointer return value and potentially use that logic to determine capacity in returned allocations. This will never be an issue with regard to performance, as no architecture allocates expects fewer than two registers to be clobbered by a function call, and so there is no cost to returning the wide pointer. Some callers will elect to ignore extra capacity; similarly, some implementors will elect not to offer it. The language around implementation safety ensures that these cases are supported, and recommends that implementors not signal extra capacity if it would be expensive to do so. That is to say, both the caller and implementor must cooperate for the excess to be meaningfully usable; otherwise there is no performance impact in a correct implementation. ### Associated types Having an associated type, especially for the returned error on allocation failure, had been mentioned as a possible addition; however, doing so would add significant complexity to the trait while also making `dyn`-compatibility impossible. Few concrete usecases came up where the allocator itself has meaningful error information that would be actionable to callers, and therefore it was elected to keep the current ZST `AllocError`. ## Future work A large part of the standard library will need review as we determine what the correct way is for various collection and pointer types to work with custom allocators. Notably, there are multiple outstanding proposals for integrating fallible allocation APIs into the standard library, and a stable mechanism needs to be decided on for exposing the `Allocator` + `Clone` interaction. ## Outlined potential extensions The following is a possible future outline of what the `Allocator` trait and related might look like under this proposal, assuming both of supertrait item shadowing and defaulted associated items being added: ```rust unsafe trait Allocator: Deallocator { fn allocate( &self, layout: Layout, ) -> Result<NonNull<[u8]>, AllocError>; unsafe fn deallocate( &self, ptr: NonNull<u8>, layout: Layout, ); // Provided methods unsafe fn reallocate( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } /// Minimum alignment that will always be returned, regardless /// of what alignment is requested. const fn min_align(&self) -> usize { 1 } } unsafe trait Deallocator { unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout); } // Enabled by supertrait item shadowing. impl<A: Allocator> Deallocator for A { unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) { <Self as Allocator>::deallocate(self, ptr, layout) } } /// The allocator is suitable for use as the global allocator. /// /// # Safety /// /// `reentrant_in_std` must never be `false` incorrectly, and /// if `true`, the allocator may only use those parts of `std` /// which explicitly allow themselves to be called from the global /// allocator (such as thread-locals). unsafe trait GlobalAllocator: Allocator + Sync + 'static { const REENTRANT_IN_STD: bool; } /// `Clone` will create an equivalent (de)allocator (i.e. both /// can deallocate the same memory), and `Copy` either is /// the same as clone (i.e. `Clone` is a memcpy) or impossible /// to implement. unsafe trait AllocatorClone: Deallocator + Clone {} /// Polls allocator equivalence. unsafe trait AllocatorEq<Other: AllocatorEq = Self>: Deallocator { /// If `other` can free something, so can `self`. /// Implementors must never incorrectly return `true`, /// and equality must be transitive and reflexive. fn is_equivalent(&self, other: &Other) -> bool; } /// The allocator in question will not break `Pin` guarantees /// even if subtyped with a shorter lifetime; that is, memory /// is never deallocated except via an explicit call to `deallocate` /// (and not via dropping the allocator, etc.). unsafe trait StaticAllocator: Allocator {} impl<T, A, D> Box<T, D> where A: Allocator + AllocatorEq<D>, D: AllocatorEq<A>, { // bikeshed better names fn new_in_with(x: T, alloc: A, dealloc: D) -> Self { if dealloc.is_equivalent(&alloc) { unsafe { Box::new_in_with_unchecked(...) } } } fn with_dealloc(boxed: Box<T, A>, dealloc: D) -> Self { ... } } impl<T, A: StaticAllocator> Box<T, A> { fn into_pin(boxed: Box<T, A>) -> Pin<Box<T, A>> { ... } } /// Calls to this allocator are not considered part of program /// behaviour, and thus may be elided or created by the optimiser. /// Additionally, such an allocator will never return an excess /// and makes no promises about alignment beyond what is requested. #[lang = "native_allocator"] struct NativeAllocator<A: StaticAllocator>(A); impl<A: StaticAllocator> Allocator for NativeAllocator<A> { ... } impl<A: StaticAllocator> StaticAllocator for NativeAllocator<A> {} ``` cc @rust-lang/libs @rust-lang/libs-api @rust-lang/opsem r? libs
alloc: stabilise `Allocator` # Allocator stabilisation report Reference PR: - rust-lang/reference#2364 This is the stabilisation report for a subset of the feature `allocator_api`, with tracking issue [#32838](rust-lang/rust#32838) under the purview of wg-allocators, initially proposeed by [RFC rust-lang#1398](https://rust-lang.github.io/rfcs/1398-kinds-of-allocators.html). The remainder of the feature will be renamed to `allocator_ext`. This was a collaborative effort of t-libs, wg-allocators, members of t-types, t-lang, and t-opsem, alongside interested parties in the ecosystem and contributors to the initial attempt at stabilisation on GitHub. See also the new [wg-allocators roadmap](rust-lang/wg-allocators#150) on the matter. ## Summary The following is a proposal following several conversations, [in-person](rust-lang/all-hands-2026#48) and [online](https://rust-lang.zulipchat.com/#narrow/channel/197181-t-libs.2Fwg-allocators), with libs team members and interested ecosystem participants and represents an attempt at stabilising an MVP for the `Allocator` trait and its implementation safety requirements, alongside minimal functionality to make its use possible in the standard library. While an effort was made to align with the stated positions of the team, the opinions and rationale stated are **the author's own**, and should **not** be seen as representative of the libs(-api) team as a whole except insofar as individual members therein choose to endorse the contents of report. Any mention of "we", "us", etc. should be understood to refer to the author alongside those who have explicitly expressed agreement. ## API & considerations The stabilised API surface consists of: ```rust unsafe trait Allocator { // Required methods fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>; unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout); // Provided methods fn allocate_zeroed( &self, layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn grow( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn grow_zeroed( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } unsafe fn shrink( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } } // N.B.: This particular point is lang-relevant since `Box` // would be stabilised without being fundamental over `A`. struct Box<T, #[stable(...)] A: Allocator>(...) impl<T, A: Allocator> Box<T, A> { fn new_in(x: T, alloc: A) -> Box<T, A>; } impl<T: ?Sized, A: Allocator> Box<T, A> { unsafe fn from_raw_in(raw: *mut T, alloc: A) -> Self; unsafe fn from_non_null_in(raw: NonNull<T>, alloc: A) -> Self; fn into_raw_with_allocator(b: Self) -> (*mut T, A); fn into_non_null_with_allocator(b: Self) -> (NonNull<T>, A); fn allocator(b: &Self) -> &A; } struct Vec<T, #[stable(...)] A: Allocator> { ... } impl<T, A: Allocator> Vec<T, A> { fn new_in(alloc: A) -> Vec<T, A>; fn with_capacity_in(capacity: usize, alloc: A) -> Self; unsafe fn from_raw_parts_in( ptr: *mut T, length: usize, capacity: usize, alloc: A, ) -> Self; unsafe fn from_parts_in( ptr: NonNull<T>, length: usize, capacity: usize, alloc: A, ) -> Self; fn into_raw_parts_with_allocator(self) -> (*mut T, usize, usize, A); fn into_parts_with_allocator(self) -> (NonNull<T>, usize, usize, A); fn allocator(&self) -> &A; } struct Global; // implementor of Allocator struct System; // implementor of Allocator unsafe impl<A: Allocator + ?Sized> Allocator for &A { ... } unsafe impl<A: Allocator + ?Sized> Allocator for &mut A { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Box<A, _> { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Rc<A, _> { ... } unsafe impl<A: Allocator + ?Sized> Allocator for Arc<A, _> { ... } ``` The `by_ref` method on `Allocator` was removed, as it was only a postfix syntax convenience (equivalent to writing `(&alloc)`). The safety requirements on implementors of `Allocator` were tightened to the most restrictive sound form we expect to possibly want, in order to enable us to iterate on the design in the future and relax these bounds if it is deemed possible. Notable changes from the form assumed before the stabilisation effort started: - the implicit requirement that the standard library's `Clone for Arc<T, A>` implementation relied on but was improperly documented, for implementors not to invalidate allocated memory on drop or mutable access was made explicit; - implementors are now required not unwind from any of the methods on the trait or from drop; - the safety invariants implementors of `Allocator + Clone` must uphold were moved to their own unstable marker subtrait as the requirement was deemed unjustifiable. Additionally, [it was decided](rust-lang/rust#156906) that `Allocator` will be `dyn`-compatible, as the resulting constraints on the design were deemed acceptable given the significant increased flexibility for users. ## Soundness developments The process of attempting stabilisation resulted in several soundness issues arising, especially with regard to the interaction between custom allocators and `Box`es. Thus, some points had to be adjusted: - there existed a requirement for trait implementors to obey certain semantics if an implementor of `Allocator` is also `Clone`, which constituted possible UB if broken. These have been dropped, as unsafe implementors [cannot guard against possible unsoundness](rust-lang/rust#156920) from incorrect implementations in downstream safe code, but the equivalent functionality may be added backwards-compatibly with an unsafe marker trait or language mechanism; - `Box::into_pin` will not yet be possible with custom allocators. This is because of a [soundness bug](rust-lang/rust#157089) relating to an interaction between the possibility of manually implementing `Clone for Box<T, A>` and `Box` being covariant over `A`, allowing for a pinned box to be cloned with a non-`'static` allocator from one with a correct `'static` allocator subtyped to a non-static one. Making `Box` invariant over the allocator was considered, but was deemed far too limiting and would technically be a [breaking change](rust-lang/rust#153607) to reverse later. Thus, for now, an unstable and unsafe marker trait `StaticAllocator` will be introduced to mark an allocator as guaranteeing that its allocations live for `'static` (i.e. will never be lost unless explicitly de/reallocated). This will be implemented for the `Global` and `System` allocators; - due to `Pin`'s preexisting implementation of a safe `Pin::new` for any pointer type where `<Ptr as Deref>::Target: Unpin`, it *will* be stably possible to call `Pin::new()` on a box with a custom allocator as changing this would require significant special-casing in trait resolution. Experiments in this direction surfaced a [soundness bug](rust-lang/rust#159445 (comment)), addressed by tightening the requirements of `impl PinSafePointer for Box` to necessitate a pin-safe `StaticAllocator`; - further discussion revealed that these same semantics are necessary for integrating custom allocators into LLVM's proposed semantics for allocator intrinsics, as below; - a preexisting hack whereby `Box` had `noalias` semantics for its pointer if and only if the allocator is `Global` - alongside a similar hack to make ["unleaking"](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak) work - is to be moved to an unstable wrapper type `NativeAllocator<A: StaticAllocator>`, which enables us to make use of LLVM's new [allocator intrinsics](https://rust-lang.zulipchat.com/#narrow/channel/136281-t-opsem/topic/Enabling.20compiler.20magic.20on.20custom.20allocators). `Global` would then be equivalent to `NativeAllocator<A>` with the concrete allocator substituted in. Per a conversation with members of opsem, this appeared to be a reasonable way forward; - unwinding out of many allocation-related methods was found to be a pervasive source of unsoundness. Thus, language on allocator cloning and allocation methods was expanded so as to ensure unwinds never come out of methods on an `Allocator`, clones, or drops. ## Backwards-compatible changes Several designs were considered to extend or modify the trait's semantics. We have opted to defer full consideration of many of these for later, as we have determined they can be added backwards-compatibly to the existing API. A list of these is present below, alongside rationale for their postponement. ### `Store` API This is an alternative, more complex proposal for custom allocators (see the [draft RFC](rust-lang/rfcs#3446)). Per a conversation in-person with one of the authors of the `Store` proposal, we have established that it could be added backwards-compatibly (in `Store` terminology, the stabilised `Allocator` trait is effectively a storage with pointer handles). The details were thus deferred for potential post-stabilisation changes. ### Split `Deallocator` trait [Supertrait item shadowing](rust-lang/rust#89151) alongside a blanket `impl<A: Allocator + ?Sized> Deallocator for A` will allow us to add a `Deallocator` supertrait backwards-compatibly, and to relax the requirements for collection types to insted hold a `Deallocator`. Conversations with those involved in the above issue suggest it is likely for a PR implementing this to be merged in the near future. ### `fn reallocate()` The current design uses dedicated `grow`, `grow_zeroed`, and `shrink` methods instead of a way to reallocate between arbitrary sizes. However, such a function could be added with a defaulted body in the future, forwarding to the extant `grow`/`shrink` implementations. ### Conditional reentrancy in `std` Not all allocators will be [reentrant in `std`](rust-lang/libs-team#743), and thus the standard library may want to be able to conditionally call the global allocator in areas it has otherwise promised not to. Thus, the proposed unstable `GlobalAllocator: Allocator` marker trait could be extended with a defaulted associated constant `REENTRANT_IN_STD: bool = true` wherein implementors could promise that a certain allocator never calls *any* part of `std`. ## Possible but less clean additions Several options appeared to signal compelling usecases, but were sufficiently niche that we did not consider them to be blocking for an MVP stabilisation so long as it was realistically possible to express their semantics. ### Associated constants Several usecases would be facilitated by having certain associated items on the `Allocator` trait; notably, `const MIN_ALIGN: usize` for the minimum alignment an allocator is always guaranteed to return. Adding the semantics of these backwards-compatibly would rely on maybe trait bounds being stabilised, which per conversations with the lang & types teams we believe is feasible in the near future. Alternatively, much of the same functionality could be added with defaulted `const` methods, which are also on the stabilisation path. ### `grow_in_place()` There is currently no obvious way to signal through the API whether a move of the data is acceptable when reallocating memory. Though messy, a way to express these semantics with the current design does exist, even if non-obvious: ```rust struct A; // `grow`/`grow_zeroed` have non-in-place semantics unsafe impl Allocator for A { /* ... */ } struct B(A); // in-place-grow semantics unsafe impl Allocator for B { /* ... */ } impl A { fn as_pinning(self) -> B { B(self) } } impl B { fn as_nonpinning(self) -> A { self.0 } } ``` We have decided that this is acceptable, given that it is "only" a point of design and not underlying functionality. A cleaner way to signal such semantics would be of interest for future extensions to the trait. Notably, in-place growing and/or shrinking without invalidating preexisting pointers (i.e. actually changing the size of the allocation in the abstract machine) needs proper support from LLVM which may not happen in the near future. ### Allocation flags A similar transmute-based mechanism as for the above can be used to reference a local inside of the allocator, though this could be UB-prone. Alternatively, and much more nicely, argument splatting could allow us to backwards-compatibly extend the trait (assuming implementors as well as callers may ignore optional fields). However, this would depend on the details of such a proposal. The main stakeholder who approached us with concerns on this topic - Rust for Linux - signalled willingness to maintain a downstream extension trait for such functionality for the time being. ## Rejected alternative proposals The following changes were explicitly not made to the API pre-stabilisation, despite it being unlikely that their semantics could be nicely expressed in the (near) future. In all cases, notable arguments existed to make the requested change, but we decided they were not sufficiently compelling. Should a way to express these semantics emerge in the future backwards-compatibly, we would be open to re-reviewing them. ### `NonZeroLayout` arguments An idea had been proposed to change the signature of the allocating/freeing methods to take a `Layout` that is guaranteed to have a nonzero size. We determined that API cleanliness and potential simplification of library code (once `const Trait`s are stable, collection types could drop special-case logic when using a `const Allocator` at zero capacity) outweigh the arguments for not allowing zero-sized allocations. As we see it, in the cases where it would genuinely be problematic, this will only move the branch on zero-sized allocation to the other side of the call. At worst, it would put marginally more pressure on a branch predictor. Though the possibility of zero-size allocations being probematic is often mentioned, we have not seen sufficiently convincing concrete cases where this is the case. One pointed-to example was that of highly performance-sensitive allocators (e.g. bump allocators); however, it appears most of these cases can trivially support zero-size allocations (e.g. bumping by zero). Consequently, we have decided to keep the nicer logic for downstream users of the trait. An argument had also been made around `jemalloc` being unable to correctly handle zero-sized allocations, but this appears to only apply to internal APIs. A similar idea wherein `allocate` was an unsafe method and support for zero-sized allocations was implementation-defined was rejected on similar usability grounds. Several members of the libs team expressed their opinion that the design of `GlobalAlloc` (featuring a similar unsafe allocating method wherein the caller must guarantee the size is nonzero) was not desirable in hindsight. ### `NonNull<u8>` return type Lacking a better way to signal returned vs. requested capacity, and not wishing to duplicate all of the allocating methods, we have decided that we would prefer to keep the wide-pointer return value and potentially use that logic to determine capacity in returned allocations. This will never be an issue with regard to performance, as no architecture allocates expects fewer than two registers to be clobbered by a function call, and so there is no cost to returning the wide pointer. Some callers will elect to ignore extra capacity; similarly, some implementors will elect not to offer it. The language around implementation safety ensures that these cases are supported, and recommends that implementors not signal extra capacity if it would be expensive to do so. That is to say, both the caller and implementor must cooperate for the excess to be meaningfully usable; otherwise there is no performance impact in a correct implementation. ### Associated types Having an associated type, especially for the returned error on allocation failure, had been mentioned as a possible addition; however, doing so would add significant complexity to the trait while also making `dyn`-compatibility impossible. Few concrete usecases came up where the allocator itself has meaningful error information that would be actionable to callers, and therefore it was elected to keep the current ZST `AllocError`. ## Future work A large part of the standard library will need review as we determine what the correct way is for various collection and pointer types to work with custom allocators. Notably, there are multiple outstanding proposals for integrating fallible allocation APIs into the standard library, and a stable mechanism needs to be decided on for exposing the `Allocator` + `Clone` interaction. ## Outlined potential extensions The following is a possible future outline of what the `Allocator` trait and related might look like under this proposal, assuming both of supertrait item shadowing and defaulted associated items being added: ```rust unsafe trait Allocator: Deallocator { fn allocate( &self, layout: Layout, ) -> Result<NonNull<[u8]>, AllocError>; unsafe fn deallocate( &self, ptr: NonNull<u8>, layout: Layout, ); // Provided methods unsafe fn reallocate( &self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout, ) -> Result<NonNull<[u8]>, AllocError> { ... } /// Minimum alignment that will always be returned, regardless /// of what alignment is requested. const fn min_align(&self) -> usize { 1 } } unsafe trait Deallocator { unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout); } // Enabled by supertrait item shadowing. impl<A: Allocator> Deallocator for A { unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) { <Self as Allocator>::deallocate(self, ptr, layout) } } /// The allocator is suitable for use as the global allocator. /// /// # Safety /// /// `reentrant_in_std` must never be `false` incorrectly, and /// if `true`, the allocator may only use those parts of `std` /// which explicitly allow themselves to be called from the global /// allocator (such as thread-locals). unsafe trait GlobalAllocator: Allocator + Sync + 'static { const REENTRANT_IN_STD: bool; } /// `Clone` will create an equivalent (de)allocator (i.e. both /// can deallocate the same memory), and `Copy` either is /// the same as clone (i.e. `Clone` is a memcpy) or impossible /// to implement. unsafe trait AllocatorClone: Deallocator + Clone {} /// Polls allocator equivalence. unsafe trait AllocatorEq<Other: AllocatorEq = Self>: Deallocator { /// If `other` can free something, so can `self`. /// Implementors must never incorrectly return `true`, /// and equality must be transitive and reflexive. fn is_equivalent(&self, other: &Other) -> bool; } /// The allocator in question will not break `Pin` guarantees /// even if subtyped with a shorter lifetime; that is, memory /// is never deallocated except via an explicit call to `deallocate` /// (and not via dropping the allocator, etc.). unsafe trait StaticAllocator: Allocator {} impl<T, A, D> Box<T, D> where A: Allocator + AllocatorEq<D>, D: AllocatorEq<A>, { // bikeshed better names fn new_in_with(x: T, alloc: A, dealloc: D) -> Self { if dealloc.is_equivalent(&alloc) { unsafe { Box::new_in_with_unchecked(...) } } } fn with_dealloc(boxed: Box<T, A>, dealloc: D) -> Self { ... } } impl<T, A: StaticAllocator> Box<T, A> { fn into_pin(boxed: Box<T, A>) -> Pin<Box<T, A>> { ... } } /// Calls to this allocator are not considered part of program /// behaviour, and thus may be elided or created by the optimiser. /// Additionally, such an allocator will never return an excess /// and makes no promises about alignment beyond what is requested. #[lang = "native_allocator"] struct NativeAllocator<A: StaticAllocator>(A); impl<A: StaticAllocator> Allocator for NativeAllocator<A> { ... } impl<A: StaticAllocator> StaticAllocator for NativeAllocator<A> {} ``` cc @rust-lang/libs @rust-lang/libs-api @rust-lang/opsem r? libs
View all comments
The Store offers a more flexible allocation API, suitable for in-line memory store, shared memory store, compact "pointers", const/static use, and more.
Adoption of this API, and its use in standard collections, would render a number of specialized crates obsolete in full or in part, such as StackFuture.
Rendered