From 6997a2978c5cd29e3b0deccf6880178666c81f8d Mon Sep 17 00:00:00 2001 From: shruti2522 Date: Wed, 17 Jun 2026 23:43:27 +0000 Subject: [PATCH 1/3] research custom pointer implementation for boa --- notes/custom_pointer.md | 125 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 notes/custom_pointer.md diff --git a/notes/custom_pointer.md b/notes/custom_pointer.md new file mode 100644 index 0000000..a920623 --- /dev/null +++ b/notes/custom_pointer.md @@ -0,0 +1,125 @@ +# Research Notes: Custom Pointers for Oscars + +This document answers the questions from issue #86 about adding custom pointers to oscars + +Our main goal is to find a reliable way to point to memory on the heap. Unlike normal pointers, we want these pointers to work even if the operating system loads the program at a different memory address next time. This makes it possible to "pin" specific objects and easily serialize/deserialize the heap. + +We can build a custom pointer by combining our `MempoolAllocator` design with ideas from modern GC research. + +Reference: https://kyju.org/blog/tokioconf-2026/#a-sketch-of-a-real-raw-pointer-based-gc + +## 1. What is the most optimal representation for that pointer? + +To be able to serialize and deserialize the heap, we cannot use regular memory addresses (`*mut T` or `NonNull`). Regular addresses change every time we run the program. Instead, we need a stable ID. + +Since we use a `MempoolAllocator`, which organizes memory into blocks called pools, the best choice is a **Segmented ID** + +### Segmented ID Representation +A custom pointer should just be a 32 bit number, `u32` or `NonZeroU32` so `Option` stays small + +This 32 bit number is split into two parts: +- **`pool_id`:** Tells us which pool the object is in. +- **`slot_idx`:** Tells us the exact slot within that pool. + +**Why this is the best choice:** +1. **Fits Mempool Perfectly:** `MempoolAllocator` already organizes memory into Pools and Slots, this ID directly matches that setup. +2. **Saves Memory:** Using a 32 bit number instead of a 64 bit pointer cuts the size of all GC references in half, making the program faster because more data fits in the CPU cache. +3. **Easy to Serialize/Deserialize:** The ID is just a logical coordinate (`pool_id`, `slot_idx`), not a physical memory address. When we deserialize a serialized heap, these coordinates still point to the correct objects, even if the OS puts the pools in a different physical location. + +## 2. What is the API for a custom pointer? + +Because a custom pointer is just an index and not a real pointer, so it cannot safely use the `core::ops::Deref` trait. we can't turn a number into a reference without knowing where the memory is actually stored. + +Instead, we use branding, i.e. we wrap the 32 bit number in type `Gc<'gc, T>` + +### The `Gc` Wrapper +```rust +use core::num::NonZeroU32; +use core::marker::PhantomData; + +/// 32 bit number +#[derive(Copy, Clone, PartialEq, Eq)] +#[repr(transparent)] +pub struct CustomPtr(NonZeroU32); + +/// GC pointer +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct Gc<'gc, T: ?Sized> { + ptr: CustomPtr, + _marker: PhantomData<(&'gc (), *const T)>, +} +``` + +### The `Deref` Problem +Right now, the new `mark_sweep_branded` and `null_collector_branded` APIs wrap real physical pointers (`NonNull`) and implement `Deref`. This makes them easy to use. + +If we change our pointers to be 32 bit Segmented IDs, **we will lose the ability to use `Deref`**. + +Instead, developers will have to pass the pointer back to the GC context (`MutationContext` or `ArenaCtx`) to read the data: + +```rust +impl<'gc> MutationContext<'gc> { + /// Turns the custom pointer into a real Rust ref + pub fn get(&self, gc: Gc<'gc, T>) -> &T { + // Looks up the memory using the pool_id and slot_idx + } +} +``` + +If we really want to keep `Deref` for ease of use, there are two workarounds: +1. **Thread-Local Storage (TLS):** Put the `MempoolAllocator` in a `thread_local!` variable. This lets `Deref` look up the memory secretly behind the scenes. This is easy to use but makes it harder to move the heap between threads. +2. **Hybrid Approach:** Keep using real pointers (`NonNull`) for `Gc` during normal code execution so `Deref` works. But, create a new `HeapPtr` type that uses the Segmented ID only when we need to pin or serialize the object to disk. + +### Benefits of Thread safety +A huge benefit of using a 32 bit index is that it is totally harmless on its own. we can't read the memory without the `MutationContext`. + +Because of this, `Gc<'gc, T>` can safely implement `Send` and `Sync`. We can safely pass these pointers between different threads, even if the data they point to can be mutated, like `Cell` + +```rust +// Safe because Gc is just 32 bit number +unsafe impl<'gc, T> Send for Gc<'gc, T> {} +unsafe impl<'gc, T> Sync for Gc<'gc, T> {} +``` + +## 3. How should memory stores and loads work? + +To read or write memory, we must always use the context that owns the `MempoolAllocator`. + +### Looking up the Memory +When we call `ctx.get(gc)` or `ctx.get_mut(gc)`, the context breaks the 32 bit number into its two parts and finds the memory: + +```rust +impl CustomPtr { + #[inline(always)] + pub fn pool_id(&self) -> usize { + (self.0.get() >> 20) as usize // top 12 bits + } + + #[inline(always)] + pub fn slot_idx(&self) -> usize { + (self.0.get() & 0x000F_FFFF) as usize // bottom 20 bits + } +} + +// Inside MutationContext::get: +let pool_id = gc.ptr.pool_id(); +let slot_idx = gc.ptr.slot_idx(); + +// find the pool +let pool = &self.heap.pools[pool_id]; + +// find the exact slot +let value_ref = pool.get_slot(slot_idx); +``` + +### Performance +Even though this requires two lookups, finding the pool and then finding the slot, it is extremely fast. The table of pools is small and stays in the CPU cache. The speed gained from using 32 bit pointers more than makes up for this tiny delay. + +### Serializing and Deserializing the Heap +With this design, serializing the heap to disk is very easy: + +1. **Pause Changes:** Make sure no code is currently modifying the heap. +2. **Serialize Pools:** Loop through all the pools in the `MempoolAllocator`. Serialize their metadata (like the ID) and write the raw bytes of all used slots to disk. +3. **Serialize Roots:** Serialize the 32 bit IDs of any root objects. +4. **Deserialize the Heap:** Recreate the `MempoolAllocator` with the exact same pool IDs and deserialize the raw bytes back in. Because all Gc pointers are just `(pool_id, slot_idx)` numbers, they will automatically point to the right places. We do not need to rewrite or fix any pointers From e61b6ba37add8a120fab0312e2c3a308138ff7f2 Mon Sep 17 00:00:00 2001 From: shruti2522 Date: Sat, 27 Jun 2026 01:49:34 +0000 Subject: [PATCH 2/3] implement mempool4, explore integration blockers for custom pointers --- notes/custom_ptr_integration_blockers.md | 46 +++ oscars/examples/mempool4_demo.rs | 109 ++++++ oscars/src/alloc/mempool4/mod.rs | 438 +++++++++++++++++++++++ oscars/src/alloc/mempool4/ptr.rs | 172 +++++++++ oscars/src/alloc/mempool4/serialize.rs | 158 ++++++++ oscars/src/alloc/mempool4/tests.rs | 213 +++++++++++ oscars/src/alloc/mod.rs | 1 + 7 files changed, 1137 insertions(+) create mode 100644 notes/custom_ptr_integration_blockers.md create mode 100644 oscars/examples/mempool4_demo.rs create mode 100644 oscars/src/alloc/mempool4/mod.rs create mode 100644 oscars/src/alloc/mempool4/ptr.rs create mode 100644 oscars/src/alloc/mempool4/serialize.rs create mode 100644 oscars/src/alloc/mempool4/tests.rs diff --git a/notes/custom_ptr_integration_blockers.md b/notes/custom_ptr_integration_blockers.md new file mode 100644 index 0000000..97bb73b --- /dev/null +++ b/notes/custom_ptr_integration_blockers.md @@ -0,0 +1,46 @@ +# Custom Pointer Integration Blockers + + +This is a follow-up to our initial research on adding custom pointers to `oscars`. We've built the `mempool4` prototype to test out the `(pool_id, slot_idx)` custom pointer idea, so let's get into what we found. + +The primary goal of this exercise was to see if a 32-bit stable coordinate could actually work for allocations, resolutions, and heap serialization. + +## General notes + +The implementation itself does appear to function correctly. We can allocate, we can safely resolve using a `'gc` branded context, and the serialization story is incredibly clean since the coordinates don't need any fixup passes after restarting. + +However, there are a few caveats. If we want to make this custom pointer approach work with the existing `mark_sweep_branded` API, we run into some serious integration blockers. + +### Major blocker: Loss of `Deref` + +Right now, the existing `Gc<'gc, T>` uses raw physical pointers under the hood and implements the `Deref` trait. This makes it really nice to use: `obj.properties()` just works. + +Because our custom pointer is just a 32-bit number, it can't safely implement `Deref`. The compiler has no idea where the memory actually is without asking the allocator. So every read has to become `cx.resolve(obj).properties()`. + +Why is this a major blocker? We have hundreds of call sites across `builtins/`, `object/`, `vm/`, and `environments/` that rely on `Deref`. Migrating all of those introduces a lot of API friction. + +There are a few ways around this: +1. We just bite the bullet and migrate all the code. +2. The Hybrid Approach: we keep using real pointers for `Gc` at runtime (so `Deref` still works), and we only convert them into Custom Pointers when we need to serialize or pin something. +3. We put the allocator in Thread Local Storage (TLS) so `Deref` can look it up behind the scenes. + +### Major blocker: The `Trace` trait + +Currently, the `Trace` trait passes a real memory address to the `Tracer`. + +With `CustomPtr`, it's just a `NonZeroU32`. The tracer sees it and does nothing. It can't follow the coordinate because it doesn't have access to the `PoolAllocator4`. If it can't follow it, it thinks the object is dead and frees it, causing UAF. + +We'd have to either pass the allocator into the tracer (an additive change) or change the signature of `Trace` entirely (a massive breaking change). Note that the Hybrid Approach mentioned above also neatly sidesteps this issue, since the tracer would only ever see real pointers. + +### Room for improvement + +There are a couple other open questions around the integration: + +1. **Write Barriers:** When we assign a new GC pointer (like `node.next = other_gc`), the GC needs to know. With `Deref`, we could intercept this. With a raw `u32`, we can't. We'd need an explicit write API on the context. +2. **Pinning:** We built custom pointers to make pinning easy, but we haven't actually specced out what a "pinned object" looks like in the allocator. + +## Conclusion + +The core custom pointer concept may very well be a valid path forward, but it will be dependent on how we want to handle the loss of `Deref`. + +If we choose the Hybrid Approach, we solve both the `Deref` ergonomics issue and the `Trace` issue, though we pay a small cost in runtime conversions. Otherwise, we have to commit to a massive API migration. We need to make this decision before moving ahead. diff --git a/oscars/examples/mempool4_demo.rs b/oscars/examples/mempool4_demo.rs new file mode 100644 index 0000000..4324c1d --- /dev/null +++ b/oscars/examples/mempool4_demo.rs @@ -0,0 +1,109 @@ +//! mempool4 demo: allocate via CustomPtr, serialize, deserialize, verify +//! +//! what this demo proves: +//! 1. CustomPtr coordinates (pool_id, slot_idx) survive serialization and deserialization +//! without requiring any pointer fixup passes. A linked list serialized to bytes +//! can be traversed using the exact same head coordinate after being restored. +//! 2. The allocator's internal state (pool IDs, bump pointers) is correctly restored, +//! allowing safe incremental allocations after deserialization without colliding +//! with existing data. +//! +//! Run: `cargo run --example mempool4_demo --features std` + +use oscars::alloc::mempool4::{AllocCtx, CustomPtr, Gc, PoolAllocator4, deserialize, serialize}; + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Entry { + key: u32, + value: i64, + /// Raw CustomPtr of the next entry or 0 for end of list + next_raw: u32, +} + +fn push_front(cx: &AllocCtx<'_>, head_raw: u32, key: u32, value: i64) -> u32 { + cx.try_alloc(Entry { + key, + value, + next_raw: head_raw, + }) + .expect("allocation failed") + .as_custom_ptr() + .to_raw() +} + +fn print_list(cx: &AllocCtx<'_>, head_raw: u32) { + let mut raw = head_raw; + while let Some(ptr) = CustomPtr::from_raw(raw) { + // SAFETY: ptr came from a live allocation or a valid deserialized snapshot. + let e: &Entry = cx.resolve(unsafe { Gc::from_custom_ptr(ptr) }); + println!(" -> key={} value={}", e.key, e.value); + raw = e.next_raw; + } +} + +fn collect_list(cx: &AllocCtx<'_>, head_raw: u32) -> Vec<(u32, i64)> { + let mut out = Vec::new(); + let mut raw = head_raw; + while let Some(ptr) = CustomPtr::from_raw(raw) { + let e: &Entry = cx.resolve(unsafe { Gc::from_custom_ptr(ptr) }); + out.push((e.key, e.value)); + raw = e.next_raw; + } + out +} + +fn main() { + println!("Phase 1: allocating entries"); + let mut alloc = PoolAllocator4::new().with_page_size(4096); + + let head_raw = alloc.mutate(|cx: AllocCtx<'_>| { + let mut head = 0u32; + head = push_front(&cx, head, 30, 3000); + head = push_front(&cx, head, 20, 2000); + head = push_front(&cx, head, 10, 1000); + println!("before serialization:"); + print_list(&cx, head); + println!( + "live slots: {} pool count: {}", + cx.live_slot_count(), + cx.pool_count() + ); + head + }); + + println!("\nPhase 2: Serializing"); + let snapshot = serialize(&alloc); + println!("snapshot: {} bytes", snapshot.len()); + + println!("\nPhase 3: deserializing"); + let mut alloc2 = deserialize(&snapshot).expect("deserialization failed"); + let entries_after = alloc2.mutate(|cx: AllocCtx<'_>| { + println!("after deserialization:"); + print_list(&cx, head_raw); + collect_list(&cx, head_raw) + }); + + println!("\nPhase 4: verifying"); + let entries_before = alloc.mutate(|cx: AllocCtx<'_>| collect_list(&cx, head_raw)); + assert_eq!(entries_before, entries_after); + println!("{} entries match after round trip", entries_before.len()); + + println!("\nPhase 5: mutating and re-serializing"); + let new_head = alloc2.mutate(|cx: AllocCtx<'_>| { + let h = push_front(&cx, head_raw, 5, 500); + println!("after mutation:"); + print_list(&cx, h); + h + }); + + let snapshot2 = serialize(&alloc2); + println!("snapshot 2: {} bytes", snapshot2.len()); + + let mut alloc3 = deserialize(&snapshot2).unwrap(); + alloc3.mutate(|cx: AllocCtx<'_>| { + println!("round trip 2:"); + print_list(&cx, new_head); + }); + + println!("\ndone."); +} diff --git a/oscars/src/alloc/mempool4/mod.rs b/oscars/src/alloc/mempool4/mod.rs new file mode 100644 index 0000000..6b8ddd9 --- /dev/null +++ b/oscars/src/alloc/mempool4/mod.rs @@ -0,0 +1,438 @@ +//! Allocations return a [`Gc<'_, T>`] wrapping a [`CustomPtr`] `(pool_id, slot_idx)` +//! Values are read back through [`PoolAllocator4::mutate`] -> [`AllocCtx::resolve`] +//! The heap can be saved and restored with [`serialize`] / [`deserialize`] + +use core::{cell::Cell, marker::PhantomData, ptr::NonNull}; +use rust_alloc::alloc::{Layout, alloc, dealloc, handle_alloc_error}; +use rust_alloc::vec::Vec; + +mod ptr; +mod serialize; + +#[cfg(test)] +mod tests; + +pub use ptr::{CustomPtr, Gc, MAX_POOL_ID, MAX_SLOT_IDX}; +pub use serialize::{DeserializeError, deserialize, serialize}; + +// errors + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PoolAllocError4 { + OutOfMemory, + LayoutError, + PoolIdExhausted, + /// `(pool_id, slot_idx)` can't fit in 32 bits + PointerOverflow, +} + +const SIZE_CLASSES: &[usize] = &[16, 24, 32, 48, 64, 96, 128, 192, 256, 512, 1024, 2048]; + +#[inline(always)] +fn size_class_for(size: usize) -> usize { + SIZE_CLASSES + .partition_point(|&sc| sc < size) + .min(SIZE_CLASSES.len() - 1) +} + +const DEFAULT_PAGE_BYTES: usize = 65_536; + + +#[repr(C)] +struct FreeSlot { + next: *mut FreeSlot, +} + +/// fixed size slot pool. buffer layout: `[ bitmap ][ slot_0 | slot_1 | ... ]` +pub struct Pool4 { + pub(crate) pool_id: u32, + pub(crate) slot_size: usize, + pub(crate) slot_count: usize, + pub(crate) layout: Layout, + buffer: NonNull, + bitmap_bytes: usize, + bump: Cell, + free_list: Cell>>, + live: Cell, +} + +impl core::fmt::Debug for Pool4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Pool4") + .field("pool_id", &self.pool_id) + .field("slot_size", &self.slot_size) + .field("slot_count", &self.slot_count) + .field("live", &self.live.get()) + .finish() + } +} + +impl Pool4 { + /// Creates a new pool, `pool_id` must be unique within the allocator. + pub fn try_init( + pool_id: u32, + slot_size: usize, + capacity: usize, + ) -> Result { + assert!( + slot_size >= core::mem::size_of::(), + "slot_size must fit a FreeSlot" + ); + + let estimated_slot_count = capacity / slot_size; + let bitmap_bytes = estimated_slot_count.div_ceil(64) * 8; + let slot_area = capacity.saturating_sub(bitmap_bytes); + let slot_count = slot_area / slot_size; + + let layout = + Layout::from_size_align(capacity, 16).map_err(|_| PoolAllocError4::LayoutError)?; + + let buffer = unsafe { + let ptr = alloc(layout); + match NonNull::new(ptr) { + Some(nn) => nn, + None => handle_alloc_error(layout), + } + }; + + unsafe { core::ptr::write_bytes(buffer.as_ptr(), 0, bitmap_bytes) }; + + Ok(Self { + pool_id, + slot_size, + slot_count, + layout, + buffer, + bitmap_bytes, + bump: Cell::new(0), + free_list: Cell::new(None), + live: Cell::new(0), + }) + } + + #[inline] + fn slot_base(&self) -> *mut u8 { + unsafe { self.buffer.as_ptr().add(self.bitmap_bytes) } + } + + #[inline] + pub(crate) fn slot_ptr(&self, i: usize) -> NonNull { + debug_assert!(i < self.slot_count); + unsafe { NonNull::new_unchecked(self.slot_base().add(i * self.slot_size)) } + } + + #[inline] + fn slot_index(&self, ptr: NonNull) -> usize { + (ptr.as_ptr() as usize - self.slot_base() as usize) / self.slot_size + } + + #[inline] + fn bitmap_chunk(&self, i: usize) -> &Cell { + unsafe { &*(self.buffer.as_ptr().add((i / 64) * 8) as *const Cell) } + } + + #[inline] + fn bitmap_set(&self, i: usize) { + let c = self.bitmap_chunk(i); + c.set(c.get() | (1u64 << (i % 64))); + } + + #[inline] + fn bitmap_clear(&self, i: usize) { + let c = self.bitmap_chunk(i); + c.set(c.get() & !(1u64 << (i % 64))); + } + + /// Returns a free slot index or `None` if full. + pub fn alloc_slot(&self) -> Option { + if let Some(head) = self.free_list.get() { + let next = unsafe { (*head.as_ptr()).next }; + self.free_list.set(NonNull::new(next)); + let idx = self.slot_index(head.cast::()); + self.bitmap_set(idx); + self.live.set(self.live.get() + 1); + return Some(idx); + } + let idx = self.bump.get(); + if idx >= self.slot_count { + return None; + } + self.bump.set(idx + 1); + self.bitmap_set(idx); + self.live.set(self.live.get() + 1); + Some(idx) + } + + /// Returns a slot to the free list + /// + /// # Safety + /// `slot_idx` must be a live slot from this pool. + pub unsafe fn free_slot(&self, slot_idx: usize) { + debug_assert!(slot_idx < self.slot_count); + debug_assert!(self.live.get() > 0, "free_slot on empty pool"); + self.bitmap_clear(slot_idx); + unsafe { + let node = self.slot_ptr(slot_idx).cast::(); + let next = self + .free_list + .get() + .map_or(core::ptr::null_mut(), |h| h.as_ptr()); + node.as_ptr().write(FreeSlot { next }); + self.free_list.set(Some(node)); + } + self.live.set(self.live.get() - 1); + } + + /// `true` when the pool has no live slots + #[inline] + pub fn is_empty(&self) -> bool { + self.live.get() == 0 + } + + /// Number of live slots + #[inline] + pub fn live_count(&self) -> usize { + self.live.get() + } + + /// Yields the index of every live slot + pub fn iter_live(&self) -> impl Iterator + '_ { + (0..self.slot_count).filter_map(move |i| { + if self.bitmap_chunk(i).get() & (1u64 << (i % 64)) != 0 { + Some(i as u32) + } else { + None + } + }) + } + + /// Raw bytes of slot `i` + /// + /// # Safety + /// `slot_idx` must be live. + pub(crate) unsafe fn slot_bytes(&self, slot_idx: usize) -> &[u8] { + unsafe { core::slice::from_raw_parts(self.slot_ptr(slot_idx).as_ptr(), self.slot_size) } + } +} + +impl Drop for Pool4 { + fn drop(&mut self) { + unsafe { dealloc(self.buffer.as_ptr(), self.layout) } + } +} + +/// Size-class pool allocator that returns [`Gc<'_, T>`] handles +/// +/// Use [`mutate`](Self::mutate) to open a window for allocating and resolving. +pub struct PoolAllocator4 { + pub(crate) pools: Vec, + pub(crate) next_pool_id: u32, + pub(crate) page_size: usize, +} + +impl core::fmt::Debug for PoolAllocator4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PoolAllocator4") + .field("pool_count", &self.pools.len()) + .field("next_pool_id", &self.next_pool_id) + .finish() + } +} + +impl Default for PoolAllocator4 { + fn default() -> Self { + Self::new() + } +} + +impl PoolAllocator4 { + /// Creates an empty allocator with a 64 KiB default page size + pub fn new() -> Self { + Self { + pools: Vec::new(), + next_pool_id: 0, + page_size: DEFAULT_PAGE_BYTES, + } + } + + /// Sets the page size used when creating new pools + pub fn with_page_size(mut self, page_size: usize) -> Self { + self.page_size = page_size; + self + } + + // mutation window + + /// Opens a scoped mutation window. The closure receives an [`AllocCtx<'gc>`] + /// that can hold multiple [`Gc`] handles simultaneously + pub fn mutate(&mut self, f: F) -> R + where + F: for<'gc> FnOnce(AllocCtx<'gc>) -> R, + { + // SAFETY: `self` is exclusively borrowed for the life of `f`. + // The `'gc` brand prevents `AllocCtx` from escaping the closure. + f(AllocCtx { + inner: self as *mut Self, + _marker: PhantomData, + }) + } + + // raw allocation + + /// Allocates `value` and returns a `Gc<'static, T>` + /// + /// # Safety + /// Prefer [`mutate`](Self::mutate). The returned `Gc` must not outlive this allocator. + pub unsafe fn try_alloc_raw(&mut self, value: T) -> Result, PoolAllocError4> { + let slot_size = core::mem::size_of::().max(core::mem::size_of::()); + let actual_slot_size = SIZE_CLASSES + .get(size_class_for(slot_size)) + .copied() + .unwrap_or(slot_size); + + for pool in self.pools.iter() { + if pool.slot_size == actual_slot_size + && let Some(slot_idx) = pool.alloc_slot() + { + let ptr = CustomPtr::new(pool.pool_id, slot_idx as u32) + .ok_or(PoolAllocError4::PointerOverflow)?; + unsafe { (pool.slot_ptr(slot_idx).as_ptr() as *mut T).write(value) }; + return Ok(Gc { + ptr, + _marker: PhantomData, + }); + } + } + + let pool_id = self.next_pool_id; + if pool_id > MAX_POOL_ID { + return Err(PoolAllocError4::PoolIdExhausted); + } + self.next_pool_id += 1; + + let pool = Pool4::try_init( + pool_id, + actual_slot_size, + self.page_size.max(actual_slot_size * 4), + )?; + let slot_idx = pool.alloc_slot().ok_or(PoolAllocError4::OutOfMemory)?; + let ptr = + CustomPtr::new(pool_id, slot_idx as u32).ok_or(PoolAllocError4::PointerOverflow)?; + unsafe { (pool.slot_ptr(slot_idx).as_ptr() as *mut T).write(value) }; + self.pools.push(pool); + + Ok(Gc { + ptr, + _marker: PhantomData, + }) + } + + /// Returns a shared reference to the value at `gc` + #[inline] + pub fn resolve<'gc, T>(&'gc self, gc: Gc<'gc, T>) -> &'gc T { + let pool = self + .find_pool(gc.ptr.pool_id()) + .expect("Gc pool_id not found in this allocator"); + unsafe { &*(pool.slot_ptr(gc.ptr.slot_idx()).as_ptr() as *const T) } + } + + /// Returns an exclusive reference to the value at `gc` + #[inline] + pub fn resolve_mut<'gc, T>(&'gc mut self, gc: Gc<'gc, T>) -> &'gc mut T { + let pool = self + .find_pool_mut(gc.ptr.pool_id()) + .expect("Gc pool_id not found in this allocator"); + unsafe { &mut *(pool.slot_ptr(gc.ptr.slot_idx()).as_ptr() as *mut T) } + } + + /// Drops the value at `gc` and frees the slot. + /// + /// # Safety + /// `gc` must be live. Don't use the handle after this call. + pub unsafe fn free(&mut self, gc: Gc<'_, T>) { + let pool = self + .find_pool(gc.ptr.pool_id()) + .expect("Gc pool_id not found in this allocator"); + unsafe { + core::ptr::drop_in_place(pool.slot_ptr(gc.ptr.slot_idx()).as_ptr() as *mut T); + pool.free_slot(gc.ptr.slot_idx()); + } + } + + pub fn pool_count(&self) -> usize { + self.pools.len() + } + + pub fn live_slot_count(&self) -> usize { + self.pools.iter().map(|p| p.live_count()).sum() + } + + // private + + // TODO(perf): O(n) scan; replace with a sorted index at scale. + fn find_pool(&self, pool_id: usize) -> Option<&Pool4> { + self.pools.iter().find(|p| p.pool_id as usize == pool_id) + } + + fn find_pool_mut(&mut self, pool_id: usize) -> Option<&mut Pool4> { + self.pools + .iter_mut() + .find(|p| p.pool_id as usize == pool_id) + } +} + + +/// Scoped context from [`PoolAllocator4::mutate`] +/// +/// Holds multiple [`Gc`] handles at once without borrow conflicts. +/// The `'gc` brand prevents handles from escaping the closure. +pub struct AllocCtx<'gc> { + inner: *mut PoolAllocator4, + _marker: PhantomData<*mut &'gc ()>, +} + +impl<'gc> AllocCtx<'gc> { + /// Allocates `value` and returns a `Gc<'gc, T>` + pub fn try_alloc(&self, value: T) -> Result, PoolAllocError4> { + unsafe { (*self.inner).try_alloc_raw(value) } + } + + /// Returns a shared reference to the value at `gc`. + pub fn resolve(&self, gc: Gc<'gc, T>) -> &'gc T { + let alloc = unsafe { &*self.inner }; + let pool = alloc + .find_pool(gc.ptr.pool_id()) + .expect("Gc pool_id not found in this allocator"); + unsafe { &*(pool.slot_ptr(gc.ptr.slot_idx()).as_ptr() as *const T) } + } + + /// Returns an exclusive reference to the value at `gc`. + /// + /// # Safety + /// No other reference to the same slot may exist + pub unsafe fn resolve_mut(&self, gc: Gc<'gc, T>) -> &'gc mut T { + // Borrow the allocator shared only to find the pool; the &mut T is + // into the slot buffer, disjoint from the allocator struct. + let alloc = unsafe { &*self.inner }; + let pool = alloc + .find_pool(gc.ptr.pool_id()) + .expect("Gc pool_id not found in this allocator"); + unsafe { &mut *(pool.slot_ptr(gc.ptr.slot_idx()).as_ptr() as *mut T) } + } + + /// Drops the value at gc and frees the slot + /// + /// # Safety + /// `gc` must be live, don't use the handle after this call. + pub unsafe fn free(&self, gc: Gc<'gc, T>) { + unsafe { (*self.inner).free(gc) } + } + + pub fn live_slot_count(&self) -> usize { + unsafe { (*self.inner).live_slot_count() } + } + + pub fn pool_count(&self) -> usize { + unsafe { (*self.inner).pool_count() } + } +} diff --git a/oscars/src/alloc/mempool4/ptr.rs b/oscars/src/alloc/mempool4/ptr.rs new file mode 100644 index 0000000..1a30501 --- /dev/null +++ b/oscars/src/alloc/mempool4/ptr.rs @@ -0,0 +1,172 @@ +//! Custom 32 bit pointer types +//! +//! [`CustomPtr`] stores `(pool_id, slot_idx)` instead of an address so it survives serialization. +//! [`Gc<'gc, T>`] wraps it with a lifetime brand, use `resolve` instead of `Deref` + +use core::marker::PhantomData; +use core::num::NonZeroU32; + +const SLOT_BITS: u32 = 20; +const SLOT_MASK: u32 = (1 << SLOT_BITS) - 1; +pub const MAX_POOL_ID: u32 = (1 << (32 - SLOT_BITS)) - 1; // 4095 +pub const MAX_SLOT_IDX: u32 = SLOT_MASK; + +/// A stable, address independent index into a [`PoolAllocator4`](super::PoolAllocator4) +/// +/// `Option` is the same size as `CustomPtr` +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct CustomPtr(NonZeroU32); + +impl CustomPtr { + /// Packs `pool_id` and `slot_idx` into a `CustomPtr` + #[inline] + pub fn new(pool_id: u32, slot_idx: u32) -> Option { + if pool_id > MAX_POOL_ID || slot_idx > MAX_SLOT_IDX { + return None; + } + let packed = (pool_id << SLOT_BITS) | slot_idx; + // +1 bias keeps NonZeroU32 valid; checked_add handles the overflow + packed + .checked_add(1) + .and_then(NonZeroU32::new) + .map(CustomPtr) + } + + /// Pool index (bits 31–20) + #[inline] + pub fn pool_id(self) -> usize { + ((self.0.get() - 1) >> SLOT_BITS) as usize + } + + /// Slot index (bits 19–0) + #[inline] + pub fn slot_idx(self) -> usize { + ((self.0.get() - 1) & SLOT_MASK) as usize + } + + /// Raw `u32` + #[inline] + pub fn to_raw(self) -> u32 { + self.0.get() + } + + /// Reconstruct from `to_raw` + /// + /// # Safety + /// Only pass valid raw values + #[inline] + pub fn from_raw(raw: u32) -> Option { + NonZeroU32::new(raw).map(CustomPtr) + } +} + +impl core::fmt::Debug for CustomPtr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "CustomPtr({}, {})", self.pool_id(), self.slot_idx()) + } +} + +impl core::fmt::Display for CustomPtr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "({}, {})", self.pool_id(), self.slot_idx()) + } +} + +/// Lifetime branded GC handle backed by a `CustomPtr` +#[derive(Copy, Clone, PartialEq, Eq)] +#[repr(transparent)] +pub struct Gc<'gc, T: ?Sized> { + // use from_custom_ptr + pub(super) ptr: CustomPtr, + pub(super) _marker: PhantomData<(&'gc (), *const T)>, +} + +// Gc is a plain integer +unsafe impl<'gc, T: ?Sized> Send for Gc<'gc, T> {} +unsafe impl<'gc, T: ?Sized> Sync for Gc<'gc, T> {} + +impl<'gc, T> Gc<'gc, T> { + /// Returns underlying `CustomPtr` + #[inline] + pub fn as_custom_ptr(self) -> CustomPtr { + self.ptr + } + + /// Rebuilds `Gc` from a `CustomPtr` + /// + /// # Safety + /// `ptr` must refer to a live slot of type `T` + #[inline] + pub unsafe fn from_custom_ptr(ptr: CustomPtr) -> Self { + Self { + ptr, + _marker: PhantomData, + } + } +} + +impl<'gc, T: ?Sized> core::fmt::Debug for Gc<'gc, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Gc({:?})", self.ptr) + } +} + +#[cfg(test)] +mod ptr_unit_tests { + use super::*; + + #[test] + fn roundtrip_pool_and_slot() { + let cases: &[(u32, u32)] = &[ + (0, 0), + (0, MAX_SLOT_IDX), + (MAX_POOL_ID, 0), + (1, 100), + (255, 1_000), + ]; + for &(pool_id, slot_idx) in cases { + let ptr = CustomPtr::new(pool_id, slot_idx) + .unwrap_or_else(|| panic!("failed for ({pool_id}, {slot_idx})")); + assert_eq!(ptr.pool_id() as u32, pool_id); + assert_eq!(ptr.slot_idx() as u32, slot_idx); + } + // +1 overflows to None + assert!(CustomPtr::new(MAX_POOL_ID, MAX_SLOT_IDX).is_none()); + } + + #[test] + fn out_of_range_pool_returns_none() { + assert!(CustomPtr::new(MAX_POOL_ID + 1, 0).is_none()); + } + + #[test] + fn out_of_range_slot_returns_none() { + assert!(CustomPtr::new(0, MAX_SLOT_IDX + 1).is_none()); + } + + #[test] + fn option_size_is_same_as_custom_ptr() { + assert_eq!( + core::mem::size_of::>(), + core::mem::size_of::(), + ); + } + + #[test] + fn raw_roundtrip() { + let ptr = CustomPtr::new(42, 7).unwrap(); + assert_eq!(ptr, CustomPtr::from_raw(ptr.to_raw()).unwrap()); + } + + #[test] + fn from_raw_zero_returns_none() { + assert!(CustomPtr::from_raw(0).is_none()); + } + + fn _assert_send_sync() {} + fn _check() { + _assert_send_sync::>(); + } +} + diff --git a/oscars/src/alloc/mempool4/serialize.rs b/oscars/src/alloc/mempool4/serialize.rs new file mode 100644 index 0000000..6b27d66 --- /dev/null +++ b/oscars/src/alloc/mempool4/serialize.rs @@ -0,0 +1,158 @@ +//! Heap serialization for `PoolAllocator4` +//! +//! Format: little-endian integers +//! [pool_count] -> per pool: [id, size, count, live_count] -> per slot: [idx, data] +//! Slot data must not contain raw pointers + +use super::{Pool4, PoolAllocError4, PoolAllocator4}; +use rust_alloc::vec::Vec; + +// errors + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeserializeError { + UnexpectedEof, + /// Index out of range + InvalidIndex, + InvalidSlotSize, + AllocError(PoolAllocError4), +} + +impl From for DeserializeError { + fn from(e: PoolAllocError4) -> Self { + Self::AllocError(e) + } +} + +// helpers + +struct Reader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Reader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn read_u32(&mut self) -> Result { + let end = self.pos + 4; + if end > self.data.len() { + return Err(DeserializeError::UnexpectedEof); + } + let bytes: [u8; 4] = self.data[self.pos..end].try_into().unwrap(); + self.pos = end; + Ok(u32::from_le_bytes(bytes)) + } + + fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], DeserializeError> { + let end = self.pos + n; + if end > self.data.len() { + return Err(DeserializeError::UnexpectedEof); + } + let slice = &self.data[self.pos..end]; + self.pos = end; + Ok(slice) + } +} + +struct Writer { + buf: Vec, +} + +impl Writer { + fn new() -> Self { + Self { buf: Vec::new() } + } + + fn write_u32(&mut self, v: u32) { + self.buf.extend_from_slice(&v.to_le_bytes()); + } + + fn write_bytes(&mut self, b: &[u8]) { + self.buf.extend_from_slice(b); + } + + fn finish(self) -> Vec { + self.buf + } +} + +// public API + +/// Serializes all live slots +pub fn serialize(allocator: &PoolAllocator4) -> Vec { + let mut w = Writer::new(); + w.write_u32(allocator.pools.len() as u32); + for pool in &allocator.pools { + let live: Vec = pool.iter_live().collect(); + w.write_u32(pool.pool_id); + w.write_u32(pool.slot_size as u32); + w.write_u32(pool.slot_count as u32); + w.write_u32(live.len() as u32); + for idx in live { + w.write_u32(idx); + // idx is live + w.write_bytes(unsafe { pool.slot_bytes(idx as usize) }); + } + } + w.finish() +} + +/// Reconstructs `PoolAllocator4` from `serialize` bytes +pub fn deserialize(bytes: &[u8]) -> Result { + let mut r = Reader::new(bytes); + let pool_count = r.read_u32()? as usize; + let mut allocator = PoolAllocator4::new(); + + for _ in 0..pool_count { + let pool_id = r.read_u32()?; + let slot_size = r.read_u32()? as usize; + let slot_count = r.read_u32()? as usize; + let live_count = r.read_u32()? as usize; + + if slot_size == 0 { + return Err(DeserializeError::InvalidSlotSize); + } + + // overflow guard + if slot_count as u64 > super::MAX_SLOT_IDX as u64 { + return Err(DeserializeError::InvalidIndex); + } + + // snapshot is corrupt + if live_count > slot_count { + return Err(DeserializeError::InvalidIndex); + } + + let capacity = slot_size * slot_count + slot_count.div_ceil(64) * 8; + let pool = Pool4::try_init(pool_id, slot_size, capacity)?; + + for _ in 0..live_count { + let slot_idx = r.read_u32()? as usize; + let slot_bytes = r.read_bytes(slot_size)?; + + let allocated = pool.alloc_slot().ok_or(DeserializeError::UnexpectedEof)?; + // slots are in ascending order + if allocated != slot_idx { + return Err(DeserializeError::InvalidIndex); + } + // slot is freshly allocated + unsafe { + core::ptr::copy_nonoverlapping( + slot_bytes.as_ptr(), + pool.slot_ptr(allocated).as_ptr(), + slot_size, + ); + } + } + + if pool_id >= allocator.next_pool_id { + allocator.next_pool_id = pool_id + 1; + } + allocator.pools.push(pool); + } + + Ok(allocator) +} diff --git a/oscars/src/alloc/mempool4/tests.rs b/oscars/src/alloc/mempool4/tests.rs new file mode 100644 index 0000000..a5af0a8 --- /dev/null +++ b/oscars/src/alloc/mempool4/tests.rs @@ -0,0 +1,213 @@ +use super::{AllocCtx, CustomPtr, Gc, PoolAllocator4, deserialize, serialize}; + +#[test] +fn alloc_and_resolve() { + let mut alloc = PoolAllocator4::new(); + alloc.mutate(|cx: AllocCtx<'_>| { + let a: Gc<'_, i32> = cx.try_alloc(42_i32).unwrap(); + let b: Gc<'_, i32> = cx.try_alloc(-7_i32).unwrap(); + let c: Gc<'_, u64> = cx.try_alloc(u64::MAX).unwrap(); + assert_eq!(*cx.resolve(a), 42); + assert_eq!(*cx.resolve(b), -7); + assert_eq!(*cx.resolve(c), u64::MAX); + }); +} + +#[test] +fn custom_ptr_encoding() { + use super::ptr::{MAX_POOL_ID, MAX_SLOT_IDX}; + + let cases: &[(u32, u32)] = &[ + (0, 0), + (0, 1), + (1, 0), + (42, 1_000), + (4095, 0), + (0, 1_048_575), + ]; + for &(pid, sidx) in cases { + let ptr = CustomPtr::new(pid, sidx) + .unwrap_or_else(|| panic!("CustomPtr::new({pid}, {sidx}) returned None")); + assert_eq!(ptr.pool_id() as u32, pid); + assert_eq!(ptr.slot_idx() as u32, sidx); + } + assert!(CustomPtr::new(MAX_POOL_ID + 1, 0).is_none()); + assert!(CustomPtr::new(0, MAX_SLOT_IDX + 1).is_none()); + assert!(CustomPtr::new(MAX_POOL_ID, MAX_SLOT_IDX).is_none()); +} + +#[test] +fn free_and_reuse() { + let mut alloc = PoolAllocator4::new(); + alloc.mutate(|cx: AllocCtx<'_>| { + let gc1: Gc<'_, u64> = cx.try_alloc(111_u64).unwrap(); + let slot_before = gc1.as_custom_ptr().slot_idx(); + let pool_before = gc1.as_custom_ptr().pool_id(); + + // gc1 is not used after this call + unsafe { cx.free(gc1) }; + + let gc2: Gc<'_, u64> = cx.try_alloc(222_u64).unwrap(); + assert_eq!(gc2.as_custom_ptr().pool_id(), pool_before); + assert_eq!(gc2.as_custom_ptr().slot_idx(), slot_before); + assert_eq!(*cx.resolve(gc2), 222); + }); +} + +// Gc must be Send + Sync +fn _assert_send_sync() {} +fn _check_gc_send_sync() { + _assert_send_sync::>(); +} + +#[test] +fn serialize_roundtrip() { + let (bytes, a_ptr, b_ptr, c_ptr) = { + let mut alloc = PoolAllocator4::new(); + let ptrs = alloc.mutate(|cx: AllocCtx<'_>| { + let a = cx.try_alloc(100_u32).unwrap(); + let b = cx.try_alloc(200_u32).unwrap(); + let c = cx.try_alloc(300_u32).unwrap(); + (a.as_custom_ptr(), b.as_custom_ptr(), c.as_custom_ptr()) + }); + (serialize(&alloc), ptrs.0, ptrs.1, ptrs.2) + }; + + assert!(!bytes.is_empty()); + let mut alloc2 = deserialize(&bytes).unwrap(); + alloc2.mutate(|cx: AllocCtx<'_>| { + // coordinates came from live slots + let a2: Gc<'_, u32> = unsafe { Gc::from_custom_ptr(a_ptr) }; + let b2: Gc<'_, u32> = unsafe { Gc::from_custom_ptr(b_ptr) }; + let c2: Gc<'_, u32> = unsafe { Gc::from_custom_ptr(c_ptr) }; + assert_eq!(*cx.resolve(a2), 100); + assert_eq!(*cx.resolve(b2), 200); + assert_eq!(*cx.resolve(c2), 300); + }); +} + +#[test] +fn serialize_roots() { + /// Node storing value and next CustomPtr + #[derive(Copy, Clone)] + struct Node { + value: u32, + next_raw: u32, + } + + let (bytes, head_raw) = { + let mut alloc = PoolAllocator4::new(); + let head_raw = alloc.mutate(|cx: AllocCtx<'_>| { + let tail = cx + .try_alloc(Node { + value: 99, + next_raw: 0, + }) + .unwrap(); + let head = cx + .try_alloc(Node { + value: 1, + next_raw: tail.as_custom_ptr().to_raw(), + }) + .unwrap(); + head.as_custom_ptr().to_raw() + }); + (serialize(&alloc), head_raw) + }; + + let mut alloc2 = deserialize(&bytes).unwrap(); + alloc2.mutate(|cx: AllocCtx<'_>| { + // head_raw was serialized from a live allocation + let root: Gc<'_, Node> = + unsafe { Gc::from_custom_ptr(CustomPtr::from_raw(head_raw).unwrap()) }; + let head = *cx.resolve(root); + assert_eq!(head.value, 1); + + // same as above + let tail: Gc<'_, Node> = + unsafe { Gc::from_custom_ptr(CustomPtr::from_raw(head.next_raw).unwrap()) }; + let tail_node = *cx.resolve(tail); + assert_eq!(tail_node.value, 99); + assert_eq!(tail_node.next_raw, 0); + }); +} + +#[test] +fn drop_empty_pools() { + let mut alloc = PoolAllocator4::new(); + alloc.mutate(|cx: AllocCtx<'_>| { + let g1 = cx.try_alloc(1_u64).unwrap(); + let g2 = cx.try_alloc(2_u64).unwrap(); + let g3 = cx.try_alloc(3_u64).unwrap(); + assert_eq!(cx.live_slot_count(), 3); + // each handle used exactly once + unsafe { + cx.free(g1); + cx.free(g2); + cx.free(g3); + } + assert_eq!(cx.live_slot_count(), 0); + }); + for pool in &alloc.pools { + assert!(pool.is_empty(), "pool {} not empty", pool.pool_id); + } +} + +#[test] +fn resolve_mut_mutates() { + let mut alloc = PoolAllocator4::new(); + alloc.mutate(|cx: AllocCtx<'_>| { + let gc = cx.try_alloc(0_u32).unwrap(); + // no other references to this slot + unsafe { *cx.resolve_mut(gc) = 42 }; + assert_eq!(*cx.resolve(gc), 42); + }); +} + +#[test] +fn multi_type_alloc() { + let mut alloc = PoolAllocator4::new(); + alloc.mutate(|cx: AllocCtx<'_>| { + let i = cx.try_alloc(i32::MIN).unwrap(); + let f = cx.try_alloc(1.5_f64).unwrap(); + let b = cx.try_alloc(true).unwrap(); + assert_eq!(*cx.resolve(i), i32::MIN); + assert!((*cx.resolve(f) - 1.5_f64).abs() < f64::EPSILON); + assert!(*cx.resolve(b)); + }); +} + +#[test] +fn option_customptr_niche() { + assert_eq!( + core::mem::size_of::>(), + core::mem::size_of::(), + ); +} + +#[test] +fn alloc_after_deserialize() { + // verify next_pool_id is correctly restored + let (bytes, existing_ptr) = { + let mut alloc = PoolAllocator4::new(); + let ptr = alloc.mutate(|cx: AllocCtx<'_>| { + cx.try_alloc(42_u32).unwrap().as_custom_ptr() + }); + (serialize(&alloc), ptr) + }; + + let mut alloc2 = deserialize(&bytes).unwrap(); + alloc2.mutate(|cx: AllocCtx<'_>| { + // allocate a new value + let new_gc = cx.try_alloc(99_u32).unwrap(); + let new_ptr = new_gc.as_custom_ptr(); + + // pointers must not collide + assert_ne!(new_ptr, existing_ptr, "new allocation collided with restored slot"); + + // existing_ptr came from a live allocation before serialization + let old: &u32 = cx.resolve(unsafe { Gc::from_custom_ptr(existing_ptr) }); + assert_eq!(*old, 42); + assert_eq!(*cx.resolve(new_gc), 99); + }); +} diff --git a/oscars/src/alloc/mod.rs b/oscars/src/alloc/mod.rs index 1f4a7d4..5622a08 100644 --- a/oscars/src/alloc/mod.rs +++ b/oscars/src/alloc/mod.rs @@ -5,3 +5,4 @@ pub mod arena2; pub mod mempool; pub mod mempool2; pub mod mempool3; +pub mod mempool4; From 3bf10a700beae4732673862caf23c4090d94df4b Mon Sep 17 00:00:00 2001 From: shruti2522 Date: Sat, 27 Jun 2026 02:07:09 +0000 Subject: [PATCH 3/3] fix ci --- oscars/src/alloc/mempool4/mod.rs | 4 +--- oscars/src/alloc/mempool4/ptr.rs | 5 ++--- oscars/src/alloc/mempool4/serialize.rs | 2 +- oscars/src/alloc/mempool4/tests.rs | 9 +++++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/oscars/src/alloc/mempool4/mod.rs b/oscars/src/alloc/mempool4/mod.rs index 6b8ddd9..5ecb90a 100644 --- a/oscars/src/alloc/mempool4/mod.rs +++ b/oscars/src/alloc/mempool4/mod.rs @@ -1,6 +1,6 @@ //! Allocations return a [`Gc<'_, T>`] wrapping a [`CustomPtr`] `(pool_id, slot_idx)` //! Values are read back through [`PoolAllocator4::mutate`] -> [`AllocCtx::resolve`] -//! The heap can be saved and restored with [`serialize`] / [`deserialize`] +//! The heap can be saved and restored with [`serialize()`] / [`deserialize()`] use core::{cell::Cell, marker::PhantomData, ptr::NonNull}; use rust_alloc::alloc::{Layout, alloc, dealloc, handle_alloc_error}; @@ -37,7 +37,6 @@ fn size_class_for(size: usize) -> usize { const DEFAULT_PAGE_BYTES: usize = 65_536; - #[repr(C)] struct FreeSlot { next: *mut FreeSlot, @@ -381,7 +380,6 @@ impl PoolAllocator4 { } } - /// Scoped context from [`PoolAllocator4::mutate`] /// /// Holds multiple [`Gc`] handles at once without borrow conflicts. diff --git a/oscars/src/alloc/mempool4/ptr.rs b/oscars/src/alloc/mempool4/ptr.rs index 1a30501..e73fa3b 100644 --- a/oscars/src/alloc/mempool4/ptr.rs +++ b/oscars/src/alloc/mempool4/ptr.rs @@ -1,5 +1,5 @@ //! Custom 32 bit pointer types -//! +//! //! [`CustomPtr`] stores `(pool_id, slot_idx)` instead of an address so it survives serialization. //! [`Gc<'gc, T>`] wraps it with a lifetime brand, use `resolve` instead of `Deref` @@ -13,7 +13,7 @@ pub const MAX_SLOT_IDX: u32 = SLOT_MASK; /// A stable, address independent index into a [`PoolAllocator4`](super::PoolAllocator4) /// -/// `Option` is the same size as `CustomPtr` +/// `Option` is the same size as `CustomPtr` #[derive(Copy, Clone, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct CustomPtr(NonZeroU32); @@ -169,4 +169,3 @@ mod ptr_unit_tests { _assert_send_sync::>(); } } - diff --git a/oscars/src/alloc/mempool4/serialize.rs b/oscars/src/alloc/mempool4/serialize.rs index 6b27d66..61c4041 100644 --- a/oscars/src/alloc/mempool4/serialize.rs +++ b/oscars/src/alloc/mempool4/serialize.rs @@ -1,7 +1,7 @@ //! Heap serialization for `PoolAllocator4` //! //! Format: little-endian integers -//! [pool_count] -> per pool: [id, size, count, live_count] -> per slot: [idx, data] +//! `[pool_count]` -> per pool: `[id, size, count, live_count]` -> per slot: `[idx, data]` //! Slot data must not contain raw pointers use super::{Pool4, PoolAllocError4, PoolAllocator4}; diff --git a/oscars/src/alloc/mempool4/tests.rs b/oscars/src/alloc/mempool4/tests.rs index a5af0a8..a6c97c1 100644 --- a/oscars/src/alloc/mempool4/tests.rs +++ b/oscars/src/alloc/mempool4/tests.rs @@ -190,9 +190,7 @@ fn alloc_after_deserialize() { // verify next_pool_id is correctly restored let (bytes, existing_ptr) = { let mut alloc = PoolAllocator4::new(); - let ptr = alloc.mutate(|cx: AllocCtx<'_>| { - cx.try_alloc(42_u32).unwrap().as_custom_ptr() - }); + let ptr = alloc.mutate(|cx: AllocCtx<'_>| cx.try_alloc(42_u32).unwrap().as_custom_ptr()); (serialize(&alloc), ptr) }; @@ -203,7 +201,10 @@ fn alloc_after_deserialize() { let new_ptr = new_gc.as_custom_ptr(); // pointers must not collide - assert_ne!(new_ptr, existing_ptr, "new allocation collided with restored slot"); + assert_ne!( + new_ptr, existing_ptr, + "new allocation collided with restored slot" + ); // existing_ptr came from a live allocation before serialization let old: &u32 = cx.resolve(unsafe { Gc::from_custom_ptr(existing_ptr) });