Skip to content

Stabilize dyn subset of Allocator as core::alloc::Alloc#157286

Open
jmillikin wants to merge 4 commits into
rust-lang:mainfrom
jmillikin:stabilize-alloc-subset
Open

Stabilize dyn subset of Allocator as core::alloc::Alloc#157286
jmillikin wants to merge 4 commits into
rust-lang:mainfrom
jmillikin:stabilize-alloc-subset

Conversation

@jmillikin
Copy link
Copy Markdown
Contributor

Stabilize an MVP subset of the unstable Allocator trait as core::alloc::Alloc. This new trait provides memory allocation only (it can be considered a trait version of alloc / realloc / dealloc), and does not require the shared-identity Copy / Clone of Allocator.

The Allocator trait remains as a marker trait for the safety properties required by the standard container types, such as Box, and is not being stabilized in this commit. It is expected that future work on allocator_api will expand the Allocator trait with new safety properties or functionality (which may be dyn-incompatible).

As part of the split, functions of Alloc were adjusted to return NonNull<u8> instead of NonNull<[u8]>. This change is intended to better match the actual usage of allocation APIs, make the API more resistant to accidental unsoundness, and relax the expectation of slice length elision via inlining.

The Allocator::allocate_at_least function is added for compatibility with hashbrown, which depends on the existing unstable allocator_api signature when being built as part of the standard library. That wrinkle will need to be resolved via an updated hashbrown before this can be merged.

cc @rust-lang/libs @rust-lang/libs-api @rust-lang/opsem

r? libs

@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Jun 2, 2026

These commits modify the library/Cargo.lock file. Unintentional changes to library/Cargo.lock can be introduced when switching branches and rebasing PRs.

If this was unintentional then you should revert the changes before this PR is merged.
Otherwise, you can ignore this comment.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jun 2, 2026
@jmillikin
Copy link
Copy Markdown
Contributor Author

Example of unsoundness in existing code caused by the NonNull<[u8]> signatures, which I found while updating the tests:

fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
    let ptr = self.allocate(layout)?;
    if layout.size() > 0 {
        unsafe {
            ptr.as_mut_ptr().write_bytes(0, layout.size());
        }
    }
    Ok(ptr)
}

The expectation that layout.size() == allocation len is pretty common but causes wrapper implementations of allocate_zeroed to potentially return buffers of uninitialized bytes if the underlying allocator returned more than requested.

@jmillikin jmillikin force-pushed the stabilize-alloc-subset branch 2 times, most recently from 8e19eb8 to 0dc6d4f Compare June 2, 2026 06:24
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@jmillikin jmillikin force-pushed the stabilize-alloc-subset branch from 1394323 to c047de3 Compare June 2, 2026 08:32
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Jun 2, 2026

This PR modifies tests/ui/issues/. If this PR is adding new tests to tests/ui/issues/,
please refrain from doing so, and instead add it to more descriptive subdirectories.

@jmillikin jmillikin force-pushed the stabilize-alloc-subset branch 2 times, most recently from 15faebd to 4b90609 Compare June 2, 2026 08:53
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Jun 2, 2026

These commits modify the Cargo.lock file. Unintentional changes to Cargo.lock can be introduced when switching branches and rebasing PRs.

If this was unintentional then you should revert the changes before this PR is merged.
Otherwise, you can ignore this comment.

@jmillikin jmillikin force-pushed the stabilize-alloc-subset branch from 385c4cf to 161846f Compare June 2, 2026 09:19
@rustbot rustbot added the T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) label Jun 2, 2026
@theemathas
Copy link
Copy Markdown
Contributor

Does this stabilization allow using a type that implements Alloc as an allocator for stdlib collections?

If so, how do you deal with #157089?

@jmillikin
Copy link
Copy Markdown
Contributor Author

The remaining CI failures are related to using a non-ustream source for hashbrown, which works around hashbrown having a hardcoded assumption about the unstable Allocator API.

I'm hesitant to do major surgery on the build just to get that temporary workaround working better. If this seems like a reasonable way to stabilize allocation then I'll send a separate PR to set up Allocator::allocate_at_least + update hashbrown to use it.

@Noratrieb
Copy link
Copy Markdown
Member

Are you aware of the existing ongoing stabilization effort and the other stabilization PR?

@jmillikin
Copy link
Copy Markdown
Contributor Author

Does this stabilization allow using a type that implements Alloc as an allocator for stdlib collections?

If so, how do you deal with #157089?

This PR does not allow an Alloc to be used for stdlib collections unless it also implements Allocator, which is still unstable.

#[stable]
pub trait Alloc { /* memory allocation */ }

