From 0887802002ebf8c6e23c4c14a3fe39cd3e02cd08 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:00:01 +0200 Subject: [PATCH 01/10] Update repository defaults, remove nightly feature --- Cargo.toml | 6 ++++++ fill/Cargo.toml | 8 +++++--- static-alloc/Cargo.toml | 14 ++++++-------- static-alloc/tests/vec_try.rs | 2 -- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ee1345..102968b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,9 @@ members = [ "unsize", "without-alloc", ] + +[workspace.package] +authors = ["Aurelia Molzer <5550310+197g@users.noreply.github.com>"] +license = "MIT OR Apache-2.0 OR Zlib" +repository = "https://github.com/197g/static-alloc" +readme = "Readme.md" diff --git a/fill/Cargo.toml b/fill/Cargo.toml index d97737a..9094c85 100644 --- a/fill/Cargo.toml +++ b/fill/Cargo.toml @@ -2,15 +2,17 @@ name = "fill" version = "0.1.1" description = "Provides the Fill trait, an alternative to Extend for finite containers" -authors = ["Aurelia Molzer <5550310+197g@users.noreply.github.com>"] edition = "2018" -license = "MIT OR Apache-2.0 OR Zlib" documentation = "https://docs.rs/fill" -repository = "https://github.com/HeroicKatora/static-alloc" readme = "Readme.md" keywords = ["fill", "extend", "no-std", "no_std"] categories = ["embedded", "no-std", "data-structures"] +authors.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true + [dependencies] # None. diff --git a/static-alloc/Cargo.toml b/static-alloc/Cargo.toml index 95384df..e30838f 100644 --- a/static-alloc/Cargo.toml +++ b/static-alloc/Cargo.toml @@ -2,14 +2,15 @@ name = "static-alloc" version = "0.2.6" description = "A bump allocator on static memory for the alloc-traits crate" -authors = ["Aurelia Molzer <5550310+197g@users.noreply.github.com>"] edition = "2018" -license = "MIT OR Apache-2.0 OR Zlib" documentation = "https://docs.rs/static-alloc" -repository = "https://github.com/197g/static-alloc" -readme = "Readme.md" categories = ["embedded", "memory-management", "no-std"] +authors.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true + [package.metadata.docs.rs] all-features = true @@ -19,9 +20,6 @@ portable-atomic = { version = "1", optional = true, default-features = false } [features] alloc = [] -# For apis depending on "try_reserve" (#48043). -# Currently only used in a test for ensure future opportunities. -nightly_try_reserve = [] # Enables the `unsync::Chain` module. Note that this is explicitly outside the # SemVer stability guarantees! nightly_chain = ["alloc"] @@ -37,7 +35,7 @@ path = "tests/vec.rs" [[test]] name = "vec_try" path = "tests/vec_try.rs" -required-features = ["nightly_try_reserve"] +required-features = [] [[test]] name = "huuuuuge" diff --git a/static-alloc/tests/vec_try.rs b/static-alloc/tests/vec_try.rs index 6a64f71..dc688a1 100644 --- a/static-alloc/tests/vec_try.rs +++ b/static-alloc/tests/vec_try.rs @@ -1,5 +1,3 @@ -#![feature(try_reserve)] - use static_alloc::Bump; // Provide more memory than #[test] needs for the setup. From 5d4d5acab5737353f669ee9c35a418ef92d3654e Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:00:40 +0200 Subject: [PATCH 02/10] Update version target for static-alloc --- static-alloc/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static-alloc/Cargo.toml b/static-alloc/Cargo.toml index e30838f..ab1dade 100644 --- a/static-alloc/Cargo.toml +++ b/static-alloc/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "static-alloc" -version = "0.2.6" +version = "0.3.0-prerelease" description = "A bump allocator on static memory for the alloc-traits crate" -edition = "2018" +edition = "2024" documentation = "https://docs.rs/static-alloc" categories = ["embedded", "memory-management", "no-std"] +rust-version = "1.85" authors.workspace = true license.workspace = true From fcaf3fd1278092acb6f186cf1e0f529386b258e0 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:15:37 +0200 Subject: [PATCH 03/10] Update cargo metadata --- Cargo.toml | 1 + exit-stack/Cargo.toml | 2 +- fill/Cargo.toml | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 102968b..0548e5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "3" members = [ "alloc-traits", "exit-stack", diff --git a/exit-stack/Cargo.toml b/exit-stack/Cargo.toml index ada7d49..760ec8a 100644 --- a/exit-stack/Cargo.toml +++ b/exit-stack/Cargo.toml @@ -11,7 +11,7 @@ readme = "Readme.md" categories = ["embedded", "memory-management", "no-std"] [dependencies] -static-alloc = { path = "../static-alloc", version = "0.2.1" } +static-alloc = { path = "../static-alloc", version = "0.3.0-prerelease" } [dev-dependencies] pin-utils = "0.1" diff --git a/fill/Cargo.toml b/fill/Cargo.toml index 9094c85..73e9344 100644 --- a/fill/Cargo.toml +++ b/fill/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.1" description = "Provides the Fill trait, an alternative to Extend for finite containers" edition = "2018" documentation = "https://docs.rs/fill" -readme = "Readme.md" keywords = ["fill", "extend", "no-std", "no_std"] categories = ["embedded", "no-std", "data-structures"] From d0b536bd5eed74d595273eb3d0f48be0a0745d34 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:25:15 +0200 Subject: [PATCH 04/10] Resolve lints in bump --- static-alloc/src/bump.rs | 60 +++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/static-alloc/src/bump.rs b/static-alloc/src/bump.rs index 6e00140..823ac2f 100644 --- a/static-alloc/src/bump.rs +++ b/static-alloc/src/bump.rs @@ -7,7 +7,7 @@ use core::alloc::{GlobalAlloc, Layout}; use core::cell::UnsafeCell; use core::mem::{self, MaybeUninit}; -use core::ptr::{null_mut, NonNull}; +use core::ptr::{NonNull, null_mut}; #[cfg(not(feature = "polyfill"))] use core::sync::atomic::{AtomicUsize, Ordering}; @@ -375,7 +375,7 @@ impl Bump { /// /// # Panics /// This function may panic if the provided `level` is from a different slab. - pub fn alloc_at(&self, layout: Layout, level: Level) -> Result { + pub fn alloc_at(&self, layout: Layout, level: Level) -> Result, Failure> { let Allocation { ptr, lifetime, @@ -433,7 +433,7 @@ impl Bump { /// /// assert_eq!(**cell_ref, 0xff); /// ``` - pub fn get(&self) -> Option> { + pub fn get(&self) -> Option> { if mem::size_of::() == 0 { return Some(self.zst_fake_alloc()); } @@ -457,7 +457,7 @@ impl Bump { /// See [`get`] for usage. /// /// [`get`]: #method.get - pub fn get_at(&self, level: Level) -> Result, Failure> { + pub fn get_at(&self, level: Level) -> Result, Failure> { if mem::size_of::() == 0 { let fake = self.zst_fake_alloc(); // Note: zst_fake_alloc is a noop on the level, we may as well check after. @@ -585,8 +585,11 @@ impl Bump { ); let base_ptr = self.storage.get() as *mut T as *mut u8; - let alloc = base_ptr.add(level.0); - let ptr = NonNull::new_unchecked(alloc).cast::(); + + // SAFETY: `level.0` is in bounds as assert above, or by the caller by having provided an + // existing allocation—all allocations we hand out are in bounds. + let alloc = unsafe { base_ptr.add(level.0) }; + let ptr = NonNull::new(alloc).unwrap().cast::(); debug_assert!( ptr.as_ptr().is_aligned(), @@ -842,9 +845,10 @@ impl<'alloc, T> Allocation<'alloc, T> { /// /// [`Bump::leak`]: struct.Bump.html#method.leak pub unsafe fn leak(self, val: T) -> &'alloc mut T { - // The pointer is not borrowed and valid as guaranteed by the caller. - core::ptr::write(self.ptr.as_ptr(), val); - &mut *self.ptr.as_ptr() + // Safety: The pointer is valid for a write as per caller. + unsafe { core::ptr::write(self.ptr.as_ptr(), val) }; + // Safety: The pointer is not borrowed and valid as guaranteed by the caller. + unsafe { &mut *self.ptr.as_ptr() } } /// Write a value into the allocation and own it. @@ -861,9 +865,9 @@ impl<'alloc, T> Allocation<'alloc, T> { /// [`Bump::leak`]: struct.Bump.html#method.leak pub unsafe fn boxed(self, val: T) -> LeakBox<'alloc, T> { // The pointer is not aliased and valid as guaranteed by the caller. - core::ptr::write(self.ptr.as_ptr(), val); + unsafe { core::ptr::write(self.ptr.as_ptr(), val) }; // Safety: the instance is valid, was just initialized. - LeakBox::from_raw(self.ptr.as_ptr()) + unsafe { LeakBox::from_raw(self.ptr.as_ptr()) } } /// Convert this into a mutable reference to an uninitialized slot. @@ -872,7 +876,7 @@ impl<'alloc, T> Allocation<'alloc, T> { /// /// Must have been allocated for a layout that fits the layout of T previously. pub unsafe fn uninit(self) -> &'alloc mut MaybeUninit { - &mut *self.ptr.cast().as_ptr() + unsafe { &mut *self.ptr.cast().as_ptr() } } /// An 'allocation' for an arbitrary ZST, at some arbitrary level. @@ -919,8 +923,8 @@ unsafe impl GlobalAlloc for Bump { unsafe fn realloc(&self, ptr: *mut u8, current: Layout, new_size: usize) -> *mut u8 { let current = NonZeroLayout::from_layout(current.into()).unwrap(); - // As guaranteed, `new_size` is greater than 0. - let new_size = core::num::NonZeroUsize::new_unchecked(new_size); + // Safety: As required of the caller, `new_size` is greater than 0. + let new_size = unsafe { core::num::NonZeroUsize::new_unchecked(new_size) }; let target = match layout_reallocated(current, new_size) { Some(target) => target, @@ -928,13 +932,15 @@ unsafe impl GlobalAlloc for Bump { }; // Construct an allocation. This is not safe in general but the lifetime is not important. - let fake = alloc_traits::Allocation { - ptr: NonNull::new_unchecked(ptr), + let reconstructed = alloc_traits::Allocation { + // Safety: `ptr` is currently allocated via this allocator, i.e. non-null. + ptr: unsafe { NonNull::new_unchecked(ptr) }, layout: current, lifetime: AllocTime::default(), }; - alloc_traits::LocalAlloc::realloc(self, fake, target) + // Safety: satisfies our own invariants. + unsafe { alloc_traits::LocalAlloc::realloc(self, reconstructed, target) } .map(|alloc| alloc.ptr.as_ptr()) .unwrap_or_else(core::ptr::null_mut) } @@ -995,11 +1001,21 @@ unsafe impl<'alloc, T> LocalAlloc<'alloc> for Bump { // succeeds, there is no copying necessary. This was the point of `Level` anyways. let new_alloc = LocalAlloc::alloc(self, layout)?; - core::ptr::copy_nonoverlapping( - alloc.ptr.as_ptr(), - new_alloc.ptr.as_ptr(), - layout.size().min(alloc.layout.size()).into(), - ); + + // Safety: + // - the old allocation is valid for the old size, as required of the caller. + // - the old allocation is valid for reads as it is an allocation of the allocator. + // - the new allocation is valid for the new size. + // - the new allocation is valid for writes as it was successful. + // - our effective copy is at most the old and new size. + unsafe { + core::ptr::copy_nonoverlapping( + alloc.ptr.as_ptr(), + new_alloc.ptr.as_ptr(), + layout.size().min(alloc.layout.size()).into(), + ); + } + // No dealloc. return Some(new_alloc); } From f96d086989b7e852eeecddea1a3b727b34a5b23b Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:41:03 +0200 Subject: [PATCH 05/10] Resolve more warnings --- static-alloc/src/leaked.rs | 6 ++-- static-alloc/src/unsync/bump.rs | 56 ++++++++++++++++++++------------- static-alloc/tests/leak.rs | 10 +++--- static-alloc/tests/leak_box.rs | 2 +- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/static-alloc/src/leaked.rs b/static-alloc/src/leaked.rs index 91ded7a..b2aa571 100644 --- a/static-alloc/src/leaked.rs +++ b/static-alloc/src/leaked.rs @@ -174,7 +174,7 @@ impl<'ctx, T> LeakBox<'ctx, T> { // * `ptr` points to an allocation with correct layout for `V`. // * It is valid for write as it is the only pointer to it. // * The allocation lives for at least `'ctx`. - core::ptr::write(pointer.as_ptr(), val); + unsafe { core::ptr::write(pointer.as_ptr(), val) }; Self { pointer, lifetime, } } } @@ -237,9 +237,11 @@ impl<'ctx, T: ?Sized> LeakBox<'ctx, T> { /// sound. pub unsafe fn from_raw(pointer: *mut T) -> Self { debug_assert!(!pointer.is_null(), "Null pointer passed to LeakBox::from_raw"); + LeakBox { lifetime: AllocTime::default(), - pointer: NonNull::new_unchecked(pointer), + // Safety: caller guarantees this points to a valid instance. Null never does that. + pointer: unsafe { NonNull::new_unchecked(pointer) }, } } diff --git a/static-alloc/src/unsync/bump.rs b/static-alloc/src/unsync/bump.rs index 14a0575..8425466 100644 --- a/static-alloc/src/unsync/bump.rs +++ b/static-alloc/src/unsync/bump.rs @@ -80,13 +80,7 @@ pub struct FromMemError { /// A dynamically sized allocation block in which any type can be allocated. #[repr(C)] pub struct MemBump { - /// An index into the data field. This index - /// will always be an index to an element - /// that has not been allocated into. - /// Again this is wrapped in a Cell, - /// to allow modification with just a - /// &self reference. - index: Cell, + header: Header, /// The data slice of a node. This slice /// may be of any arbitrary size. We use @@ -99,6 +93,16 @@ pub struct MemBump { data: UnsafeCell<[MaybeUninit]>, } +struct Header { + /// An index into the data field. This index + /// will always be an index to an element + /// that has not been allocated into. + /// Again this is wrapped in a Cell, + /// to allow modification with just a + /// &self reference. + index: Cell, +} + impl Bump { /// Create an allocator with uninitialized memory. /// @@ -156,6 +160,7 @@ impl MemBump { mem.get_mut(..header.size()) .ok_or(FromMemError { _inner: () })? .fill(MaybeUninit::new(0)); + // Safety: we just verified the size, and pivoted to the correct alignment. Ok(unsafe { Self::from_mem_unchecked(mem) }) } @@ -173,13 +178,18 @@ impl MemBump { /// more specifically the provenance of these pointers is no longer valid! You _must_ derive /// new pointers based on their offsets. pub unsafe fn from_mem_unchecked(mem: &mut [MaybeUninit]) -> LeakBox<'_, Self> { - let raw = Self::from_aligned_mem(mem); - LeakBox::from_mut_unchecked(raw) + // Safety: memory already valid, according to the caller. + let raw = unsafe { Self::reinterpret_aligned_mem(mem) }; + // Safety: we own this value in the sense that `Drop` is not called by the caller. + unsafe { LeakBox::from_mut_unchecked(raw) } } /// Cast pre-initialized, aligned memory into a bump allocator. #[allow(unused_unsafe)] - unsafe fn from_aligned_mem(mem: &mut [MaybeUninit]) -> &mut Self { + unsafe fn reinterpret_aligned_mem(mem: &mut [MaybeUninit]) -> &mut Self { + // Safety: supposedly guaranteed by the caller. + unsafe { core::hint::assert_unchecked(mem.as_ptr().cast::
().is_aligned()) }; + let header = Self::header_layout(); // debug_assert!(mem.len() >= header.size()); // debug_assert!(mem.as_ptr().align_offset(header.align()) == 0); @@ -315,7 +325,7 @@ impl MemBump { /// ``` /// /// FIXME(breaking): this could well be a `Result<_, Failure>`. - pub fn get(&self) -> Option> { + pub fn get(&self) -> Option> { let alloc = self.try_alloc(Layout::new::())?; Some(Allocation { lifetime: alloc.lifetime, @@ -330,7 +340,7 @@ impl MemBump { /// access to the allocator. /// /// [`get`]: #method.get - pub fn get_at(&self, level: Level) -> Result, Failure> { + pub fn get_at(&self, level: Level) -> Result, Failure> { let alloc = self.try_alloc_at(Layout::new::(), level.0)?; Ok(Allocation { lifetime: alloc.lifetime, @@ -397,10 +407,11 @@ impl MemBump { "Tried to access an allocation that does not yet exist" ); - let ptr = self.data_ptr().as_ptr(); - // Safety: guaranteed by the caller. - let alloc = ptr.add(level.0); - let ptr = NonNull::new_unchecked(alloc).cast::(); + let base_ptr = self.data_ptr().as_ptr(); + // SAFETY: `level.0` is in bounds as assert above, or by the caller by having provided an + // existing allocation—all allocations we hand out are in bounds. + let alloc = unsafe { base_ptr.add(level.0) }; + let ptr = NonNull::new(alloc).unwrap().cast::(); debug_assert!( ptr.as_ptr().is_aligned(), @@ -476,7 +487,7 @@ impl MemBump { /// Get the number of already allocated bytes. pub fn level(&self) -> Level { - Level(self.index.get()) + Level(self.header.index.get()) } /// Reset the bump allocator. @@ -484,11 +495,11 @@ impl MemBump { /// This requires a unique reference to the allocator hence no allocation can be alive at this /// point. It will reset the internal count of used bytes to zero. pub fn reset(&mut self) { - self.index.set(0) + self.header.index.set(0) } fn try_alloc(&self, layout: Layout) -> Option> { - let consumed = self.index.get(); + let consumed = self.header.index.get(); match self.try_alloc_at(layout, consumed) { Ok(alloc) => return Some(alloc), Err(Failure::Exhausted) => return None, @@ -550,7 +561,7 @@ impl MemBump { let aligned = unsafe { // SAFETY: // * `0 <= at_aligned < length` in bounds as checked above. - (base_ptr as *mut u8).add(at_aligned) + base_ptr.byte_add(at_aligned) }; Ok(Allocation { @@ -563,11 +574,12 @@ impl MemBump { fn bump(&self, expect: usize, consume: usize) -> Result<(), usize> { debug_assert!(consume <= self.capacity()); debug_assert!(expect <= consume); - let prev = self.index.get(); + + let prev = self.header.index.get(); if prev != expect { Err(prev) } else { - self.index.set(consume); + self.header.index.set(consume); Ok(()) } } diff --git a/static-alloc/tests/leak.rs b/static-alloc/tests/leak.rs index 6dec983..d402d69 100644 --- a/static-alloc/tests/leak.rs +++ b/static-alloc/tests/leak.rs @@ -13,10 +13,10 @@ fn homogeneous() { assert_eq!(*next, 255); assert_eq!(*new_zero, 0); - let last = slab.leak(u64::max_value()).unwrap(); + let last = slab.leak(u64::MAX).unwrap(); assert_eq!(*next, 255); assert_eq!(*new_zero, 0); - assert_eq!(*last, u64::max_value()); + assert_eq!(*last, u64::MAX); assert!(slab.leak(0_u8).is_err()); } @@ -57,8 +57,10 @@ fn level() { assert_eq!(slab.level(), level); // Can not get the same level again. - assert_eq!(slab.leak_at(0u16, init).unwrap_err().kind(), - static_alloc::bump::Failure::Mismatch { observed: level }); + assert_eq!( + slab.leak_at(0u16, init).unwrap_err().kind(), + static_alloc::bump::Failure::Mismatch { observed: level } + ); let (othu16, next) = slab.leak_at(10u16, level).unwrap(); assert_eq!(*othu16, 10); diff --git a/static-alloc/tests/leak_box.rs b/static-alloc/tests/leak_box.rs index 631a48d..a0c0645 100644 --- a/static-alloc/tests/leak_box.rs +++ b/static-alloc/tests/leak_box.rs @@ -21,7 +21,7 @@ fn leak_box_drops() { #[test] fn leaking() { - struct PanicOnDrop(usize); + struct PanicOnDrop(#[expect(unused)] usize); impl Drop for PanicOnDrop { fn drop(&mut self) { panic!("Do not drop me."); From fded532e2a31664938e33bb8e9bfd50b36f01b75 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:32:03 +0200 Subject: [PATCH 06/10] Fixup all clippy lints --- static-alloc/src/bump.rs | 6 ++-- static-alloc/src/leaked.rs | 4 +-- static-alloc/src/unsync/bump.rs | 58 ++++++++++++++++++++------------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/static-alloc/src/bump.rs b/static-alloc/src/bump.rs index 823ac2f..237a391 100644 --- a/static-alloc/src/bump.rs +++ b/static-alloc/src/bump.rs @@ -672,7 +672,7 @@ impl Bump { let aligned = unsafe { // SAFETY: // * `0 <= at_aligned < length` in bounds as checked above. - (base_ptr as *mut u8).add(at_aligned) + base_ptr.byte_add(at_aligned) }; Ok(Allocation { @@ -743,6 +743,7 @@ impl Bump { /// TODO: will be deprecated sooner or later in favor of a method that does not move the /// resource on failure. // #[deprecated = "Use leak_box and initialize it with the value. This does not move the value in the failure case."] + #[expect(clippy::mut_from_ref)] // This is an allocator. pub fn leak(&self, val: V) -> Result<&mut V, LeakError> { match self.get::() { // SAFETY: Just allocated this for a `V`. @@ -786,6 +787,7 @@ impl Bump { /// resource on failure. /// // #[deprecated = "Use leak_box_at and initialize it with the value. This does not move the value in the failure case."] + #[expect(clippy::mut_from_ref)] // This is an allocator. pub fn leak_at(&self, val: V, level: Level) -> Result<(&mut V, Level), LeakError> { let alloc = match self.get_at::(level) { Ok(alloc) => alloc, @@ -1017,7 +1019,7 @@ unsafe impl<'alloc, T> LocalAlloc<'alloc> for Bump { } // No dealloc. - return Some(new_alloc); + Some(new_alloc) } unsafe fn dealloc(&'alloc self, _: alloc_traits::Allocation<'alloc>) { diff --git a/static-alloc/src/leaked.rs b/static-alloc/src/leaked.rs index b2aa571..9ea4b03 100644 --- a/static-alloc/src/leaked.rs +++ b/static-alloc/src/leaked.rs @@ -550,13 +550,13 @@ impl<'ctx, T> From<&'ctx mut [MaybeUninit]> for LeakBox<'ctx, [MaybeUninit impl AsRef for LeakBox<'_, T> { fn as_ref(&self) -> &T { - &**self + self } } impl AsMut for LeakBox<'_, T> { fn as_mut(&mut self) -> &mut T { - &mut **self + self } } diff --git a/static-alloc/src/unsync/bump.rs b/static-alloc/src/unsync/bump.rs index 8425466..29c2d9e 100644 --- a/static-alloc/src/unsync/bump.rs +++ b/static-alloc/src/unsync/bump.rs @@ -93,16 +93,6 @@ pub struct MemBump { data: UnsafeCell<[MaybeUninit]>, } -struct Header { - /// An index into the data field. This index - /// will always be an index to an element - /// that has not been allocated into. - /// Again this is wrapped in a Cell, - /// to allow modification with just a - /// &self reference. - index: Cell, -} - impl Bump { /// Create an allocator with uninitialized memory. /// @@ -130,10 +120,15 @@ impl MemBump { /// Allocate some space to use for a bump allocator. pub fn new(capacity: usize) -> alloc::boxed::Box { let layout = Self::layout_from_size(capacity).expect("Bad layout"); + // NOTE: if std allows, we'd very much like to use `Vec
::try_with_capacity` here + // instead. But currently we can't leak that into a `Box<[MaybeUninit
]>` which makes + // it unfortunately inert. let ptr = NonNull::new(unsafe { alloc::alloc::alloc(layout) }) .unwrap_or_else(|| alloc::alloc::handle_alloc_error(layout)); let ptr = ptr::slice_from_raw_parts_mut(ptr.as_ptr(), capacity); - unsafe { ptr::write(ptr as *mut Cell, Cell::new(0)) }; + // Safety: `layout_from_size` ensures at least the header fits, and the allocation was + // obviously successful as just seen. + unsafe { ptr::write(ptr as *mut Header, Header::empty()) }; unsafe { alloc::boxed::Box::from_raw(ptr as *mut MemBump) } } } @@ -157,9 +152,12 @@ impl MemBump { let offset = mem.as_ptr().align_offset(header.align()); // Align the memory for the header. let mem = mem.get_mut(offset..).ok_or(FromMemError { _inner: () })?; - mem.get_mut(..header.size()) - .ok_or(FromMemError { _inner: () })? - .fill(MaybeUninit::new(0)); + let hdr = mem + .get_mut(..header.size()) + .ok_or(FromMemError { _inner: () })?; + // Safety: `mem` is a mutable ref, and we just verified the size and align. We'd consider + // MaybeUninit::as_bytes` and copy instead but it's not stable. + unsafe { ptr::write(hdr.as_mut_ptr().cast(), Header::empty()) }; // Safety: we just verified the size, and pivoted to the correct alignment. Ok(unsafe { Self::from_mem_unchecked(mem) }) } @@ -198,7 +196,7 @@ impl MemBump { // Round down to the header alignment! The whole struct will occupy memory according to its // natural alignment. We must be prepared fro the `pad_to_align` so to speak. let datasize = datasize - datasize % header.align(); - debug_assert!(Self::layout_from_size(datasize).map_or(false, |l| l.size() <= mem.len())); + debug_assert!(Self::layout_from_size(datasize).is_ok_and(|l| l.size() <= mem.len())); let raw = mem.as_mut_ptr() as *mut u8; // Turn it into a fat pointer with correct metadata for a `MemBump`. @@ -501,8 +499,8 @@ impl MemBump { fn try_alloc(&self, layout: Layout) -> Option> { let consumed = self.header.index.get(); match self.try_alloc_at(layout, consumed) { - Ok(alloc) => return Some(alloc), - Err(Failure::Exhausted) => return None, + Ok(alloc) => Some(alloc), + Err(Failure::Exhausted) => None, Err(Failure::Mismatch { observed: _ }) => { unreachable!("Count in Cell concurrently modified, this UB") } @@ -517,9 +515,7 @@ impl MemBump { assert!(layout.size() > 0); let length = mem::size_of_val(&self.data); // We want to access contiguous slice, so cast to a single cell. - let data: &UnsafeCell<[MaybeUninit]> = - unsafe { &*(&self.data as *const _ as *const UnsafeCell<_>) }; - let base_ptr = data.get() as *mut u8; + let base_ptr = self.data.get().cast::(); let alignment = layout.align(); let requested = layout.size(); @@ -592,7 +588,7 @@ impl ops::Deref for Bump { let data_layout = Layout::new::>(); // Construct a point with the meta data of a slice to `data`, but pointing to the whole // struct instead. This meta data is later copied to the meta data of `bump` when cast. - let ptr = self as *const Self as *const MaybeUninit; + let ptr = (self as *const Self).cast::>(); let mem: *const [MaybeUninit] = ptr::slice_from_raw_parts(ptr, data_layout.size()); // Now we have a pointer to MemBump with length meta data of the data slice. let bump = unsafe { &*(mem as *const MemBump) }; @@ -607,7 +603,7 @@ impl ops::DerefMut for Bump { let data_layout = Layout::new::>(); // Construct a point with the meta data of a slice to `data`, but pointing to the whole // struct instead. This meta data is later copied to the meta data of `bump` when cast. - let ptr = self as *mut Self as *mut MaybeUninit; + let ptr = (self as *mut Self).cast::>(); let mem: *mut [MaybeUninit] = ptr::slice_from_raw_parts_mut(ptr, data_layout.size()); // Now we have a pointer to MemBump with length meta data of the data slice. let bump = unsafe { &mut *(mem as *mut MemBump) }; @@ -616,6 +612,24 @@ impl ops::DerefMut for Bump { } } +struct Header { + /// An index into the data field. This index + /// will always be an index to an element + /// that has not been allocated into. + /// Again this is wrapped in a Cell, + /// to allow modification with just a + /// &self reference. + index: Cell, +} + +impl Header { + const fn empty() -> Self { + Header { + index: Cell::new(0), + } + } +} + #[test] fn mem_bump_derefs_correctly() { let bump = Bump::::zeroed(); From 66e44def613f6d641341a17b5fe752a45f6666c1 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:24:43 +0200 Subject: [PATCH 07/10] Fix: unsound Bump/MemBump coercion Deref --- static-alloc/src/unsync/bump.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/static-alloc/src/unsync/bump.rs b/static-alloc/src/unsync/bump.rs index 29c2d9e..d437ae0 100644 --- a/static-alloc/src/unsync/bump.rs +++ b/static-alloc/src/unsync/bump.rs @@ -584,6 +584,15 @@ impl MemBump { impl ops::Deref for Bump { type Target = MemBump; fn deref(&self) -> &MemBump { + assert_eq!( + mem::offset_of!(Bump, _data), + // `data` follows header directly, using the macro requires a value for unsized types. + mem::size_of::
(), + "This `Bump<{}>` can not be used as a `MemBump` since the reinterpretation changes the data layout. (Hint: its alignment must be at most {}).", + core::any::type_name::(), + mem::size_of::
(), + ); + let from_layout = Layout::for_value(self); let data_layout = Layout::new::>(); // Construct a point with the meta data of a slice to `data`, but pointing to the whole @@ -599,6 +608,15 @@ impl ops::Deref for Bump { impl ops::DerefMut for Bump { fn deref_mut(&mut self) -> &mut MemBump { + assert_eq!( + mem::offset_of!(Bump, _data), + // `data` follows header directly, using the macro requires a value for unsized types. + mem::size_of::
(), + "This `Bump<{}>` can not be used as a `MemBump` since the reinterpretation changes the data layout. (Hint: its alignment must be at most {}).", + core::any::type_name::(), + mem::size_of::
(), + ); + let from_layout = Layout::for_value(self); let data_layout = Layout::new::>(); // Construct a point with the meta data of a slice to `data`, but pointing to the whole From ec058eecec9dac704c57f2a5f90b8e65a5546739 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:49:15 +0200 Subject: [PATCH 08/10] Avoid Bump monomorphization --- static-alloc/src/bump.rs | 510 ++++++++++++++++++++++++++------------- 1 file changed, 339 insertions(+), 171 deletions(-) diff --git a/static-alloc/src/bump.rs b/static-alloc/src/bump.rs index 237a391..81901d1 100644 --- a/static-alloc/src/bump.rs +++ b/static-alloc/src/bump.rs @@ -144,12 +144,13 @@ use alloc_traits::{AllocTime, LocalAlloc, NonZeroLayout}; /// demonstrate performance gains). /// /// WIP: slices. +#[repr(C)] pub struct Bump { /// While in shared state, an monotonic atomic counter of consumed bytes. /// /// While shared it is only mutated in `bump` which guarantees its invariants. In the mutable /// reference state it is modified arbitrarily. - consumed: AtomicUsize, + header: Header, /// Outer unsafe cell due to thread safety. /// Inner MaybeUninit because we padding may destroy initialization invariant @@ -157,6 +158,26 @@ pub struct Bump { storage: UnsafeCell>, } +/// A view of a bump allocator over an unsized arena. +/// +/// The primary way of constructing this is a by [`Bump`] with some chosen layout descriptor type. +/// It maintains the invariant of a tracking header being an accurate ledger for the use of its +/// associated memory region. This implies you must not be allowed to combine any header and data. +/// This strong association is protected by an area such as [`Bump`]. +/// +/// Note: You might think that we can +#[repr(C)] +#[derive(Clone, Copy)] +struct BumpView<'lt> { + header: &'lt Header, + storage: &'lt UnsafeCell<[MaybeUninit]>, +} + +#[repr(C)] +struct Header { + consumed: AtomicUsize, +} + /// A value could not be moved into a slab allocation. /// /// The error contains the value for which the allocation failed. Storing the value in the error @@ -285,7 +306,7 @@ impl Bump { /// The storage will contain uninitialized bytes. pub const fn uninit() -> Self { Bump { - consumed: AtomicUsize::new(0), + header: Header::empty(), storage: UnsafeCell::new(MaybeUninit::uninit()), } } @@ -297,7 +318,7 @@ impl Bump { /// but there is no good reason not to provide it regardless. pub fn zeroed() -> Self { Bump { - consumed: AtomicUsize::new(0), + header: Header::empty(), storage: UnsafeCell::new(MaybeUninit::zeroed()), } } @@ -307,7 +328,7 @@ impl Bump { /// Note that `storage` will never be dropped and there is no way to get it back. pub const fn new(storage: T) -> Self { Bump { - consumed: AtomicUsize::new(0), + header: Header::empty(), storage: UnsafeCell::new(MaybeUninit::new(storage)), } } @@ -352,7 +373,20 @@ impl Bump { /// // ------------- immutable borrow later used here /// ``` pub fn reset(&mut self) { - *self.consumed.get_mut() = 0; + self.header = Header::empty(); + } + + fn as_view(&self) -> BumpView<'_> { + BumpView { + header: &self.header, + storage: { + let base = self.storage.get(); + let len = mem::size_of::(); + let data = core::ptr::slice_from_raw_parts(base as *const _, len); + // Safety: covers exactly the memory of `storage`, which is a `MaybeUninit`. + unsafe { &*(data as *const UnsafeCell<[_]>) } + }, + } } /// Allocate a region of memory. @@ -364,7 +398,7 @@ impl Bump { /// `GlobalAlloc` this is explicitely forbidden to request and would allow any behaviour but we /// instead strictly check it. pub fn alloc(&self, layout: Layout) -> Option> { - Some(self.try_alloc(layout)?.ptr) + self.as_view().alloc(layout) } /// Try to allocate some layout with a precise base location. @@ -376,17 +410,7 @@ impl Bump { /// # Panics /// This function may panic if the provided `level` is from a different slab. pub fn alloc_at(&self, layout: Layout, level: Level) -> Result, Failure> { - let Allocation { - ptr, - lifetime, - level, - } = self.try_alloc_at(layout, level.0)?; - - Ok(Allocation { - ptr: ptr.cast(), - lifetime, - level, - }) + self.as_view().alloc_at(layout, level) } /// Get an allocation with detailed layout. @@ -396,7 +420,7 @@ impl Bump { /// /// [`Uninit`]: ../uninit/struct.Uninit.html pub fn get_layout(&self, layout: Layout) -> Option> { - self.try_alloc(layout) + self.as_view().get_layout(layout) } /// Get an allocation with detailed layout at a specific level. @@ -409,7 +433,7 @@ impl Bump { /// /// [`Uninit`]: ../uninit/struct.Uninit.html pub fn get_layout_at(&self, layout: Layout, at: Level) -> Result, Failure> { - self.try_alloc_at(layout, at.0) + self.as_view().get_layout_at(layout, at) } /// Get an allocation for a specific type. @@ -434,22 +458,7 @@ impl Bump { /// assert_eq!(**cell_ref, 0xff); /// ``` pub fn get(&self) -> Option> { - if mem::size_of::() == 0 { - return Some(self.zst_fake_alloc()); - } - - let layout = Layout::new::(); - let Allocation { - ptr, - lifetime, - level, - } = self.try_alloc(layout)?; - - Some(Allocation { - ptr: ptr.cast(), - lifetime, - level, - }) + self.as_view().get() } /// Get an allocation for a specific type at a specific level. @@ -458,30 +467,7 @@ impl Bump { /// /// [`get`]: #method.get pub fn get_at(&self, level: Level) -> Result, Failure> { - if mem::size_of::() == 0 { - let fake = self.zst_fake_alloc(); - // Note: zst_fake_alloc is a noop on the level, we may as well check after. - if fake.level != level { - return Err(Failure::Mismatch { - observed: fake.level, - }); - } - return Ok(fake); - } - - let layout = Layout::new::(); - let Allocation { - ptr, - lifetime, - level, - } = self.try_alloc_at(layout, level.0)?; - - Ok(Allocation { - // It has exactly size and alignment for `V` as requested. - ptr: ptr.cast(), - lifetime, - level, - }) + self.as_view().get_at(level) } /// Move a value into an owned allocation. @@ -539,8 +525,7 @@ impl Bump { /// drop(head); /// ``` pub fn leak_box(&self, val: V) -> Option> { - let Allocation { ptr, lifetime, .. } = self.get::()?; - Some(unsafe { LeakBox::new_from_raw_non_null(ptr, val, lifetime) }) + self.as_view().leak_box(val) } /// Move a value into an owned allocation. @@ -549,8 +534,7 @@ impl Bump { /// /// [`leak_box`]: #method.leak_box pub fn leak_box_at(&self, val: V, level: Level) -> Result, Failure> { - let Allocation { ptr, lifetime, .. } = self.get_at::(level)?; - Ok(unsafe { LeakBox::new_from_raw_non_null(ptr, val, lifetime) }) + self.as_view().leak_box_at(val, level) } /// Observe the current level. @@ -560,7 +544,7 @@ impl Bump { /// synchronization of memory accesses, only that the values observed by the caller are a /// monotonically increasing seequence while a shared reference exists. pub fn level(&self) -> Level { - Level(self.consumed.load(Ordering::SeqCst)) + self.as_view().level() } /// Get a pointer to an existing allocation at a specific level. @@ -577,109 +561,8 @@ impl Bump { /// - As a corollary, particular it must be in-bounds of the allocator's memory. /// - Another consequence, the result pointer must be aligned for the requested type. pub unsafe fn get_unchecked(&self, level: Level) -> Allocation<'_, V> { - debug_assert!(level.0 <= mem::size_of_val(&self.storage)); - - debug_assert!( - level <= self.level(), - "Tried to access an allocation that does not yet exist" - ); - - let base_ptr = self.storage.get() as *mut T as *mut u8; - - // SAFETY: `level.0` is in bounds as assert above, or by the caller by having provided an - // existing allocation—all allocations we hand out are in bounds. - let alloc = unsafe { base_ptr.add(level.0) }; - let ptr = NonNull::new(alloc).unwrap().cast::(); - - debug_assert!( - ptr.as_ptr().is_aligned(), - "Tried to access an allocation with improper type" - ); - - Allocation { - level, - lifetime: AllocTime::default(), - ptr, - } - } - - fn try_alloc(&self, layout: Layout) -> Option> { - // Guess zero, this will fail when we try to access it and it isn't. - let mut consumed = 0; - loop { - match self.try_alloc_at(layout, consumed) { - Ok(alloc) => return Some(alloc), - Err(Failure::Exhausted) => return None, - Err(Failure::Mismatch { observed }) => consumed = observed.0, - } - } - } - - /// Try to allocate some layout with a precise base location. - /// - /// The base location is the currently consumed byte count, without correction for the - /// alignment of the allocation. This will succeed if it can be allocate exactly at the - /// expected location. - /// - /// # Panics - /// This function panics if `expect_consumed` is larger than `length`. - fn try_alloc_at( - &self, - layout: Layout, - expect_consumed: usize, - ) -> Result, Failure> { - assert!(layout.size() > 0); - let length = mem::size_of::(); - let base_ptr = self.storage.get() as *mut T as *mut u8; - - let alignment = layout.align(); - let requested = layout.size(); - - // Ensure no overflows when calculating offets within. - assert!(expect_consumed <= length); - - let available = length.checked_sub(expect_consumed).unwrap(); - let ptr_to = base_ptr.wrapping_add(expect_consumed); - let offset = ptr_to.align_offset(alignment); - - if requested > available.saturating_sub(offset) { - return Err(Failure::Exhausted); // exhausted - } - - // `size` can not be zero, saturation will thus always make this true. - assert!(offset < available); - let at_aligned = expect_consumed.checked_add(offset).unwrap(); - let new_consumed = at_aligned.checked_add(requested).unwrap(); - // new_consumed - // = consumed + offset + requested [lines above] - // <= consumed + available [bail out: exhausted] - // <= length [first line of loop] - // So it's ok to store `allocated` into `consumed`. - assert!(new_consumed <= length); - assert!(at_aligned < length); - - // Try to actually allocate. - match self.bump(expect_consumed, new_consumed) { - Ok(()) => (), - Err(observed) => { - // Someone else was faster, if you want it then recalculate again. - return Err(Failure::Mismatch { - observed: Level(observed), - }); - } - } - - let aligned = unsafe { - // SAFETY: - // * `0 <= at_aligned < length` in bounds as checked above. - base_ptr.byte_add(at_aligned) - }; - - Ok(Allocation { - ptr: NonNull::new(aligned).unwrap(), - lifetime: AllocTime::default(), - level: Level(new_consumed), - }) + // Safety: forwarding requirements. + unsafe { self.as_view().get_unchecked(level) } } /// Allocate a value for the lifetime of the allocator. @@ -799,9 +682,208 @@ impl Bump { let mutref = unsafe { alloc.leak(val) }; Ok((mutref, level)) } +} + +impl<'lt> BumpView<'lt> { + pub fn alloc(self, layout: Layout) -> Option> { + Some(self.try_alloc(layout)?.ptr) + } + + pub fn alloc_at(self, layout: Layout, level: Level) -> Result, Failure> { + let Allocation { + ptr, + lifetime, + level, + } = self.try_alloc_at(layout, level.0)?; + + Ok(Allocation { + ptr: ptr.cast(), + lifetime, + level, + }) + } + + pub fn get_layout(self, layout: Layout) -> Option> { + self.try_alloc(layout) + } + + pub fn get_layout_at(self, layout: Layout, at: Level) -> Result, Failure> { + self.try_alloc_at(layout, at.0) + } + + pub fn get(self) -> Option> { + if mem::size_of::() == 0 { + return Some(self.zst_fake_alloc()); + } + + let layout = Layout::new::(); + let Allocation { + ptr, + lifetime, + level, + } = self.try_alloc(layout)?; + + Some(Allocation { + ptr: ptr.cast(), + lifetime, + level, + }) + } + + pub fn get_at(self, level: Level) -> Result, Failure> { + if mem::size_of::() == 0 { + let fake = self.zst_fake_alloc(); + // Note: zst_fake_alloc is a noop on the level, we may as well check after. + if fake.level != level { + return Err(Failure::Mismatch { + observed: fake.level, + }); + } + return Ok(fake); + } + + let layout = Layout::new::(); + let Allocation { + ptr, + lifetime, + level, + } = self.try_alloc_at(layout, level.0)?; + + Ok(Allocation { + // It has exactly size and alignment for `V` as requested. + ptr: ptr.cast(), + lifetime, + level, + }) + } + + pub fn leak_box(self, val: V) -> Option> { + let Allocation { ptr, lifetime, .. } = self.get::()?; + Some(unsafe { LeakBox::new_from_raw_non_null(ptr, val, lifetime) }) + } + + pub fn leak_box_at(self, val: V, level: Level) -> Result, Failure> { + let Allocation { ptr, lifetime, .. } = self.get_at::(level)?; + Ok(unsafe { LeakBox::new_from_raw_non_null(ptr, val, lifetime) }) + } + + pub fn level(&self) -> Level { + Level(self.header.consumed.load(Ordering::SeqCst)) + } + + /// # Safety + /// + /// - The level must refer to an existing allocation, i.e. it must previously have been + /// returned in [`Allocation::level`]. + /// - As a corollary, particular it must be in-bounds of the allocator's memory. + /// - Another consequence, the result pointer must be aligned for the requested type. + pub unsafe fn get_unchecked(self, level: Level) -> Allocation<'lt, V> { + debug_assert!(level.0 <= mem::size_of_val(&self.storage)); + + debug_assert!( + level <= self.level(), + "Tried to access an allocation that does not yet exist" + ); + + let base_ptr = self.storage.get().cast::(); + // SAFETY: `level.0` is in bounds as assert above, or by the caller by having provided an + // existing allocation—all allocations we hand out are in bounds. + let alloc = unsafe { base_ptr.add(level.0) }; + let ptr = NonNull::new(alloc).unwrap().cast::(); + + debug_assert!( + ptr.as_ptr().is_aligned(), + "Tried to access an allocation with improper type" + ); + + Allocation { + level, + lifetime: AllocTime::default(), + ptr, + } + } + + fn try_alloc(self, layout: Layout) -> Option> { + // Guess zero, this will fail when we try to access it and it isn't. + let mut consumed = 0; + loop { + match self.try_alloc_at(layout, consumed) { + Ok(alloc) => return Some(alloc), + Err(Failure::Exhausted) => return None, + Err(Failure::Mismatch { observed }) => consumed = observed.0, + } + } + } + + /// Try to allocate some layout with a precise base location. + /// + /// The base location is the currently consumed byte count, without correction for the + /// alignment of the allocation. This will succeed if it can be allocate exactly at the + /// expected location. + /// + /// # Panics + /// This function panics if `expect_consumed` is larger than `length`. + fn try_alloc_at( + self, + layout: Layout, + expect_consumed: usize, + ) -> Result, Failure> { + assert!(layout.size() > 0); + let length = self.storage.get().len(); + let base_ptr = self.storage.get().cast::(); + + let alignment = layout.align(); + let requested = layout.size(); + + // Ensure no overflows when calculating offets within. + assert!(expect_consumed <= length); + + let available = length.checked_sub(expect_consumed).unwrap(); + let ptr_to = base_ptr.wrapping_add(expect_consumed); + let offset = ptr_to.align_offset(alignment); + + if requested > available.saturating_sub(offset) { + return Err(Failure::Exhausted); // exhausted + } + + // `size` can not be zero, saturation will thus always make this true. + assert!(offset < available); + let at_aligned = expect_consumed.checked_add(offset).unwrap(); + let new_consumed = at_aligned.checked_add(requested).unwrap(); + // new_consumed + // = consumed + offset + requested [lines above] + // <= consumed + available [bail out: exhausted] + // <= length [first line of loop] + // So it's ok to store `allocated` into `consumed`. + assert!(new_consumed <= length); + assert!(at_aligned < length); + + // Try to actually allocate. + match self.bump(expect_consumed, new_consumed) { + Ok(()) => (), + Err(observed) => { + // Someone else was faster, if you want it then recalculate again. + return Err(Failure::Mismatch { + observed: Level(observed), + }); + } + } + + let aligned = unsafe { + // SAFETY: + // * `0 <= at_aligned < length` in bounds as checked above. + base_ptr.byte_add(at_aligned) + }; + + Ok(Allocation { + ptr: NonNull::new(aligned).unwrap(), + lifetime: AllocTime::default(), + level: Level(new_consumed), + }) + } /// 'Allocate' a ZST. - fn zst_fake_alloc(&self) -> Allocation<'_, Z> { + fn zst_fake_alloc(&self) -> Allocation<'lt, Z> { Allocation::for_zst(self.level()) } @@ -820,8 +902,19 @@ impl Bump { /// It also panics if the expected value is larger than the new value. fn bump(&self, expect_consumed: usize, new_consumed: usize) -> Result<(), usize> { assert!(expect_consumed <= new_consumed); - assert!(new_consumed <= mem::size_of::()); + assert!(new_consumed <= self.storage.get().len()); + self.header.bump(expect_consumed, new_consumed) + } +} + +impl Header { + const fn empty() -> Self { + Header { + consumed: AtomicUsize::new(0), + } + } + fn bump(&self, expect_consumed: usize, new_consumed: usize) -> Result<(), usize> { self.consumed .compare_exchange( expect_consumed, @@ -916,9 +1009,31 @@ impl LeakError { // SAFETY: at most one thread gets a pointer to each chunk of data. unsafe impl Sync for Bump {} +// SAFETY: at most one thread gets a pointer to each chunk of data. +unsafe impl Sync for BumpView<'_> {} +unsafe impl Send for BumpView<'_> {} + unsafe impl GlobalAlloc for Bump { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - Bump::alloc(self, layout) + // Safety: just handing over arguments exactly as is. These two allocators are 'compatible' + // in the sense they hold onto the same value handles. + unsafe { GlobalAlloc::alloc(&self.as_view(), layout) } + } + + unsafe fn realloc(&self, ptr: *mut u8, current: Layout, new_size: usize) -> *mut u8 { + // Safety: just handing over arguments exactly as is. These two allocators are 'compatible' + // in the sense they hold onto the same value handles. + unsafe { GlobalAlloc::realloc(&self.as_view(), ptr, current, new_size) } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + // We are a slab allocator and do not deallocate. + } +} + +unsafe impl GlobalAlloc for BumpView<'_> { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + BumpView::alloc(*self, layout) .map(NonNull::as_ptr) .unwrap_or_else(null_mut) } @@ -964,7 +1079,60 @@ fn layout_reallocated( unsafe impl<'alloc, T> LocalAlloc<'alloc> for Bump { fn alloc(&'alloc self, layout: NonZeroLayout) -> Option> { - let raw_alloc = Bump::get_layout(self, layout.into())?; + let raw_alloc = self.get_layout(layout.into())?; + Some(alloc_traits::Allocation { + ptr: raw_alloc.ptr, + layout, + lifetime: AllocTime::default(), + }) + } + + unsafe fn realloc( + &'alloc self, + alloc: alloc_traits::Allocation<'alloc>, + layout: NonZeroLayout, + ) -> Option> { + if alloc.ptr.as_ptr() as usize % layout.align() == 0 && alloc.layout.size() >= layout.size() + { + // Obvious fit, nothing to do. + return Some(alloc_traits::Allocation { + ptr: alloc.ptr, + layout, + lifetime: alloc.lifetime, + }); + } + + // TODO: we could try to allocate at the exact level that the allocation ends. If this + // succeeds, there is no copying necessary. This was the point of `Level` anyways. + + let new_alloc = LocalAlloc::alloc(self, layout)?; + + // Safety: + // - the old allocation is valid for the old size, as required of the caller. + // - the old allocation is valid for reads as it is an allocation of the allocator. + // - the new allocation is valid for the new size. + // - the new allocation is valid for writes as it was successful. + // - our effective copy is at most the old and new size. + unsafe { + core::ptr::copy_nonoverlapping( + alloc.ptr.as_ptr(), + new_alloc.ptr.as_ptr(), + layout.size().min(alloc.layout.size()).into(), + ); + } + + // No dealloc. + Some(new_alloc) + } + + unsafe fn dealloc(&'alloc self, _: alloc_traits::Allocation<'alloc>) { + // We are a slab allocator and do not deallocate. + } +} + +unsafe impl<'alloc> LocalAlloc<'alloc> for BumpView<'alloc> { + fn alloc(&'alloc self, layout: NonZeroLayout) -> Option> { + let raw_alloc = self.get_layout(layout.into())?; Some(alloc_traits::Allocation { ptr: raw_alloc.ptr, layout, From a11d7a54c72fb30e5aaae7ecdef41dd3eb8862e2 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:46:04 +0200 Subject: [PATCH 09/10] Post-Mono error for unsync::Bump Deref --- static-alloc/src/unsync/bump.rs | 57 ++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/static-alloc/src/unsync/bump.rs b/static-alloc/src/unsync/bump.rs index d437ae0..7ca1858 100644 --- a/static-alloc/src/unsync/bump.rs +++ b/static-alloc/src/unsync/bump.rs @@ -62,6 +62,30 @@ use crate::leaked::LeakBox; /// handle_request(&local_page, request); /// } /// ``` +/// +/// ## Coercion into [`MemBump`] +/// +/// This allocator nominally implements [`Deref`](core::ops::Deref) into [`MemBump`]. However, the +/// layout of these two structs is equivalent only for types that have at most an alignment of +/// [`usize`] (e.g. arrays of `u8`, `u16`, or more integers depending on the platform pointer size). +/// +/// Warning: An attempt to use this dereference with an invalid type will trigger a +/// post-monomorphization error! This choice was made to avoid complicated encoding of the +/// precondition into a viral trait bound and considering you're likely to use very concrete +/// instances that either work, or would have been UB. +/// +/// For instance, this will *fail* to compile: +/// +/// ```compile_fail +/// use static_alloc::unsync::{Bump, MemBump}; +/// +/// #[repr(align(32))] +/// struct HighlyAligned([u8; 128]); +/// +/// let mut arena: Bump = Bump::uninit(); +/// // Fails here, attempting to resolve `impl Deref for Bump`. +/// let _ = arena.get::(); +/// ``` #[repr(C)] pub struct Bump { /// The index used in allocation. @@ -581,17 +605,24 @@ impl MemBump { } } +struct EnsureDerefIsApplicable(core::marker::PhantomData); + +impl EnsureDerefIsApplicable { + pub const ASSERT: () = { + if mem::offset_of!(Bump, _data) != mem::size_of::
() { + panic!( + // `data` follows header directly, using the macro requires a value for unsized types. + "This `unsync::Bump` can not be used as a `MemBump` since the reinterpretation changes the data layout. (Hint: its alignment must be at most `usize`).", + ); + } + }; +} + impl ops::Deref for Bump { type Target = MemBump; fn deref(&self) -> &MemBump { - assert_eq!( - mem::offset_of!(Bump, _data), - // `data` follows header directly, using the macro requires a value for unsized types. - mem::size_of::
(), - "This `Bump<{}>` can not be used as a `MemBump` since the reinterpretation changes the data layout. (Hint: its alignment must be at most {}).", - core::any::type_name::(), - mem::size_of::
(), - ); + // This provokes post-mono error! + let _: () = EnsureDerefIsApplicable::::ASSERT; let from_layout = Layout::for_value(self); let data_layout = Layout::new::>(); @@ -608,14 +639,8 @@ impl ops::Deref for Bump { impl ops::DerefMut for Bump { fn deref_mut(&mut self) -> &mut MemBump { - assert_eq!( - mem::offset_of!(Bump, _data), - // `data` follows header directly, using the macro requires a value for unsized types. - mem::size_of::
(), - "This `Bump<{}>` can not be used as a `MemBump` since the reinterpretation changes the data layout. (Hint: its alignment must be at most {}).", - core::any::type_name::(), - mem::size_of::
(), - ); + // This provokes post-mono error! + let _: () = EnsureDerefIsApplicable::::ASSERT; let from_layout = Layout::for_value(self); let data_layout = Layout::new::>(); From 94ae4cadce5003101446ccb5595f24acbc7f7ba7 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:42:04 +0200 Subject: [PATCH 10/10] Implement allocation methods on MemBump --- static-alloc/src/bump.rs | 456 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 455 insertions(+), 1 deletion(-) diff --git a/static-alloc/src/bump.rs b/static-alloc/src/bump.rs index 81901d1..43e4dd7 100644 --- a/static-alloc/src/bump.rs +++ b/static-alloc/src/bump.rs @@ -158,6 +158,22 @@ pub struct Bump { storage: UnsafeCell>, } +/// An unsized bump allocator arena. +/// +/// This does not enforce any particular alignment on its storage. You can, in general, expect that +/// it is at least 4-byte aligned but should not rely on it for soundness purposes. +#[repr(C)] +pub struct BumpSlice { + /// While in shared state, an monotonic atomic counter of consumed bytes. + /// + /// While shared it is only mutated in `bump` which guarantees its invariants. In the mutable + /// reference state it is modified arbitrarily. + header: Header, + + /// See [`Bump::storage`], same function but with a concrete type. + storage: UnsafeCell<[MaybeUninit]>, +} + /// A view of a bump allocator over an unsized arena. /// /// The primary way of constructing this is a by [`Bump`] with some chosen layout descriptor type. @@ -166,7 +182,6 @@ pub struct Bump { /// This strong association is protected by an area such as [`Bump`]. /// /// Note: You might think that we can -#[repr(C)] #[derive(Clone, Copy)] struct BumpView<'lt> { header: &'lt Header, @@ -333,6 +348,91 @@ impl Bump { } } + /// Convert this into a type-erased byte-slice bump allocator. + /// + /// This returns `None` if the layout of `T` causes the layout of `self` to not be compatible + /// with the layout of [`BumpSlice`]. The criteria is that `T` must have an alignment not greater + /// than that of `usize` (which is used internally for accounting the used portion). + /// + /// For instance, this is guaranteed to work: + /// + /// ``` + /// use static_alloc::Bump; + /// + /// let byte_array: Bump<[u8; 128]> = Bump::uninit(); + /// assert!(byte_array.as_bump_slice().is_some()); + /// let usize_array: Bump<[usize; 128]> = Bump::uninit(); + /// assert!(usize_array.as_bump_slice().is_some()); + /// ``` + /// + /// On the other hand, this is very unlikely to work on any platform: + /// + /// ``` + /// # if core::mem::size_of::() < 32 { + /// use static_alloc::Bump; + /// + /// #[repr(C, align(32))] + /// struct WhyAlignSoHigh([u8; 128]); + /// + /// let oof: Bump = Bump::uninit(); + /// assert!(oof.as_bump_slice().is_none()); + /// # } + /// ``` + pub const fn as_bump_slice(&self) -> Option<&BumpSlice> { + // Safety: + if mem::offset_of!(Self, storage) != mem::size_of::
() { + return None; + } + + let data_len = mem::size_of::(); + // Construct a point with the meta data of a slice to `data`, but pointing to the whole + // struct instead. This meta data is later copied to the meta data of `bump` when cast. + let ptr = (self as *const Self).cast::>(); + let mem: *const [MaybeUninit] = core::ptr::slice_from_raw_parts(ptr, data_len); + + // Safety: The layout of this type is compatible with ours. Both are `repr(C)`, so we can go + // field-by-field and the total layout. + // + // - Firstly, they share a `Header` field. + // - Secondly, `data` is located immediately behind `Header`. In `self` we verify this + // above, in `BumpSlice` that follows directly from `u8` being 1-aligned. + // - The alignment requirement of `MemBump`, exactly that of `Header`, is fulfilled as that + // is also a field of `Self`. + // - The size of both values is compatible. We construct the metadata such that the return + // value covers exactly the length of the `storage` field. What follows is padding to + // cover the alignment requirement. The `Self` type has the same alignment and same offset + // past-the-field and hence will receive the same padding. + Some(unsafe { &*(mem as *const BumpSlice) }) + } + + /// Mutable variant of [`Self::as_bump_slice`]. + pub const fn as_mut_bump_slice(&mut self) -> Option<&mut BumpSlice> { + // Safety: + if mem::offset_of!(Self, storage) != mem::size_of::
() { + return None; + } + + let data_len = mem::size_of::(); + // Construct a point with the meta data of a slice to `data`, but pointing to the whole + // struct instead. This meta data is later copied to the meta data of `bump` when cast. + let ptr = (self as *mut Self).cast::>(); + let mem: *mut [MaybeUninit] = core::ptr::slice_from_raw_parts_mut(ptr, data_len); + + // Safety: The layout of this type is compatible with ours. Both are `repr(C)`, so we can go + // field-by-field and the total layout. + // + // - Firstly, they share a `Header` field. + // - Secondly, `data` is located immediately behind `Header`. In `self` we verify this + // above, in `MemBump` that follows directly from `u8` being 1-aligned. + // - The alignment requirement of `MemBump`, exactly that of `Header`, is fulfilled as that + // is also a field of `Self`. + // - The size of both values is compatible. We construct the metadata such that the return + // value covers exactly the length of the `storage` field. What follows is padding to + // cover the alignment requirement. The `Self` type has the same alignment and same offset + // past-the-field and hence will receive the same padding. + Some(unsafe { &mut *(mem as *mut BumpSlice) }) + } + /// Reset the bump allocator. /// /// Requires a mutable reference, as no allocations can be active when doing it. This behaves @@ -684,6 +784,360 @@ impl Bump { } } +impl BumpSlice { + /// Reset the bump allocator. + /// + /// Requires a mutable reference, as no allocations can be active when doing it. This behaves + /// as if a fresh instance was assigned but it does not overwrite the bytes in the backing + /// storage. (You can unsafely rely on this). + /// + /// ## Usage + /// + /// ``` + /// # use static_alloc::bump::{Bump, BumpSlice}; + /// let mut stack_buf = Bump::::uninit(); + /// let stack_buf = stack_buf.as_mut_bump_slice().unwrap(); + /// + /// let bytes = stack_buf.leak(0usize.to_be_bytes()).unwrap(); + /// // Now the bump allocator is full. + /// assert!(stack_buf.leak(0u8).is_err()); + /// + /// // We can reuse if we are okay with forgetting the previous value. + /// stack_buf.reset(); + /// let val = stack_buf.leak(0usize).unwrap(); + /// ``` + /// + /// Trying to use the previous value does not work, as the stack is still borrowed. Note that + /// any user unsafely tracking the lifetime must also ensure this through proper lifetimes that + /// guarantee that borrows are alive for appropriate times. + /// + /// ```compile_fail + /// // error[E0502]: cannot borrow `stack_buf` as mutable because it is also borrowed as immutable + /// # use static_alloc::bump::{Bump, BumpSlice}; + /// let mut stack_buf = Bump::::uninit(); + /// let stack_buf = stack_buf.as_mut_bump_slice().unwrap(); + /// + /// let bytes = stack_buf.leak(0usize).unwrap(); + /// // --------- immutably borrow occurs here + /// stack_buf.reset(); + /// // ^^^^^^^ mutable borrow occurs here. + /// let other = stack_buf.leak(0usize).unwrap(); + /// + /// *bytes += *other; + /// // ------------- immutable borrow later used here + /// ``` + pub fn reset(&mut self) { + self.header = Header::empty(); + } + + fn as_view(&self) -> BumpView<'_> { + BumpView { + header: &self.header, + storage: &self.storage, + } + } + + /// Allocate a region of memory. + /// + /// This is a safe alternative to [GlobalAlloc::alloc](#impl-GlobalAlloc). + /// + /// # Panics + /// This function will panic if the requested layout has a size of `0`. For the use in a + /// `GlobalAlloc` this is explicitely forbidden to request and would allow any behaviour but we + /// instead strictly check it. + pub fn alloc(&self, layout: Layout) -> Option> { + self.as_view().alloc(layout) + } + + /// Try to allocate some layout with a precise base location. + /// + /// The base location is the currently consumed byte count, without correction for the + /// alignment of the allocation. This will succeed if it can be allocate exactly at the + /// expected location. + /// + /// # Panics + /// This function may panic if the provided `level` is from a different slab. + pub fn alloc_at(&self, layout: Layout, level: Level) -> Result, Failure> { + self.as_view().alloc_at(layout, level) + } + + /// Get an allocation with detailed layout. + /// + /// Provides an [`Uninit`] wrapping several aspects of initialization in a safe interface, + /// bound by the lifetime of the reference to the allocator. + /// + /// [`Uninit`]: ../uninit/struct.Uninit.html + pub fn get_layout(&self, layout: Layout) -> Option> { + self.as_view().get_layout(layout) + } + + /// Get an allocation with detailed layout at a specific level. + /// + /// Provides an [`Uninit`] wrapping several aspects of initialization in a safe interface, + /// bound by the lifetime of the reference to the allocator. + /// + /// Since the underlying allocation is the same, it would be `unsafe` but justified to fuse + /// this allocation with the preceding or succeeding one. + /// + /// [`Uninit`]: ../uninit/struct.Uninit.html + pub fn get_layout_at(&self, layout: Layout, at: Level) -> Result, Failure> { + self.as_view().get_layout_at(layout, at) + } + + /// Get an allocation for a specific type. + /// + /// It is not yet initialized but provides a safe interface for that initialization. + /// + /// ## Usage + /// + /// ``` + /// # use static_alloc::bump::{Bump, BumpSlice}; + /// use core::cell::{Ref, RefCell}; + /// + /// let backing: Bump<[Ref<'static, usize>; 1]> = Bump::uninit(); + /// let slab = backing.as_bump_slice().unwrap(); + /// + /// let data = RefCell::new(0xff); + /// + /// // We can place a `Ref` here but we did not yet. + /// let alloc = slab.get::>().unwrap(); + /// let cell_ref = unsafe { + /// alloc.leak(data.borrow()) + /// }; + /// + /// assert_eq!(**cell_ref, 0xff); + /// ``` + pub fn get(&self) -> Option> { + self.as_view().get() + } + + /// Get an allocation for a specific type at a specific level. + /// + /// See [`get`] for usage. + /// + /// [`get`]: #method.get + pub fn get_at(&self, level: Level) -> Result, Failure> { + self.as_view().get_at(level) + } + + /// Move a value into an owned allocation. + /// + /// For safely initializing a value _after_ a successful allocation, see [`LeakBox::write`]. + /// + /// [`LeakBox::write`]: ../leaked/struct.LeakBox.html#method.write + /// + /// ## Usage + /// + /// This can be used to push the value into a caller provided stack buffer where it lives + /// longer than the current stack frame. For example, you might create a linked list with a + /// dynamic number of values living in the frame below while still being dropped properly. This + /// is impossible to do with a return value. + /// + /// ``` + /// # use static_alloc::bump::{Bump, BumpSlice}; + /// # use static_alloc::leaked::LeakBox; + /// fn rand() -> usize { 4 } + /// + /// enum Chain<'buf, T> { + /// Tail, + /// Link(T, LeakBox<'buf, Self>), + /// } + /// + /// fn make_chain(buf: &BumpSlice, mut new_node: impl FnMut() -> T) + /// -> Option> + /// { + /// let count = rand(); + /// let mut chain = Chain::Tail; + /// for _ in 0..count { + /// let node = new_node(); + /// chain = Chain::Link(node, buf.leak_box(chain)?); + /// } + /// Some(chain) + /// } + /// + /// struct Node (usize); + /// impl Drop for Node { + /// fn drop(&mut self) { + /// println!("Dropped {}", self.0); + /// } + /// } + /// let mut counter = 0..; + /// let new_node = || Node(counter.next().unwrap()); + /// + /// let buffer: Bump<[u8; 128]> = Bump::uninit(); + /// let buffer = buffer.as_bump_slice().unwrap(); + /// let head = make_chain(buffer, new_node).unwrap(); + /// + /// // Prints the message in reverse order. + /// // Dropped 3 + /// // Dropped 2 + /// // Dropped 1 + /// // Dropped 0 + /// drop(head); + /// ``` + pub fn leak_box(&self, val: V) -> Option> { + self.as_view().leak_box(val) + } + + /// Move a value into an owned allocation. + /// + /// See [`leak_box`] for usage. + /// + /// [`leak_box`]: #method.leak_box + pub fn leak_box_at(&self, val: V, level: Level) -> Result, Failure> { + self.as_view().leak_box_at(val, level) + } + + /// Observe the current level. + /// + /// Keep in mind that concurrent usage of the same slab may modify the level before you are + /// able to use it in `alloc_at`. Calling this method provides also no other guarantees on + /// synchronization of memory accesses, only that the values observed by the caller are a + /// monotonically increasing seequence while a shared reference exists. + pub fn level(&self) -> Level { + self.as_view().level() + } + + /// Get a pointer to an existing allocation at a specific level. + /// + /// The resulting pointer may be used to access an arbitrary allocation starting at the pointer + /// (i.e. including additional allocations immediately afterwards) but the caller is + /// responsible for ensuring that these accesses do not overlap other accesses. There must be + /// no more life [`LeakBox`] to any allocation being accessed this way. + /// + /// # Safety + /// + /// - The level must refer to an existing allocation, i.e. it must previously have been + /// returned in [`Allocation::level`]. + /// - As a corollary, particular it must be in-bounds of the allocator's memory. + /// - Another consequence, the result pointer must be aligned for the requested type. + pub unsafe fn get_unchecked(&self, level: Level) -> Allocation<'_, V> { + // Safety: forwarding requirements. + unsafe { self.as_view().get_unchecked(level) } + } + + /// Allocate a value for the lifetime of the allocator. + /// + /// The value is leaked in the sense that + /// + /// 1. the drop implementation of the allocated value is never called; + /// 2. reusing the memory for another allocation in the same `Bump` requires manual unsafe code + /// to handle dropping and reinitialization. + /// + /// However, it does not mean that the underlying memory used for the allocated value is never + /// reclaimed. If the `Bump` itself is a stack value then it will get reclaimed together with + /// it. + /// + /// ## Safety notice + /// + /// It is important to understand that it is undefined behaviour to reuse the allocation for + /// the *whole lifetime* of the returned reference. That is, dropping the allocation in-place + /// while the reference is still within its lifetime comes with the exact same unsafety caveats + /// as [`ManuallyDrop::drop`]. + /// + /// ``` + /// # use static_alloc::bump::{Bump, BumpSlice}; + /// #[derive(Debug, Default)] + /// struct FooBar { + /// // ... + /// # _private: [u8; 1], + /// } + /// + /// let local: Bump<[FooBar; 3]> = Bump::uninit(); + /// let local = local.as_bump_slice().unwrap(); + /// let one = local.leak(FooBar::default()).unwrap(); + /// + /// // Dangerous but justifiable. + /// let one = unsafe { + /// // Ensures there is no current mutable borrow. + /// core::ptr::drop_in_place(&mut *one); + /// }; + /// ``` + /// + /// ## Usage + /// + /// ``` + /// use static_alloc::bump::{Bump, BumpSlice}; + /// + /// let local: Bump<[u64; 3]> = Bump::uninit(); + /// let local = local.as_bump_slice().unwrap(); + /// + /// let one = local.leak(0_u64).unwrap(); + /// assert_eq!(*one, 0); + /// *one = 42; + /// ``` + /// + /// ## Limitations + /// + /// Only sized values can be allocated in this manner for now, unsized values are blocked on + /// stabilization of [`ptr::slice_from_raw_parts`]. We can not otherwise get a fat pointer to + /// the allocated region. + /// + /// [`ptr::slice_from_raw_parts`]: https://github.com/rust-lang/rust/issues/36925 + /// [`ManuallyDrop::drop`]: https://doc.rust-lang.org/beta/std/mem/struct.ManuallyDrop.html#method.drop + /// + /// TODO: will be deprecated sooner or later in favor of a method that does not move the + /// resource on failure. + // #[deprecated = "Use leak_box and initialize it with the value. This does not move the value in the failure case."] + #[expect(clippy::mut_from_ref)] // This is an allocator. + pub fn leak(&self, val: V) -> Result<&mut V, LeakError> { + match self.get::() { + // SAFETY: Just allocated this for a `V`. + Some(alloc) => Ok(unsafe { alloc.leak(val) }), + None => Err(LeakError::new(val, Failure::Exhausted)), + } + } + + /// Allocate a value with a precise location. + /// + /// See [`leak`] for basics on allocation of values. + /// + /// The level is an identifer for a base location (more at [`level`]). This will succeed if it + /// can be allocate exactly at the expected location. + /// + /// This method will return the new level of the slab allocator. A next allocation at the + /// returned level will be placed next to this allocation, only separated by necessary padding + /// from alignment. In particular, this is the same strategy as applied for the placement of + /// `#[repr(C)]` struct members. (Except for the final padding at the last member to the full + /// struct alignment.) + /// + /// ## Usage + /// + /// ``` + /// use static_alloc::bump::{Bump, BumpSlice}; + /// + /// let local: Bump<[u64; 3]> = Bump::uninit(); + /// let local = local.as_bump_slice().unwrap(); + /// + /// let base = local.level(); + /// let (one, level) = local.leak_at(1_u64, base).unwrap(); + /// // Will panic when an allocation happens in between. + /// let (two, _) = local.leak_at(2_u64, level).unwrap(); + /// + /// assert_eq!((one as *const u64).wrapping_offset(1), two); + /// ``` + /// + /// [`leak`]: #method.leak + /// [`level`]: #method.level + /// + /// TODO: will be deprecated sooner or later in favor of a method that does not move the + /// resource on failure. + /// + // #[deprecated = "Use leak_box_at and initialize it with the value. This does not move the value in the failure case."] + #[expect(clippy::mut_from_ref)] // This is an allocator. + pub fn leak_at(&self, val: V, level: Level) -> Result<(&mut V, Level), LeakError> { + let alloc = match self.get_at::(level) { + Ok(alloc) => alloc, + Err(err) => return Err(LeakError::new(val, err)), + }; + + // SAFETY: Just allocated this for a `V`. + let level = alloc.level; + let mutref = unsafe { alloc.leak(val) }; + Ok((mutref, level)) + } +} + impl<'lt> BumpView<'lt> { pub fn alloc(self, layout: Layout) -> Option> { Some(self.try_alloc(layout)?.ptr)