Skip to content

Commit

Permalink
TFW allocators aren't stabilized
Browse files Browse the repository at this point in the history
  • Loading branch information
Jake-Shadle committed Aug 29, 2021
1 parent be41b38 commit 07373b7
Show file tree
Hide file tree
Showing 5 changed files with 1,635 additions and 68 deletions.
371 changes: 371 additions & 0 deletions breakpad-handler/src/alloc.rs
@@ -0,0 +1,371 @@
#![allow(unused_unsafe)]

mod page_allocator;
mod page_vec;
mod raw_vec;

use std::{
alloc::{Layout, LayoutError},
fmt,
ptr::{self, NonNull},
};

/// Copy of [`AllocError`](std::alloc::AllocError)
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct AllocError;

impl fmt::Display for AllocError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("memory allocation failed")
}
}

/// The error type for `try_reserve` methods.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TryReserveError {
/// Error due to the computed capacity exceeding the collection's maximum
/// (usually `isize::MAX` bytes).
CapacityOverflow,

/// The memory allocator returned an error
AllocError {
/// The layout of allocation request that failed
layout: Layout,
},
}

impl From<LayoutError> for TryReserveError {
#[inline]
fn from(_: LayoutError) -> Self {
TryReserveError::CapacityOverflow
}
}

impl fmt::Display for TryReserveError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("memory allocation failed")?;
let reason = match &self {
TryReserveError::CapacityOverflow => {
" because the computed capacity exceeded the collection's maximum"
}
TryReserveError::AllocError { .. } => " because the memory allocator returned a error",
};
fmt.write_str(reason)
}
}

/// Copy of [`AllocRef`](std::alloc::AllocRef)
pub unsafe trait AllocRef {
/// Attempts to allocate a block of memory.
///
/// On success, returns a [`NonNull<[u8]>`](std::mem::NonNull) meeting the size and alignment guarantees of `layout`.
///
/// The returned block may have a larger size than specified by `layout.size()`, and may or may
/// not have its contents initialized.
///
/// # Errors
///
/// Returning `Err` indicates that either memory is exhausted or `layout` does not meet
/// allocator's size or alignment constraints.
///
/// Implementations are encouraged to return `Err` on memory exhaustion rather than panicking or
/// aborting, but this is not a strict requirement. (Specifically: it is *legal* to implement
/// this trait atop an underlying native allocation library that aborts on memory exhaustion.)
///
/// Clients wishing to abort computation in response to an allocation error are encouraged to
/// call the [`handle_alloc_error`](alloc::alloc::handle_alloc_error) function, rather than
/// directly invoking `panic!` or similar.
fn alloc(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>;

/// Behaves like `alloc`, but also ensures that the returned memory is zero-initialized.
///
/// # Errors
///
/// Returning `Err` indicates that either memory is exhausted or `layout` does not meet
/// allocator's size or alignment constraints.
///
/// Implementations are encouraged to return `Err` on memory exhaustion rather than panicking or
/// aborting, but this is not a strict requirement. (Specifically: it is *legal* to implement
/// this trait atop an underlying native allocation library that aborts on memory exhaustion.)
///
/// Clients wishing to abort computation in response to an allocation error are encouraged to
/// call the [`handle_alloc_error`](alloc::alloc::handle_alloc_error) function, rather than
/// directly invoking `panic!` or similar.
fn alloc_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
let ptr = self.alloc(layout)?;
// SAFETY: `alloc` returns a valid memory block
unsafe {
(*ptr.as_ptr())
.as_mut_ptr()
.write_bytes(0, (*ptr.as_ptr()).len())
}
Ok(ptr)
}

/// Deallocates the memory referenced by `ptr`.
///
/// # Safety
///
/// * `ptr` must denote a block of memory [*currently allocated*] via this allocator, and
/// * `layout` must [*fit*] that block of memory.
///
/// [*currently allocated*]: #currently-allocated-memory
/// [*fit*]: #memory-fitting
unsafe fn dealloc(&self, ptr: NonNull<u8>, layout: Layout);

/// Attempts to extend the memory block.
///
/// Returns a new [`NonNull<[u8]>`](std::mem::NonNull) containing a pointer and the actual size of the allocated
/// memory. The pointer is suitable for holding data described by `new_layout`. To accomplish
/// this, the allocator may extend the allocation referenced by `ptr` to fit the new layout.
///
/// If this returns `Ok`, then ownership of the memory block referenced by `ptr` has been
/// transferred to this allocator. The memory may or may not have been freed, and should be
/// considered unusable unless it was transferred back to the caller again via the return value
/// of this method.
///
/// If this method returns `Err`, then ownership of the memory block has not been transferred to
/// this allocator, and the contents of the memory block are unaltered.
///
/// # Safety
///
/// * `ptr` must denote a block of memory [*currently allocated*] via this allocator.
/// * `old_layout` must [*fit*] that block of memory (The `new_layout` argument need not fit it.).
/// * `new_layout.size()` must be greater than or equal to `old_layout.size()`.
///
/// [*currently allocated*]: #currently-allocated-memory
/// [*fit*]: #memory-fitting
///
/// # Errors
///
/// Returns `Err` if the new layout does not meet the allocator's size and alignment
/// constraints of the allocator, or if growing otherwise fails.
///
/// Implementations are encouraged to return `Err` on memory exhaustion rather than panicking or
/// aborting, but this is not a strict requirement. (Specifically: it is *legal* to implement
/// this trait atop an underlying native allocation library that aborts on memory exhaustion.)
///
/// Clients wishing to abort computation in response to an allocation error are encouraged to
/// call the [`handle_alloc_error`](alloc::alloc::handle_alloc_error) function, rather than
/// directly invoking `panic!` or similar.
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
debug_assert!(
new_layout.size() >= old_layout.size(),
"`new_layout.size()` must be greater than or equal to `old_layout.size()`"
);

let new_ptr = self.alloc(new_layout)?;

// SAFETY: because `new_layout.size()` must be greater than or equal to
// `old_layout.size()`, both the old and new memory allocation are valid for reads and
// writes for `old_layout.size()` bytes. Also, because the old allocation wasn't yet
// deallocated, it cannot overlap `new_ptr`. Thus, the call to `copy_nonoverlapping` is
// safe. The safety contract for `dealloc` must be upheld by the caller.
unsafe {
ptr::copy_nonoverlapping(
ptr.as_ptr(),
(*new_ptr.as_ptr()).as_mut_ptr(),
old_layout.size(),
);
self.dealloc(ptr, old_layout);
}

Ok(new_ptr)
}

