#![allow(unsafe_code)]
use core::ptr::NonNull;
use core::sync::atomic::{AtomicU8, Ordering};
use allocator_api2::alloc::{AllocError, Allocator, Global, Layout};
use super::{Memory, MemoryError, Work};
mod shared;
pub(super) use shared::SharedText;
pub(super) fn retain<T, W: Work>(
values: allocator_api2::vec::Vec<T, AccountAllocator<W>>,
) -> allocator_api2::vec::Vec<T, AccountAllocator> {
transfer(values, |allocator| {
let memory = allocator.execution.into_storage_owner(allocator.storage);
AccountAllocator::new(memory)
})
}
pub(super) fn scope<T, W: Work>(
values: allocator_api2::vec::Vec<T, AccountAllocator>,
execution: &W,
) -> allocator_api2::vec::Vec<T, AccountAllocator<W>> {
transfer(values, |allocator| {
let (execution, storage) = execution.with_storage_owner(allocator.execution);
AccountAllocator::with_storage(execution, storage)
})
}
fn transfer<T, W: Work, V: Work>(
values: allocator_api2::vec::Vec<T, AccountAllocator<W>>,
convert: impl FnOnce(AccountAllocator<W>) -> AccountAllocator<V>,
) -> allocator_api2::vec::Vec<T, AccountAllocator<V>> {
let (pointer, length, capacity, allocator) = values.into_raw_parts_with_alloc();
let allocator = convert(allocator);
unsafe { allocator_api2::vec::Vec::from_raw_parts_in(pointer, length, capacity, allocator) }
}
#[derive(Debug)]
pub(super) struct AccountAllocator<W: Work = Memory> {
execution: W,
storage: W::Retained,
denied: AtomicU8,
#[cfg(test)]
attempts: core::sync::atomic::AtomicUsize,
#[cfg(test)]
fail_backend: core::sync::atomic::AtomicBool,
}
impl<W: Work> AccountAllocator<W> {
pub(super) fn new(execution: W) -> Self {
let retained = execution.retain_storage();
Self::with_storage(execution, retained)
}
fn with_storage(execution: W, storage: W::Retained) -> Self {
Self {
execution,
storage,
denied: AtomicU8::new(0),
#[cfg(test)]
attempts: core::sync::atomic::AtomicUsize::new(0),
#[cfg(test)]
fail_backend: core::sync::atomic::AtomicBool::new(false),
}
}
pub(super) fn memory(&self) -> &Memory {
self.execution.memory()
}
fn into_memory(self) -> Memory {
self.execution.into_storage_owner(self.storage)
}
pub(super) fn work(&self) -> &W {
&self.execution
}
pub(super) fn clear_failure(&self) {
self.denied.store(0, Ordering::Relaxed);
}
pub(super) fn failure(&self) -> MemoryError {
match self.denied.load(Ordering::Relaxed) {
1 => MemoryError::LimitExceeded,
2 => MemoryError::WorkExceeded,
_ => MemoryError::OutOfMemory,
}
}
fn reserve(&self, bytes: usize) -> Result<super::Reservation<'_>, AllocError> {
self.memory().account.reserve(bytes).map_err(|_| {
self.denied.store(1, Ordering::Relaxed);
AllocError
})
}
fn admit(&self, bytes: usize) -> Result<(), AllocError> {
self.execution.charge_work(bytes as u64).map_err(|_| {
self.denied.store(2, Ordering::Relaxed);
AllocError
})
}
#[cfg(test)]
fn backend_attempt(&self) -> Result<(), AllocError> {
self.attempts.fetch_add(1, Ordering::Relaxed);
if self.fail_backend.load(Ordering::Relaxed) {
Err(AllocError)
} else {
Ok(())
}
}
}
unsafe impl<W: Work> Allocator for AccountAllocator<W> {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
let reservation = self.reserve(layout.size())?;
self.admit(layout.size())?;
#[cfg(test)]
self.backend_attempt()?;
let block = Global.allocate(layout)?;
reservation.commit();
Ok(block)
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe {
Global.deallocate(ptr, layout);
}
self.memory().account.release(layout.size());
}
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old: Layout,
new: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
let reservation = self.reserve(new.size())?;
self.admit(new.size())?;
self.admit(old.size())?;
#[cfg(test)]
self.backend_attempt()?;
let block = unsafe { Global.grow(ptr, old, new)? };
self.memory().account.release(old.size());
reservation.commit();
Ok(block)
}
unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old: Layout,
new: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
let reservation = self.reserve(new.size())?;
self.admit(new.size())?;
self.admit(old.size())?;
#[cfg(test)]
self.backend_attempt()?;
let block = unsafe { Global.grow_zeroed(ptr, old, new)? };
self.memory().account.release(old.size());
reservation.commit();
Ok(block)
}
}
unsafe impl<W: Work> allocator_api2_hashbrown::alloc::Allocator for AccountAllocator<W> {
fn allocate(
&self,
layout: Layout,
) -> Result<NonNull<[u8]>, allocator_api2_hashbrown::alloc::AllocError> {
<Self as Allocator>::allocate(self, layout)
.map_err(|_| allocator_api2_hashbrown::alloc::AllocError)
}
fn allocate_zeroed(
&self,
layout: Layout,
) -> Result<NonNull<[u8]>, allocator_api2_hashbrown::alloc::AllocError> {
<Self as Allocator>::allocate_zeroed(self, layout)
.map_err(|_| allocator_api2_hashbrown::alloc::AllocError)
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe {
<Self as Allocator>::deallocate(self, ptr, layout);
}
}
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old: Layout,
new: Layout,
) -> Result<NonNull<[u8]>, allocator_api2_hashbrown::alloc::AllocError> {
unsafe { <Self as Allocator>::grow(self, ptr, old, new) }
.map_err(|_| allocator_api2_hashbrown::alloc::AllocError)
}
unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old: Layout,
new: Layout,
) -> Result<NonNull<[u8]>, allocator_api2_hashbrown::alloc::AllocError> {
unsafe { <Self as Allocator>::grow_zeroed(self, ptr, old, new) }
.map_err(|_| allocator_api2_hashbrown::alloc::AllocError)
}
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old: Layout,
new: Layout,
) -> Result<NonNull<[u8]>, allocator_api2_hashbrown::alloc::AllocError> {
unsafe { <Self as Allocator>::shrink(self, ptr, old, new) }
.map_err(|_| allocator_api2_hashbrown::alloc::AllocError)
}
}
#[cfg(test)]
mod tests {
use alloc::sync::Arc;
use core::sync::atomic::Ordering;
use super::super::{Memory, MemoryAccount, MemoryError};
#[test]
fn both_allocator_interfaces_share_live_capacity_and_growth_peak() {
let memory = Memory::new(Arc::new(MemoryAccount::new(24, usize::MAX)));
let mut current = memory.vector::<u8>();
current.try_extend_from_slice(&[7; 8]).unwrap();
let mut table_interface = allocator_api2_hashbrown::vec::Vec::<u8, _>::new_in(
super::AccountAllocator::new(memory.clone()),
);
table_interface.try_reserve_exact(8).unwrap();
table_interface.push(42);
assert_eq!(memory.used(), 16);
let attempts = table_interface.allocator().attempts.load(Ordering::Relaxed);
assert!(table_interface.try_reserve_exact(8).is_err());
assert_eq!(
table_interface.allocator().failure(),
MemoryError::LimitExceeded
);
assert_eq!(
table_interface.allocator().attempts.load(Ordering::Relaxed),
attempts
);
assert_eq!(&*table_interface, &[42]);
assert_eq!(table_interface.capacity(), 8);
assert_eq!(memory.used(), 16);
drop(current);
assert_eq!(memory.used(), 8);
table_interface.try_reserve_exact(8).unwrap();
assert_eq!(&*table_interface, &[42]);
assert_eq!(memory.used(), table_interface.capacity());
drop(table_interface);
assert_eq!(memory.used(), 0);
}
#[test]
fn quota_rejection_never_reaches_the_backend() {
let memory = Memory::new(Arc::new(MemoryAccount::new(0, usize::MAX)));
let mut bytes = memory.vector::<u8>();
assert_eq!(bytes.try_reserve_exact(1), Err(MemoryError::LimitExceeded));
assert_eq!(bytes.values.allocator().attempts.load(Ordering::Relaxed), 0);
assert_eq!(memory.used(), 0);
}
#[test]
fn backend_failure_rolls_back_a_pending_reservation() {
let memory = Memory::new(Arc::new(MemoryAccount::new(16, usize::MAX)));
let mut bytes = memory.vector::<u8>();
bytes
.values
.allocator()
.fail_backend
.store(true, Ordering::Relaxed);
assert_eq!(bytes.try_reserve_exact(8), Err(MemoryError::OutOfMemory));
assert_eq!(bytes.capacity(), 0);
assert_eq!(memory.used(), 0);
}
#[test]
fn failed_growth_keeps_old_bytes_and_old_charge() {
let memory = Memory::new(Arc::new(MemoryAccount::new(32, usize::MAX)));
let mut bytes = memory.vector::<u8>();
bytes.try_reserve_exact(8).unwrap();
bytes.try_push(42).unwrap();
bytes
.values
.allocator()
.fail_backend
.store(true, Ordering::Relaxed);
assert_eq!(bytes.try_reserve_exact(8), Err(MemoryError::OutOfMemory));
assert_eq!(&*bytes, &[42]);
assert_eq!(bytes.capacity(), 8);
assert_eq!(memory.used(), 8);
}
}