use core::mem::size_of;
use crate::sync::{AtomicU32, AtomicU64, Ordering};
use crate::{Region, SlotState, VarSlotMeta};
const fn align_up(n: usize, align: usize) -> usize {
(n + align - 1) & !(align - 1)
}
const EMPTY: u32 = u32::MAX;
#[inline]
fn pack(slot_idx: u32, aba_gen: u32) -> u64 {
((aba_gen as u64) << 32) | (slot_idx as u64)
}
#[inline]
fn unpack(v: u64) -> (u32, u32) {
(v as u32, (v >> 32) as u32)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizeClassConfig {
pub slot_size: u32,
pub slot_count: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SlotRef {
pub class_idx: u8,
pub extent_idx: u8,
pub slot_idx: u32,
pub generation: u32,
}
#[derive(Debug)]
pub struct DoubleFreeError {
pub slot: SlotRef,
}
#[repr(C, align(64))]
pub struct SizeClassHeader {
pub slot_size: u32,
pub slot_count: u32,
pub free_head: AtomicU64,
_pad: [u8; 48],
}
#[cfg(not(loom))]
const _: () = assert!(size_of::<SizeClassHeader>() == 64);
struct ClassView {
header: *mut SizeClassHeader,
meta: *mut VarSlotMeta,
data: *mut u8,
slot_count: u32,
slot_size: u32,
}
unsafe impl Send for ClassView {}
unsafe impl Sync for ClassView {}
pub struct VarSlotPool {
classes: Vec<ClassView>,
}
unsafe impl Send for VarSlotPool {}
unsafe impl Sync for VarSlotPool {}
impl VarSlotPool {
pub fn layout(configs: &[SizeClassConfig]) -> PoolLayout {
let headers_size = align_up(configs.len() * size_of::<SizeClassHeader>(), 64);
let mut offset = headers_size;
let mut class_offsets = Vec::with_capacity(configs.len());
for cfg in configs {
let meta_offset = offset;
offset += align_up(size_of::<VarSlotMeta>() * cfg.slot_count as usize, 64);
let data_offset = offset;
offset += align_up(cfg.slot_size as usize * cfg.slot_count as usize, 64);
class_offsets.push(ClassOffsets {
meta_offset,
data_offset,
});
}
PoolLayout {
total_size: offset,
class_offsets,
}
}
pub fn required_size(configs: &[SizeClassConfig]) -> usize {
Self::layout(configs).total_size
}
pub unsafe fn discover_configs(
region: Region,
base_offset: usize,
num_classes: u32,
) -> Result<Vec<SizeClassConfig>, &'static str> {
if num_classes == 0 {
return Err("segment missing var-slot classes");
}
let headers_size = num_classes as usize * size_of::<SizeClassHeader>();
if base_offset
.checked_add(headers_size)
.is_none_or(|end| end > region.len())
{
return Err("var-slot class header table out of bounds");
}
let mut configs = Vec::with_capacity(num_classes as usize);
for class_idx in 0..num_classes as usize {
let header_off = base_offset + class_idx * size_of::<SizeClassHeader>();
let header = unsafe { region.get::<SizeClassHeader>(header_off) };
if header.slot_size == 0 || header.slot_count == 0 {
return Err("invalid var-slot class config in segment");
}
configs.push(SizeClassConfig {
slot_size: header.slot_size,
slot_count: header.slot_count,
});
}
Ok(configs)
}
pub unsafe fn init(region: Region, base_offset: usize, configs: &[SizeClassConfig]) -> Self {
assert!(
base_offset.is_multiple_of(64),
"base_offset must be 64-byte aligned"
);
let layout = Self::layout(configs);
assert!(
base_offset + layout.total_size <= region.len(),
"region too small for VarSlotPool"
);
let mut classes = Vec::with_capacity(configs.len());
for (i, (cfg, offsets)) in configs.iter().zip(layout.class_offsets.iter()).enumerate() {
let hdr_off = base_offset + i * size_of::<SizeClassHeader>();
let header: *mut SizeClassHeader =
unsafe { region.get_mut::<SizeClassHeader>(hdr_off) };
unsafe {
(*header).slot_size = cfg.slot_size;
(*header).slot_count = cfg.slot_count;
(*header).free_head = AtomicU64::new(if cfg.slot_count > 0 {
pack(0, 0)
} else {
pack(EMPTY, 0)
});
(*header)._pad = [0u8; 48];
}
let meta_ptr = region.offset(base_offset + offsets.meta_offset) as *mut VarSlotMeta;
let data_ptr = region.offset(base_offset + offsets.data_offset);
for slot in 0..cfg.slot_count {
let m = unsafe { &mut *meta_ptr.add(slot as usize) };
m.generation = AtomicU32::new(0);
m.state = AtomicU32::new(SlotState::Free as u32);
m.owner_peer = AtomicU32::new(0);
m.next_free = AtomicU32::new(if slot + 1 < cfg.slot_count {
slot + 1
} else {
EMPTY
});
}
classes.push(ClassView {
header,
meta: meta_ptr,
data: data_ptr,
slot_count: cfg.slot_count,
slot_size: cfg.slot_size,
});
}
Self { classes }
}
pub unsafe fn attach(region: Region, base_offset: usize, configs: &[SizeClassConfig]) -> Self {
assert!(
base_offset.is_multiple_of(64),
"base_offset must be 64-byte aligned"
);
let layout = Self::layout(configs);
assert!(
base_offset + layout.total_size <= region.len(),
"region too small for VarSlotPool"
);
let mut classes = Vec::with_capacity(configs.len());
for (i, (cfg, offsets)) in configs.iter().zip(layout.class_offsets.iter()).enumerate() {
let hdr_off = base_offset + i * size_of::<SizeClassHeader>();
let header: *mut SizeClassHeader =
unsafe { region.get_mut::<SizeClassHeader>(hdr_off) };
let meta_ptr = region.offset(base_offset + offsets.meta_offset) as *mut VarSlotMeta;
let data_ptr = region.offset(base_offset + offsets.data_offset);
classes.push(ClassView {
header,
meta: meta_ptr,
data: data_ptr,
slot_count: cfg.slot_count,
slot_size: cfg.slot_size,
});
}
Self { classes }
}
pub fn allocate(&self, size: u32, owner_peer: u8) -> Option<SlotRef> {
let start = self.classes.iter().position(|c| c.slot_size >= size)?;
for (class_idx, view) in self.classes[start..].iter().enumerate() {
let class_idx = (start + class_idx) as u8;
if let Some(slot_ref) = self.try_alloc_from(class_idx, view, owner_peer) {
return Some(slot_ref);
}
}
None
}
fn try_alloc_from(&self, class_idx: u8, view: &ClassView, owner_peer: u8) -> Option<SlotRef> {
let header = unsafe { &*view.header };
loop {
let head = header.free_head.load(Ordering::Acquire);
let (slot_idx, aba_gen) = unpack(head);
if slot_idx == EMPTY {
return None; }
let meta = unsafe { &*view.meta.add(slot_idx as usize) };
let next = meta.next_free.load(Ordering::Acquire);
let new_head = pack(next, aba_gen.wrapping_add(1));
if header
.free_head
.compare_exchange(head, new_head, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let new_gen = meta
.generation
.fetch_add(1, Ordering::AcqRel)
.wrapping_add(1);
meta.state
.store(SlotState::Allocated as u32, Ordering::Release);
meta.owner_peer.store(owner_peer as u32, Ordering::Release);
return Some(SlotRef {
class_idx,
extent_idx: 0,
slot_idx,
generation: new_gen,
});
}
}
}
pub fn free(&self, slot_ref: SlotRef) -> Result<(), DoubleFreeError> {
let view = &self.classes[slot_ref.class_idx as usize];
let meta = unsafe { &*view.meta.add(slot_ref.slot_idx as usize) };
if meta.state.load(Ordering::Acquire) != SlotState::Allocated as u32
|| meta.generation.load(Ordering::Acquire) != slot_ref.generation
{
return Err(DoubleFreeError { slot: slot_ref });
}
meta.state.store(SlotState::Free as u32, Ordering::Release);
meta.owner_peer.store(0, Ordering::Release);
let header = unsafe { &*view.header };
loop {
let head = header.free_head.load(Ordering::Acquire);
let (head_idx, aba_gen) = unpack(head);
meta.next_free.store(head_idx, Ordering::Release);
let new_head = pack(slot_ref.slot_idx, aba_gen.wrapping_add(1));
if header
.free_head
.compare_exchange(head, new_head, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return Ok(());
}
}
}
pub unsafe fn slot_data_mut<'a>(&self, slot_ref: &SlotRef) -> &'a mut [u8] {
let view = &self.classes[slot_ref.class_idx as usize];
let offset = slot_ref.slot_idx as usize * view.slot_size as usize;
unsafe { core::slice::from_raw_parts_mut(view.data.add(offset), view.slot_size as usize) }
}
pub unsafe fn slot_data<'a>(&self, slot_ref: &SlotRef) -> &'a [u8] {
let view = &self.classes[slot_ref.class_idx as usize];
let offset = slot_ref.slot_idx as usize * view.slot_size as usize;
unsafe { core::slice::from_raw_parts(view.data.add(offset), view.slot_size as usize) }
}
pub fn class_count(&self) -> usize {
self.classes.len()
}
pub fn slot_size(&self, class_idx: usize) -> u32 {
self.classes[class_idx].slot_size
}
pub fn reclaim_peer_slots(&self, peer_id: u8) {
for (class_idx, view) in self.classes.iter().enumerate() {
for slot_idx in 0..view.slot_count {
let meta = unsafe { &*view.meta.add(slot_idx as usize) };
let owner = meta.owner_peer.load(Ordering::Acquire);
if owner != peer_id as u32 {
continue;
}
let state = meta.state.load(Ordering::Acquire);
if state != SlotState::Allocated as u32 {
continue;
}
let slot_gen = meta.generation.load(Ordering::Acquire);
let _ = self.free(SlotRef {
class_idx: class_idx as u8,
extent_idx: 0,
slot_idx,
generation: slot_gen,
});
}
}
}
}
pub struct ClassOffsets {
pub meta_offset: usize,
pub data_offset: usize,
}
pub struct PoolLayout {
pub total_size: usize,
pub class_offsets: Vec<ClassOffsets>,
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
use crate::HeapRegion;
const CLASSES: &[SizeClassConfig] = &[
SizeClassConfig {
slot_size: 1024,
slot_count: 8,
},
SizeClassConfig {
slot_size: 16384,
slot_count: 4,
},
SizeClassConfig {
slot_size: 262144,
slot_count: 2,
},
];
fn make_pool() -> (HeapRegion, VarSlotPool) {
let size = VarSlotPool::required_size(CLASSES);
let region = HeapRegion::new_zeroed(size);
let pool = unsafe { VarSlotPool::init(region.region(), 0, CLASSES) };
(region, pool)
}
#[test]
fn alloc_and_free_basic() {
let (_region, pool) = make_pool();
let slot = pool.allocate(512, 1).expect("should allocate from class 0");
assert_eq!(slot.class_idx, 0);
assert_eq!(slot.slot_idx, 0);
assert_eq!(slot.generation, 1);
pool.free(slot).expect("free should succeed");
}
#[test]
fn alloc_fills_smallest_fitting_class() {
let (_region, pool) = make_pool();
let slot = pool
.allocate(2000, 0)
.expect("should allocate from class 1");
assert_eq!(slot.class_idx, 1);
}
#[test]
fn alloc_exhausts_class_falls_through() {
let (_region, pool) = make_pool();
let mut slots = Vec::new();
for _ in 0..8 {
slots.push(pool.allocate(1, 0).expect("should allocate"));
}
assert!(slots.iter().all(|s| s.class_idx == 0));
let overflow = pool.allocate(1, 0).expect("should fall through to class 1");
assert_eq!(overflow.class_idx, 1);
for s in slots {
pool.free(s).unwrap();
}
pool.free(overflow).unwrap();
}
#[test]
fn all_classes_exhausted_returns_none() {
let (_region, pool) = make_pool();
let mut slots = Vec::new();
while let Some(s) = pool.allocate(1, 0) {
slots.push(s);
}
assert_eq!(slots.len(), 14);
assert!(pool.allocate(1, 0).is_none());
for s in slots {
pool.free(s).unwrap();
}
}
#[test]
fn free_recycles_slot() {
let (_region, pool) = make_pool();
let s1 = pool.allocate(1, 0).unwrap();
pool.free(s1).unwrap();
let s2 = pool.allocate(1, 0).unwrap();
assert_eq!(s2.slot_idx, s1.slot_idx);
assert_eq!(s2.generation, s1.generation + 1);
pool.free(s2).unwrap();
}
#[test]
fn double_free_detected() {
let (_region, pool) = make_pool();
let s = pool.allocate(1, 0).unwrap();
pool.free(s).unwrap();
assert!(pool.free(s).is_err());
}
#[test]
fn slot_data_write_read() {
let (_region, pool) = make_pool();
let s = pool.allocate(100, 0).unwrap();
unsafe {
let data = pool.slot_data_mut(&s);
data[..5].copy_from_slice(b"hello");
}
unsafe {
let data = pool.slot_data_mut(&s);
assert_eq!(&data[..5], b"hello");
}
pool.free(s).unwrap();
}
#[test]
fn size_too_large_returns_none() {
let (_region, pool) = make_pool();
assert!(pool.allocate(300_000, 0).is_none());
}
#[test]
fn reclaim_peer_slots() {
let (_region, pool) = make_pool();
let _s1 = pool.allocate(1, 7).unwrap();
let _s2 = pool.allocate(1, 7).unwrap();
let s3 = pool.allocate(1, 2).unwrap();
pool.reclaim_peer_slots(7);
let mut freed = Vec::new();
while let Some(s) = pool.allocate(1, 0) {
freed.push(s);
}
assert_eq!(freed.len(), 13);
for s in freed {
pool.free(s).unwrap();
}
pool.free(s3).unwrap();
}
#[test]
fn layout_is_deterministic() {
let l1 = VarSlotPool::layout(CLASSES);
let l2 = VarSlotPool::layout(CLASSES);
assert_eq!(l1.total_size, l2.total_size);
for (a, b) in l1.class_offsets.iter().zip(l2.class_offsets.iter()) {
assert_eq!(a.meta_offset, b.meta_offset);
assert_eq!(a.data_offset, b.data_offset);
}
}
#[test]
fn owner_peer_tracked() {
let (_region, pool) = make_pool();
let s = pool.allocate(1, 42).unwrap();
let view = &pool.classes[s.class_idx as usize];
let meta = unsafe { &*view.meta.add(s.slot_idx as usize) };
assert_eq!(meta.owner_peer.load(Ordering::Acquire), 42);
pool.free(s).unwrap();
assert_eq!(meta.owner_peer.load(Ordering::Acquire), 0);
}
}
#[cfg(loom)]
#[allow(dead_code)]
mod loom_tests {
use super::*;
use crate::HeapRegion;
use loom::sync::Arc;
const LOOM_CLASSES: &[SizeClassConfig] = &[SizeClassConfig {
slot_size: 64,
slot_count: 2,
}];
fn loom_pool() -> (HeapRegion, Arc<VarSlotPool>) {
let size = VarSlotPool::required_size(LOOM_CLASSES);
let region = HeapRegion::new_zeroed(size);
let pool = unsafe { VarSlotPool::init(region.region(), 0, LOOM_CLASSES) };
(region, Arc::new(pool))
}
#[test]
fn concurrent_alloc_no_aliasing() {
loom::model(|| {
let (_region, pool) = loom_pool();
let pool1 = pool.clone();
let pool2 = pool.clone();
let t1 = loom::thread::spawn(move || pool1.allocate(1, 1));
let t2 = loom::thread::spawn(move || pool2.allocate(1, 2));
let s1 = t1.join().unwrap().expect("thread 1 must get a slot");
let s2 = t2.join().unwrap().expect("thread 2 must get a slot");
assert_ne!(s1.slot_idx, s2.slot_idx, "threads must not alias slots");
pool.free(s1).unwrap();
pool.free(s2).unwrap();
});
}
#[test]
fn alloc_then_free_cross_thread() {
loom::model(|| {
let (_region, pool) = loom_pool();
let slot = pool.allocate(1, 0).expect("must allocate");
let pool2 = pool.clone();
let t = loom::thread::spawn(move || pool2.free(slot));
t.join().unwrap().expect("cross-thread free must succeed");
});
}
#[test]
fn concurrent_alloc_and_free() {
loom::model(|| {
let (_region, pool) = loom_pool();
let pool_alloc = pool.clone();
let pool_free = pool.clone();
let initial = pool.allocate(1, 0).expect("initial alloc");
let t_free = loom::thread::spawn(move || {
pool_free.free(initial).expect("free must succeed");
});
let t_alloc = loom::thread::spawn(move || pool_alloc.allocate(1, 0));
t_free.join().unwrap();
let maybe_slot = t_alloc.join().unwrap();
if let Some(s) = maybe_slot {
pool.free(s).unwrap();
}
});
}
}