/// Behaves like `grow`, but also ensures that the new contents are set to zero before being
/// returned.
///
/// The memory block will contain the following contents after a successful call to
/// `grow_zeroed`:
/// * Bytes `0..old_layout.size()` are preserved from the original allocation.
/// * Bytes `old_layout.size()..old_size` will either be preserved or zeroed, depending on
/// the allocator implementation. `old_size` refers to the size of the memory block prior
/// to the `grow_zeroed` call, which may be larger than the size that was originally
/// requested when it was allocated.
/// * Bytes `old_size..new_size` are zeroed. `new_size` refers to the size of the memory
/// block returned by the `grow_zeroed` call.
///
/// # Safety
///
/// * `ptr` must denote a block of memory [*currently allocated*] via this allocator.
/// * `old_layout` must [*fit*] that block of memory (The `new_layout` argument need not fit it.).
/// * `new_layout.size()` must be greater than or equal to `old_layout.size()`.
///
/// [*currently allocated*]: #currently-allocated-memory
/// [*fit*]: #memory-fitting
///
/// # Errors
///
/// Returns `Err` if the new layout does not meet the allocator's size and alignment
/// constraints of the allocator, or if growing otherwise fails.
///
/// Implementations are encouraged to return `Err` on memory exhaustion rather than panicking or
/// aborting, but this is not a strict requirement. (Specifically: it is *legal* to implement
/// this trait atop an underlying native allocation library that aborts on memory exhaustion.)
///
/// Clients wishing to abort computation in response to an allocation error are encouraged to
/// call the [`handle_alloc_error`](alloc::alloc::handle_alloc_error) function, rather than
/// directly invoking `panic!` or similar.
unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
debug_assert!(
new_layout.size() >= old_layout.size(),
"`new_layout.size()` must be greater than or equal to `old_layout.size()`"
);

let new_ptr = self.alloc_zeroed(new_layout)?;

// SAFETY: because `new_layout.size()` must be greater than or equal to
// `old_layout.size()`, both the old and new memory allocation are valid for reads and
// writes for `old_layout.size()` bytes. Also, because the old allocation wasn't yet
// deallocated, it cannot overlap `new_ptr`. Thus, the call to `copy_nonoverlapping` is
// safe. The safety contract for `dealloc` must be upheld by the caller.
unsafe {
ptr::copy_nonoverlapping(
ptr.as_ptr(),
(*new_ptr.as_ptr()).as_mut_ptr(),
old_layout.size(),
);
self.dealloc(ptr, old_layout);
}

Ok(new_ptr)
}

/// Attempts to shrink the memory block.
///
/// Returns a new [`NonNull<[u8]>`](std::mem::NonNull) containing a pointer and the actual size of the allocated
/// memory. The pointer is suitable for holding data described by `new_layout`. To accomplish
/// this, the allocator may shrink the allocation referenced by `ptr` to fit the new layout.
///
/// If this returns `Ok`, then ownership of the memory block referenced by `ptr` has been
/// transferred to this allocator. The memory may or may not have been freed, and should be
/// considered unusable unless it was transferred back to the caller again via the return value
/// of this method.
///
/// If this method returns `Err`, then ownership of the memory block has not been transferred to
/// this allocator, and the contents of the memory block are unaltered.
///
/// # Safety
///
/// * `ptr` must denote a block of memory [*currently allocated*] via this allocator.
/// * `old_layout` must [*fit*] that block of memory (The `new_layout` argument need not fit it.).
/// * `new_layout.size()` must be smaller than or equal to `old_layout.size()`.
///
/// [*currently allocated*]: #currently-allocated-memory
/// [*fit*]: #memory-fitting
///
/// # Errors
///
/// Returns `Err` if the new layout does not meet the allocator's size and alignment
/// constraints of the allocator, or if shrinking otherwise fails.
///
/// Implementations are encouraged to return `Err` on memory exhaustion rather than panicking or
/// aborting, but this is not a strict requirement. (Specifically: it is *legal* to implement
/// this trait atop an underlying native allocation library that aborts on memory exhaustion.)
///
/// Clients wishing to abort computation in response to an allocation error are encouraged to
/// call the [`handle_alloc_error`](alloc::alloc::handle_alloc_error) function, rather than
/// directly invoking `panic!` or similar.
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
debug_assert!(
new_layout.size() <= old_layout.size(),
"`new_layout.size()` must be smaller than or equal to `old_layout.size()`"
);

let new_ptr = self.alloc(new_layout)?;

// SAFETY: because `new_layout.size()` must be lower than or equal to
// `old_layout.size()`, both the old and new memory allocation are valid for reads and
// writes for `new_layout.size()` bytes. Also, because the old allocation wasn't yet
// deallocated, it cannot overlap `new_ptr`. Thus, the call to `copy_nonoverlapping` is
// safe. The safety contract for `dealloc` must be upheld by the caller.
unsafe {
ptr::copy_nonoverlapping(
ptr.as_ptr(),
(*new_ptr.as_ptr()).as_mut_ptr(),
new_layout.size(),
);
self.dealloc(ptr, old_layout);
}

Ok(new_ptr)
}

/// Creates a "by reference" adaptor for this instance of `AllocRef`.
///
/// The returned adaptor also implements `AllocRef` and will simply borrow this.
#[inline(always)]
fn by_ref(&self) -> &Self {
self
}
}

unsafe impl<A> AllocRef for &A
where
A: AllocRef + ?Sized,
{
#[inline]
fn alloc(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
(**self).alloc(layout)
}

#[inline]
fn alloc_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
(**self).alloc_zeroed(layout)
}

#[inline]
unsafe fn dealloc(&self, ptr: NonNull<u8>, layout: Layout) {
// SAFETY: the safety contract must be upheld by the caller
unsafe { (**self).dealloc(ptr, layout) }
}

#[inline]
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
// SAFETY: the safety contract must be upheld by the caller
unsafe { (**self).grow(ptr, old_layout, new_layout) }
}

#[inline]
unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
// SAFETY: the safety contract must be upheld by the caller
unsafe { (**self).grow_zeroed(ptr, old_layout, new_layout) }
}

#[inline]
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
// SAFETY: the safety contract must be upheld by the caller
unsafe { (**self).shrink(ptr, old_layout, new_layout) }
}
}

0 comments on commit 07373b7

Please sign in to comment.