#[unstable]
pub trait Allocator: Alloc { /* magic marker for ongoing stdlib collection support for custom allocation */ }

@jmillikin
Copy link
Copy Markdown
Contributor Author

jmillikin commented Jun 2, 2026

Are you aware of the existing ongoing stabilization effort and the other stabilization PR?

Yes, I commented on them several times and have also been discussing the topic in Zulip. Hence this PR.

edit:

@rust-log-analyzer
Copy link
Copy Markdown
Collaborator

The job aarch64-gnu-llvm-21-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
  failed to read `/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/build/vendor/hashbrown-0.17.1/Cargo.toml`

Caused by:
  No such file or directory (os error 2)
env -u RUSTC_WRAPPER CARGO_ENCODED_RUSTDOCFLAGS="-Zannotate-moves\u{1f}-Zrandomize-layout\u{1f}-Zunstable-options\u{1f}--check-cfg=cfg(bootstrap)\u{1f}-Wrustdoc::invalid_codeblock_attributes\u{1f}--crate-version\u{1f}1.98.0-nightly\t(07500571a\t2026-06-02)" CARGO_ENCODED_RUSTFLAGS="-Zannotate-moves\u{1f}-Zrandomize-layout\u{1f}-Zunstable-options\u{1f}--check-cfg=cfg(bootstrap)\u{1f}-Zmacro-backtrace\u{1f}-Csplit-debuginfo=off\u{1f}-Clink-arg=-L/usr/lib/llvm-21/lib\u{1f}-Cllvm-args=-import-instr-limit=10\u{1f}-Clink-args=-Wl,-z,origin\u{1f}-Clink-args=-Wl,-rpath,$ORIGIN/../lib\u{1f}-Alinker-messages\u{1f}--cap-lints=allow\u{1f}--cfg\u{1f}randomized_layouts" RUSTC="/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/dist/rustc-clif" RUSTDOC="/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/dist/rustdoc-clif" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo" "test" "--manifest-path" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/build/sysroot_tests/Cargo.toml" "--target-dir" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/build/sysroot_tests_target" "--locked" "--target" "aarch64-unknown-linux-gnu" "-p" "coretests" "-p" "alloctests" "--tests" "--" "-q" exited with status ExitStatus(unix_wait_status(25856))
Bootstrap failed while executing `--stage 2 test --skip tidy --skip src/tools/rust-analyzer --skip tests --skip coverage-map --skip coverage-run --skip library --skip tidyselftest`
Command `/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo run --target aarch64-unknown-linux-gnu -Zbinary-dep-depinfo -j 4 -Zroot-dir=/checkout --locked --color=always --profile=release --manifest-path /checkout/compiler/rustc_codegen_cranelift/build_system/Cargo.toml -- test --download-dir /checkout/obj/build/cg_clif_download --out-dir /checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif --no-unstable-features --use-backend cranelift --sysroot llvm --skip-test testsuite.extended_sysroot [workdir=/checkout/compiler/rustc_codegen_cranelift]` failed with exit code 1
Created at: src/bootstrap/src/core/build_steps/test.rs:4013:25
Executed at: src/bootstrap/src/core/build_steps/test.rs:4058:26

