diff --git a/Cargo.toml b/Cargo.toml index 8ee1345..0548e5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "3" members = [ "alloc-traits", "exit-stack", @@ -8,3 +9,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/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 d97737a..73e9344 100644 --- a/fill/Cargo.toml +++ b/fill/Cargo.toml @@ -2,15 +2,16 @@ 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..ab1dade 100644 --- a/static-alloc/Cargo.toml +++ b/static-alloc/Cargo.toml @@ -1,14 +1,16 @@ [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" -authors = ["Aurelia Molzer <5550310+197g@users.noreply.github.com>"] -edition = "2018" -license = "MIT OR Apache-2.0 OR Zlib" +edition = "2024" documentation = "https://docs.rs/static-alloc" -repository = "https://github.com/197g/static-alloc" -readme = "Readme.md" categories = ["embedded", "memory-management", "no-std"] +rust-version = "1.85" + +authors.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true [package.metadata.docs.rs] all-features = true @@ -19,9 +21,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 +36,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/src/bump.rs b/static-alloc/src/bump.rs index 6e00140..43e4dd7 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}; @@ -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,41 @@ 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. +/// 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 +#[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 +321,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 +333,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,11 +343,96 @@ 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)), } } + /// 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 @@ -352,7 +473,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 +498,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. @@ -375,18 +509,8 @@ 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 { - let Allocation { - ptr, - lifetime, - level, - } = self.try_alloc_at(layout, level.0)?; - - Ok(Allocation { - ptr: ptr.cast(), - lifetime, - level, - }) + pub fn alloc_at(&self, layout: Layout, level: Level) -> Result, Failure> { + self.as_view().alloc_at(layout, level) } /// Get an allocation with detailed layout. @@ -396,7 +520,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 +533,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. @@ -433,23 +557,8 @@ 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, - }) + pub fn get(&self) -> Option> { + self.as_view().get() } /// Get an allocation for a specific type at a specific level. @@ -457,31 +566,8 @@ impl Bump { /// See [`get`] for usage. /// /// [`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, - }) + pub fn get_at(&self, level: Level) -> Result, Failure> { + self.as_view().get_at(level) } /// Move a value into an owned allocation. @@ -539,8 +625,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 +634,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 +644,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,106 +661,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; - let alloc = base_ptr.add(level.0); - let ptr = NonNull::new_unchecked(alloc).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 as *mut u8).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. @@ -740,6 +726,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`. @@ -783,6 +770,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, @@ -794,76 +782,641 @@ impl Bump { let mutref = unsafe { alloc.leak(val) }; Ok((mutref, level)) } - - /// 'Allocate' a ZST. - fn zst_fake_alloc(&self) -> Allocation<'_, Z> { - Allocation::for_zst(self.level()) - } - - /// Try to bump the monotonic, atomic consume counter. - /// - /// This is the only place doing shared modification to `self.consumed`. - /// - /// Returns `Ok` if the consume counter was as expected. Monotonicty and atomicity guarantees - /// to the caller that no overlapping range can succeed as well. This allocates the range to - /// the caller. - /// - /// Returns the observed consume counter in an `Err` if it was not as expected. - /// - /// ## Panics - /// This function panics if either argument exceeds the byte length of the underlying memory. - /// 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::()); - - self.consumed - .compare_exchange( - expect_consumed, - new_consumed, - Ordering::SeqCst, - Ordering::SeqCst, - ) - .map(drop) - } } -impl<'alloc, T> Allocation<'alloc, T> { - /// Write a value into the allocation and leak it. - /// - /// ## Safety +impl BumpSlice { + /// Reset the bump allocator. /// - /// Must have been allocated for a layout that fits the layout of T previously. The pointer - /// must not be aliased. + /// 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 /// - /// Consider the alternative [`Bump::leak`] to safely allocate and directly leak a value. - /// - /// [`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() - } - - /// Write a value into the allocation and own it. + /// ``` + /// # use static_alloc::bump::{Bump, BumpSlice}; + /// let mut stack_buf = Bump::::uninit(); + /// let stack_buf = stack_buf.as_mut_bump_slice().unwrap(); /// - /// ## Safety + /// let bytes = stack_buf.leak(0usize.to_be_bytes()).unwrap(); + /// // Now the bump allocator is full. + /// assert!(stack_buf.leak(0u8).is_err()); /// - /// Must have been allocated for a layout that fits the layout of T previously. The pointer - /// must not be aliased. + /// // We can reuse if we are okay with forgetting the previous value. + /// stack_buf.reset(); + /// let val = stack_buf.leak(0usize).unwrap(); + /// ``` /// - /// ## Usage + /// 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) + } + + 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<'lt, Z> { + Allocation::for_zst(self.level()) + } + + /// Try to bump the monotonic, atomic consume counter. + /// + /// This is the only place doing shared modification to `self.consumed`. + /// + /// Returns `Ok` if the consume counter was as expected. Monotonicty and atomicity guarantees + /// to the caller that no overlapping range can succeed as well. This allocates the range to + /// the caller. + /// + /// Returns the observed consume counter in an `Err` if it was not as expected. + /// + /// ## Panics + /// This function panics if either argument exceeds the byte length of the underlying memory. + /// 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 <= 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, + new_consumed, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .map(drop) + } +} + +impl<'alloc, T> Allocation<'alloc, T> { + /// Write a value into the allocation and leak it. + /// + /// ## Safety + /// + /// Must have been allocated for a layout that fits the layout of T previously. The pointer + /// must not be aliased. + /// + /// ## Usage + /// + /// Consider the alternative [`Bump::leak`] to safely allocate and directly leak a value. + /// + /// [`Bump::leak`]: struct.Bump.html#method.leak + pub unsafe fn leak(self, val: T) -> &'alloc mut T { + // 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. + /// + /// ## Safety + /// + /// Must have been allocated for a layout that fits the layout of T previously. The pointer + /// must not be aliased. + /// + /// ## Usage /// /// Consider the alternative [`Bump::leak`] to safely allocate and directly leak a value. /// /// [`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 +1425,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. @@ -910,17 +1463,39 @@ 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) } 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 +1503,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) } @@ -956,7 +1533,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, @@ -995,13 +1625,23 @@ 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); + 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 91ded7a..9ea4b03 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) }, } } @@ -548,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 14a0575..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. @@ -80,13 +104,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 @@ -126,10 +144,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) } } } @@ -153,9 +176,13 @@ 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) }) } @@ -173,13 +200,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); @@ -188,7 +220,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`. @@ -315,7 +347,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 +362,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 +429,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 +509,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,14 +517,14 @@ 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, + Ok(alloc) => Some(alloc), + Err(Failure::Exhausted) => None, Err(Failure::Mismatch { observed: _ }) => { unreachable!("Count in Cell concurrently modified, this UB") } @@ -506,9 +539,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(); @@ -550,7 +581,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,24 +594,41 @@ 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(()) } } } +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 { + // This provokes post-mono error! + let _: () = EnsureDerefIsApplicable::::ASSERT; + 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 // 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) }; @@ -591,11 +639,14 @@ impl ops::Deref for Bump { impl ops::DerefMut for Bump { fn deref_mut(&mut self) -> &mut MemBump { + // This provokes post-mono error! + let _: () = EnsureDerefIsApplicable::::ASSERT; + 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 // 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) }; @@ -604,6 +655,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(); 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."); 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.