use std::sync::{Arc, Mutex, OnceLock, Weak};
use super::heap::MemoryHeapBase;
use super::wire::{export_heap, MemoryBase};
use super::{IMemory, IMemoryHeap};
use crate::binder::SIBinder;
use crate::error::{Result, StatusCode};
pub const ALLOCATION_ALIGNMENT: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Chunk {
start: usize, size: usize, free: bool,
}
struct BestFit {
chunks: Vec<Chunk>,
granules_per_page: usize,
}
impl BestFit {
fn new(heap_size: usize, page: usize) -> Self {
Self {
chunks: vec![Chunk {
start: 0,
size: heap_size / ALLOCATION_ALIGNMENT,
free: true,
}],
granules_per_page: page / ALLOCATION_ALIGNMENT,
}
}
fn page_pad(&self, start: usize) -> usize {
start.wrapping_neg() & (self.granules_per_page - 1)
}
fn alloc(&mut self, bytes: usize, page_aligned: bool) -> Option<usize> {
let size = bytes.div_ceil(ALLOCATION_ALIGNMENT).max(1);
let mut best: Option<usize> = None;
for (i, c) in self.chunks.iter().enumerate() {
let extra = if page_aligned {
self.page_pad(c.start)
} else {
0
};
if c.free && c.size >= size + extra {
if best.is_none_or(|b| c.size < self.chunks[b].size) {
best = Some(i);
}
if c.size == size {
break;
}
}
}
let i = best?;
let Chunk {
start,
size: free_size,
..
} = self.chunks[i];
let extra = if page_aligned {
self.page_pad(start)
} else {
0
};
let mut replacement = Vec::with_capacity(3);
if extra > 0 {
replacement.push(Chunk {
start,
size: extra,
free: true,
});
}
replacement.push(Chunk {
start: start + extra,
size,
free: false,
});
let tail = free_size - size - extra;
if tail > 0 {
replacement.push(Chunk {
start: start + extra + size,
size: tail,
free: true,
});
}
self.chunks.splice(i..=i, replacement);
Some((start + extra) * ALLOCATION_ALIGNMENT)
}
fn dealloc(&mut self, offset: usize) -> bool {
let start = offset / ALLOCATION_ALIGNMENT;
let Some(i) = self.chunks.iter().position(|c| c.start == start) else {
return false;
};
if self.chunks[i].free {
return false;
}
self.chunks[i].free = true;
if i + 1 < self.chunks.len() && self.chunks[i + 1].free {
self.chunks[i].size += self.chunks[i + 1].size;
self.chunks.remove(i + 1);
}
if i > 0 && self.chunks[i - 1].free {
self.chunks[i - 1].size += self.chunks[i].size;
self.chunks.remove(i);
}
true
}
fn free_bytes(&self) -> usize {
self.chunks
.iter()
.filter(|c| c.free)
.map(|c| c.size * ALLOCATION_ALIGNMENT)
.sum()
}
fn largest_free_bytes(&self) -> usize {
self.chunks
.iter()
.filter(|c| c.free)
.map(|c| c.size * ALLOCATION_ALIGNMENT)
.max()
.unwrap_or(0)
}
}
pub struct MemoryDealer {
heap: Arc<MemoryHeapBase>,
heap_binder: SIBinder,
allocator: Mutex<BestFit>,
}
impl std::fmt::Debug for MemoryDealer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryDealer")
.field("heap_size", &self.heap.size())
.field("free", &self.free_space())
.finish()
}
}
impl MemoryDealer {
pub fn new(size: usize, flags: u32) -> Result<Arc<Self>> {
Self::new_named(size, flags, "MemoryDealer")
}
pub fn new_named(size: usize, flags: u32, name: &str) -> Result<Arc<Self>> {
let heap = Arc::new(MemoryHeapBase::new_named(size, flags, name)?);
Self::over(heap)
}
pub fn over(heap: Arc<MemoryHeapBase>) -> Result<Arc<Self>> {
if heap.as_ptr().is_none() {
return Err(StatusCode::InvalidOperation);
}
let page = rustix::param::page_size();
let allocator = Mutex::new(BestFit::new(heap.size(), page));
Ok(Arc::new(Self {
heap_binder: export_heap(heap.clone()),
heap,
allocator,
}))
}
pub fn allocate(self: &Arc<Self>, size: usize) -> Result<Allocation> {
self.allocate_inner(size, false)
}
pub fn allocate_page_aligned(self: &Arc<Self>, size: usize) -> Result<Allocation> {
self.allocate_inner(size, true)
}
fn allocate_inner(self: &Arc<Self>, size: usize, page_aligned: bool) -> Result<Allocation> {
if size == 0 {
return Err(StatusCode::BadValue);
}
let offset = self
.allocator
.lock()
.unwrap_or_else(|e| e.into_inner())
.alloc(size, page_aligned)
.ok_or(StatusCode::NoMemory)?;
let mem = MemoryBase::new(
self.heap.clone() as Arc<dyn IMemoryHeap>,
self.heap_binder.clone(),
offset,
size,
)?;
Ok(Allocation {
mem: Arc::new(mem),
binder: OnceLock::new(),
dealer: Arc::downgrade(self),
})
}
fn deallocate(&self, offset: usize) {
let mut a = self.allocator.lock().unwrap_or_else(|e| e.into_inner());
if !a.dealloc(offset) {
log::error!("MemoryDealer: block at offset {offset:#x} is not allocated");
}
}
pub fn heap(&self) -> &Arc<MemoryHeapBase> {
&self.heap
}
pub fn heap_binder(&self) -> &SIBinder {
&self.heap_binder
}
pub fn free_space(&self) -> usize {
self.allocator
.lock()
.unwrap_or_else(|e| e.into_inner())
.free_bytes()
}
pub fn largest_free_block(&self) -> usize {
self.allocator
.lock()
.unwrap_or_else(|e| e.into_inner())
.largest_free_bytes()
}
}
pub struct Allocation {
mem: Arc<MemoryBase>,
binder: OnceLock<SIBinder>,
dealer: Weak<MemoryDealer>,
}
impl std::fmt::Debug for Allocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Allocation")
.field("offset", &self.offset())
.field("size", &self.size())
.finish()
}
}
impl Allocation {
pub fn export(&self) -> SIBinder {
self.binder
.get_or_init(|| self.mem.clone().export())
.clone()
}
pub fn memory_base(&self) -> &Arc<MemoryBase> {
&self.mem
}
pub fn offset(&self) -> usize {
self.mem.offset()
}
pub fn size(&self) -> usize {
self.mem.size()
}
pub fn read_at(&self, off: usize, dst: &mut [u8]) -> Result<()> {
let base = self.window(off, dst.len())?;
self.dealer()?.heap.read_at(base, dst)
}
pub fn write_at(&self, off: usize, src: &[u8]) -> Result<()> {
let base = self.window(off, src.len())?;
self.dealer()?.heap.write_at(base, src)
}
fn dealer(&self) -> Result<Arc<MemoryDealer>> {
self.dealer.upgrade().ok_or(StatusCode::DeadObject)
}
fn window(&self, off: usize, len: usize) -> Result<usize> {
let end = off.checked_add(len).ok_or(StatusCode::BadValue)?;
if end > self.mem.size() {
return Err(StatusCode::BadValue);
}
Ok(self.mem.offset() + off)
}
}
impl IMemory for Allocation {
fn memory(&self) -> &dyn IMemoryHeap {
self.mem.memory()
}
fn offset(&self) -> usize {
Allocation::offset(self)
}
fn size(&self) -> usize {
Allocation::size(self)
}
}
impl Drop for Allocation {
fn drop(&mut self) {
if let Some(d) = self.dealer.upgrade() {
d.deallocate(self.mem.offset());
}
}
}
#[cfg(all(
test,
any(target_os = "linux", target_os = "android", target_os = "macos")
))]
mod tests {
use super::*;
fn page() -> usize {
rustix::param::page_size()
}
#[test]
fn best_fit_splits_and_coalesces() {
let mut a = BestFit::new(1024, 4096);
let x = a.alloc(100, false).unwrap(); let y = a.alloc(32, false).unwrap(); let z = a.alloc(200, false).unwrap(); assert_eq!((x, y, z), (0, 128, 160));
assert_eq!(a.free_bytes(), 1024 - 384);
assert!(a.dealloc(y));
assert_eq!(a.alloc(1, false), Some(128));
assert!(a.dealloc(x));
assert!(a.dealloc(z));
assert!(a.dealloc(128));
assert_eq!(a.chunks.len(), 1);
assert_eq!(a.free_bytes(), 1024);
assert!(!a.dealloc(128));
assert!(!a.dealloc(7));
}
#[test]
fn best_fit_prefers_smallest_sufficient_hole() {
let mut a = BestFit::new(2048, 4096);
let big = a.alloc(1024, false).unwrap();
let _keep1 = a.alloc(64, false).unwrap();
let small = a.alloc(64, false).unwrap();
let _keep2 = a.alloc(64, false).unwrap();
a.dealloc(big);
a.dealloc(small);
assert_eq!(a.alloc(64, false), Some(1088));
}
#[test]
fn page_aligned_allocation_pads_then_reuses_pad() {
let mut a = BestFit::new(page() * 4, page());
let first = a.alloc(64, false).unwrap();
assert_eq!(first, 0);
let aligned = a.alloc(100, true).unwrap();
assert_eq!(aligned % page(), 0);
assert_eq!(aligned, page());
assert_eq!(a.alloc(32, false), Some(64));
}
#[test]
fn dealer_allocations_are_disjoint_windows_and_return_on_drop() {
let d = MemoryDealer::new(page() * 2, 0).unwrap();
let total = d.free_space();
let a = d.allocate(100).unwrap();
let b = d.allocate(page()).unwrap();
assert_eq!(
a.size(),
100,
"the window is the requested size, not the granule"
);
assert_eq!(b.size(), page());
assert!(a.offset() + a.size() <= b.offset());
assert_eq!(IMemory::memory(&a).size(), page() * 2);
a.write_at(0, b"alpha").unwrap();
b.write_at(0, b"beta").unwrap();
let mut s = [0u8; 5];
a.read_at(0, &mut s).unwrap();
assert_eq!(&s, b"alpha");
let mut h = [0u8; 4];
d.heap().read_at(b.offset(), &mut h).unwrap();
assert_eq!(&h, b"beta");
assert_eq!(
a.write_at(120, b"123456789").unwrap_err(),
StatusCode::BadValue
);
assert_eq!(d.free_space(), total - 128 - page());
drop(a);
drop(b);
assert_eq!(d.free_space(), total);
assert_eq!(d.largest_free_block(), total);
}
#[test]
fn dealer_exhaustion_is_no_memory_and_zero_is_bad_value() {
let d = MemoryDealer::new(page(), 0).unwrap();
let keep = d.allocate(page() - 32).unwrap();
assert_eq!(d.allocate(64).unwrap_err(), StatusCode::NoMemory);
assert_eq!(d.allocate(0).unwrap_err(), StatusCode::BadValue);
let last = d.allocate(32).unwrap();
assert_eq!(last.size(), 32);
assert_eq!(d.free_space(), 0);
drop(keep);
assert_eq!(d.largest_free_block(), page() - 32);
drop(last);
assert_eq!(d.largest_free_block(), page());
}
#[test]
fn allocation_outlives_dealer_gracefully() {
let d = MemoryDealer::new(page(), 0).unwrap();
let a = d.allocate(64).unwrap();
let binder = a.export();
assert!(binder == a.export(), "export() is stable");
drop(d);
assert_eq!(
a.read_at(0, &mut [0u8; 1]).unwrap_err(),
StatusCode::DeadObject
);
assert_eq!(IMemory::memory(&a).size(), page());
drop(a); }
#[test]
fn dealer_refuses_unmapped_heap() {
let heap =
Arc::new(MemoryHeapBase::new(page(), super::super::FLAG_DONT_MAP_LOCALLY).unwrap());
assert_eq!(
MemoryDealer::over(heap).err(),
Some(StatusCode::InvalidOperation)
);
}
}