--- BACKTRACE vvv
   0: std::backtrace_rs::backtrace::libunwind::trace
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
   1: std::backtrace_rs::backtrace::trace_unsynchronized::<<std::backtrace::Backtrace>::create::{closure#0}>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
   2: <std::backtrace::Backtrace>::create
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/backtrace.rs:331:13
   3: <bootstrap::utils::exec::DeferredCommand>::finish_process
             at /checkout/src/bootstrap/src/utils/exec.rs:939:17
   4: <bootstrap::utils::exec::DeferredCommand>::wait_for_output::<&bootstrap::utils::exec::ExecutionContext>
             at /checkout/src/bootstrap/src/utils/exec.rs:831:21
   5: <bootstrap::utils::exec::ExecutionContext>::run
             at /checkout/src/bootstrap/src/utils/exec.rs:741:45
   6: <bootstrap::utils::exec::BootstrapCommand>::run::<&bootstrap::core::builder::Builder>
             at /checkout/src/bootstrap/src/utils/exec.rs:339:27
   7: <bootstrap::core::build_steps::test::CodegenCranelift as bootstrap::core::builder::Step>::run
             at /checkout/src/bootstrap/src/core/build_steps/test.rs:4058:26
   8: <bootstrap::core::builder::Builder>::ensure::<bootstrap::core::build_steps::test::CodegenCranelift>
             at /checkout/src/bootstrap/src/core/builder/mod.rs:1596:36
   9: <bootstrap::core::build_steps::test::CodegenCranelift as bootstrap::core::builder::Step>::make_run
             at /checkout/src/bootstrap/src/core/build_steps/test.rs:3999:17
  10: <bootstrap::core::builder::StepDescription>::maybe_run
             at /checkout/src/bootstrap/src/core/builder/mod.rs:476:13
  11: bootstrap::core::builder::cli_paths::match_paths_to_steps_and_run
             at /checkout/src/bootstrap/src/core/builder/cli_paths.rs:141:22
  12: <bootstrap::core::builder::Builder>::run_step_descriptions
             at /checkout/src/bootstrap/src/core/builder/mod.rs:1139:9
  13: <bootstrap::core::builder::Builder>::execute_cli
             at /checkout/src/bootstrap/src/core/builder/mod.rs:1118:14
  14: <bootstrap::Build>::build
             at /checkout/src/bootstrap/src/lib.rs:803:25
  15: bootstrap::main
             at /checkout/src/bootstrap/src/bin/main.rs:130:11
  16: <fn() as core::ops::function::FnOnce<()>>::call_once
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/core/src/ops/function.rs:250:5
  17: std::sys::backtrace::__rust_begin_short_backtrace::<fn(), ()>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/sys/backtrace.rs:166:18
  18: std::rt::lang_start::<()>::{closure#0}
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/rt.rs:206:18
  19: <&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync as core::ops::function::FnOnce<()>>::call_once
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/core/src/ops/function.rs:287:21
  20: std::panicking::catch_unwind::do_call::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panicking.rs:581:40
  21: std::panicking::catch_unwind::<i32, &dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panicking.rs:544:19
  22: std::panic::catch_unwind::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panic.rs:359:14
  23: std::rt::lang_start_internal::{closure#0}
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/rt.rs:175:24
  24: std::panicking::catch_unwind::do_call::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panicking.rs:581:40
---
  31: __libc_start_main
  32: _start


Command has failed. Rerun with -v to see more details.
Build completed unsuccessfully in 0:25:52
  local time: Tue Jun  2 09:50:42 UTC 2026
  network time: Tue, 02 Jun 2026 09:50:43 GMT
##[error]Process completed with exit code 1.
##[group]Run echo "disk usage:"

@nia-e
Copy link
Copy Markdown
Member

nia-e commented Jun 2, 2026

I'm not sure having the dyn-compatible form of Allocator be a subtrait is the way to go. I think it would be nice for interop to have it so the stdlib collections do work with whatever trait we stabilise, and otherwise this Alloc would have no use inside std itself.

The design I am currently most in favour of would be something to the effect of:

trait Allocator {
    // example dyn-incompatible item
    const MIN_ALIGN: usize;
    fn allocate(...) -> Result<...>;
}

trait DynAllocator {
    // same methods as Allocator, different names to prevent name resolution issues
    fn dyn_allocate(...) -> Result<...>;
}

impl<A: DynAllocator> Allocator for A {
    // conservative defaults for items
    const MIN_ALIGN: usize = 1;
    fn allocate(...) -> Result<...> {
        self.dyn_allocate(...)
    }
}

that way, a dyn DynAllocator + Send + Sync for instance is trivially usable inside std collections and can transparently stand in for a concrete Allocator. I'm also still unconvinced on changing the return pointer to be thin, but that's a different issue.

@jmillikin
Copy link
Copy Markdown
Contributor Author

jmillikin commented Jun 2, 2026

Allocator and DynAllocator only need to be separate trait hierarchies if the Allocator trait's API is incompatible with DynAllocator. I don't think that's likely -- all of the non-dyn features that seem likely to land in core::alloc are extensions to basic memory allocation rather than replacement.

Even if an allocator offers flags or richer error conditions, the ability to implement Clone for Box<T, A: Allocator> implies that allocation without flags must be possible, and rich errors will need to be a superset of "OOM or bad params" that AllocError represents. So rather than having two allocate functions, there would be an allocate that does simple allocation and an allocate_with(&self, flags: Self::Flags) or some such.


Thinking about the allocator API in terms of carving out known required features and then mapping them to types,

The necessity of dyn for the low-level memory allocation use case is known, so the presence and shape of dyn Alloc is established (whatever it's called). We also know that Box<T, A: Allocator> is desired, but the shape of Allocator is unknown (and therefore its relationship to Alloc).

For this PR I somewhat arbitrarily made Alloc a supertrait because it seems strange to have a trait named Allocator that can't do allocation, but there's other shapes possible. For example we can observe that the current design of Allocator as implied by the Clone identity property is actually that of a handle to an allocator, and write something like this:

pub trait Alloc { /* memory allocation, dyn-compatible */ }

// an allocator is a handle to an `Alloc` instance
//
// safety: a clone's get() returns the same underlying `Alloc`
// as the original
pub unsafe trait Allocator {
  type Alloc: Alloc;
  fn alloc_get(&self) -> &Self::Alloc;
  fn alloc_clone(&self) -> Self;
}

impl<'a, A: Alloc> Allocator for &'a A { ... }
impl<A: Alloc> Allocator for Rc<A> { ... }
impl<A: Alloc> Allocator for Arc<A> { ... }

In that design it's not possible to create Box<T, dyn Alloc>, but you could have Box<T, &'a dyn Alloc> or Box<T, Arc<dyn Alloc>>. And of course implementations of either Alloc or Allocator could be regular non-dyn structs, or ZSTs, or wrappers around an Rc / Arc:

pub struct TCMalloc;
impl Alloc for TCMalloc { ... }
impl Allocator for TCMalloc {
  type Alloc = Self;
  fn alloc_get(&self) -> &Self::Alloc { self }
  fn alloc_clone(&self) -> Self { TCMalloc }
}

pub struct MyAlloc(Arc<InnerState>);
impl Alloc for MyAlloc { ... }
impl Allocator for MyAlloc {
  type Alloc = Self;
  fn alloc_get(&self) -> &Self::Alloc { self }
  fn alloc_clone(&self) -> Self { MyAlloc(self.0.clone()) }
}

The Allocator trait needs more design work, but the essential properties of Alloc are known and have been stable for a long time, so we can clarify the duties of Allocator by carving off and stabilizing Alloc.

@nia-e
Copy link
Copy Markdown
Member

nia-e commented Jun 2, 2026

The reason to not have this be a supertrait is my proposed blanket impl. If we split the dyn-compatible and dyn-incompatible traits entirely, the dyn-incompatible one can have a blanket impl with conservative values in terms of the dyn-compatible trait and everyone can always require said dyn-incompatible trait. Otherwise, it would be impossible to express the semantics of "we want the associated constants/types if they're available", since it would require specialisation

@jmillikin
Copy link
Copy Markdown
Contributor Author

The blanket instance would mean you couldn't have a type impl both Allocator and DynAllocator unless it uses the blanket impl's values, right?

trait Allocator {
    // alignments get rounded up to this value
    const MIN_ALIGN: usize;
    fn allocate(...) -> Result<...>;
}
impl<A: DynAllocator> Allocator for A {
    // `dyn DynAllocator` is assumed to not round alignments
    const MIN_ALIGN: usize = 1;
    fn allocate(...) -> Result<...> {
        self.dyn_allocate(...)
    }
}

struct SizeClassAlloc;
impl SizeClassAlloc {
  const MIN_ALIGN: usize = 8; // smallest size class is 8 bytes
}
impl DynAllocator for SizeClassAlloc {
    fn dyn_allocate(&self, layout: Layout) -> Result<NonNull<u8>, AllocError> {
        let align = layout.align().max(Self::MIN_ALIGN);
        // this is fine, over-aligning is always permitted
    }
}

// conflicting implementation of trait `Allocator` for type `SizeClassAlloc`
impl Allocator for SizeClassAlloc {
    const MIN_SIZE: usize = 8;
    fn allocate(&self, layout: Layout) -> Result<NonNull<u8>, AllocError> {
        todo!()
    }
}

@nia-e
Copy link
Copy Markdown
Member

nia-e commented Jun 2, 2026

Yep, it would mean newtyping. That is a notable issue, but I think it's Less Bad than making downstream users not want to use the dyn-incompatible subtrait and possibly optimise around it

@jmillikin
Copy link
Copy Markdown
Contributor Author

Yep, it would mean newtyping. That is a notable issue, but I think it's Less Bad than making downstream users not want to use the dyn-incompatible subtrait and possibly optimise around it

I'm not sure what this means, or what situation you're trying to guard against. Can you explain more about what misuse you're forseeing?

Downstream libraries that need Allocator (such as Box, Vec, or other high-level container types that want to support custom allocation) won't be able to use the dyn version because a dyn Alloc isn't copyable, and dyn Alloc + Clone doesn't guarantee the necessary clones-share-identity safety guarantee. Box<T, A: Alloc> isn't possible, it has to be Box<T, A: Allocator>. The implementation of Allocator could be &'a dyn Alloc, but that's up to the user of the library.

Downstream libraries that need dyn Alloc (such as no_std libraries that want to give the program control over scratch buffers) won't want to use the monomorphized version because that would cause code bloat. struct Decoder<'a> { alloc: &'a dyn Alloc, buf: &'a [u8] } can't be usefully replaced with struct Decoder<A: Allocator>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants