Skip to main content

nexus_slab/byte/
unbounded.rs

1//! Growable type-erased byte slab.
2
3use core::marker::PhantomData;
4use core::mem;
5
6use crate::shared::SlotCell;
7
8use super::{AlignedBytes, Slot, validate_type};
9
10/// Growable byte slab. Mirrors [`crate::unbounded::Slab`] but stores
11/// heterogeneous types in fixed-size byte slots.
12///
13/// Grows via independent chunks — no copying, no reallocation of
14/// existing slots. Pointers remain valid.
15///
16/// # Safety Contract
17///
18/// Construction is `unsafe` because it opts you into manual memory
19/// management. By creating a slab, you accept these invariants:
20///
21/// - **Free from the correct slab.** Passing a [`Slot`] to a different
22///   slab's `free()` is undefined behavior — it corrupts the freelist.
23///   In debug builds, this is caught by `debug_assert!`.
24/// - **Free everything you allocate.** Dropping the slab does NOT drop
25///   values in occupied slots. Unfreed slots leak silently.
26/// - **Single-threaded.** The slab is `!Send` and `!Sync`.
27///
28/// ## Why `free()` is safe
29///
30/// The safety contract is accepted once, at construction. After that:
31/// - [`Slot`] is move-only (no `Copy`, no `Clone`) — double-free is
32///   prevented by the type system.
33/// - `free()` consumes the `Slot` — the handle cannot be used after.
34/// - Cross-slab misuse is the only remaining hazard, and it was
35///   accepted as the caller's responsibility at construction time.
36pub struct Slab<const N: usize> {
37    inner: crate::unbounded::Slab<AlignedBytes<N>>,
38}
39
40impl<const N: usize> Slab<N> {
41    /// Creates a new unbounded byte slab with the given chunk capacity.
42    ///
43    /// # Safety
44    ///
45    /// See [struct-level safety contract](Self).
46    ///
47    /// # Panics
48    ///
49    /// Panics if `chunk_capacity` is zero.
50    #[inline]
51    pub unsafe fn with_chunk_capacity(chunk_capacity: usize) -> Self {
52        // SAFETY: caller upholds the slab contract
53        unsafe { Builder::new().chunk_capacity(chunk_capacity).build::<N>() }
54    }
55
56    /// Allocates a value. Never fails — grows if needed.
57    ///
58    /// # Panics
59    ///
60    /// - Panics if `size_of::<T>() > N`
61    /// - Panics if `align_of::<T>() > 8`
62    #[inline]
63    pub fn alloc<T>(&self, value: T) -> Slot<T> {
64        validate_type::<T, N>();
65
66        let (slot_ptr, _chunk_idx) = self.inner.claim_ptr();
67
68        // SAFETY: slot_ptr is valid and vacant. AlignedBytes<N> is
69        // repr(C, align(8)), suitable for T (asserted above).
70        unsafe {
71            let data_ptr = slot_ptr.cast::<T>();
72            core::ptr::write(data_ptr, value);
73        }
74
75        Slot {
76            ptr: slot_ptr.cast::<u8>(),
77            _marker: PhantomData,
78        }
79    }
80
81    /// Reserve a slot without writing. Always succeeds (grows if needed).
82    ///
83    /// The returned [`super::ByteClaim`] can be written to with `.write(value)`
84    /// or `.write_raw(src, size)`. If dropped without writing, the slot
85    /// is returned to the freelist.
86    #[inline]
87    pub fn claim(&self) -> super::ByteClaim<'_> {
88        let claim = self.inner.claim();
89        let (ptr, chunk_idx) = claim.into_ptr();
90        let slab_ptr = core::ptr::from_ref(&self.inner).cast::<u8>();
91        // SAFETY: ptr is a valid vacant slot. chunk_idx identifies the owning chunk.
92        unsafe {
93            super::ByteClaim::from_raw_parts(
94                ptr.cast::<u8>(),
95                slab_ptr,
96                free_raw_impl::<N>,
97                chunk_idx,
98                N,
99            )
100        }
101    }
102
103    /// Free a raw pointer without dropping content.
104    ///
105    /// # Safety
106    ///
107    /// `ptr` must point to a slot in this slab.
108    #[inline]
109    pub unsafe fn free_raw(&self, ptr: *mut u8) {
110        unsafe {
111            self.inner.free_ptr(ptr.cast());
112        }
113    }
114
115    /// Free a raw pointer with known chunk index. O(1) — no linear scan.
116    ///
117    /// # Safety
118    ///
119    /// - `ptr` must point to a slot in chunk `chunk_idx` of this slab.
120    #[inline]
121    pub unsafe fn free_raw_in_chunk(&self, ptr: *mut u8, chunk_idx: usize) {
122        unsafe {
123            self.inner.free_ptr_in_chunk(ptr.cast(), chunk_idx);
124        }
125    }
126
127    /// Claim a slot and copy raw bytes into it. Returns a raw pointer.
128    ///
129    /// # Safety
130    ///
131    /// - `src` must point to `size` valid bytes.
132    /// - `size` must be <= `N`.
133    ///
134    /// # Panics
135    ///
136    /// - Panics if `size > N`.
137    #[inline]
138    pub unsafe fn alloc_raw(&self, src: *const u8, size: usize) -> *mut u8 {
139        assert!(size <= N, "raw alloc size {size} exceeds slot size {N}");
140        let (slot_ptr, _chunk_idx) = self.inner.claim_ptr();
141        let dst = slot_ptr.cast::<u8>();
142        unsafe { core::ptr::copy_nonoverlapping(src, dst, size) };
143        dst
144    }
145
146    /// Frees a value, dropping it and returning the slot to the freelist.
147    ///
148    /// Consumes the handle — the slot cannot be used after this call.
149    #[inline]
150    pub fn free<T>(&self, ptr: Slot<T>) {
151        let data_ptr = ptr.ptr;
152        debug_assert!(
153            self.inner.contains_ptr(data_ptr as *const ()),
154            "slot was not allocated from this slab"
155        );
156        mem::forget(ptr);
157
158        unsafe {
159            core::ptr::drop_in_place(data_ptr.cast::<T>());
160            self.inner
161                .free_ptr(data_ptr.cast::<SlotCell<AlignedBytes<N>>>());
162        }
163    }
164
165    /// Takes the value out without dropping it, freeing the slot.
166    ///
167    /// Consumes the handle — the slot cannot be used after this call.
168    #[inline]
169    pub fn take<T>(&self, ptr: Slot<T>) -> T {
170        let data_ptr = ptr.ptr;
171        debug_assert!(
172            self.inner.contains_ptr(data_ptr as *const ()),
173            "slot was not allocated from this slab"
174        );
175        mem::forget(ptr);
176
177        unsafe {
178            let value = core::ptr::read(data_ptr.cast::<T>());
179            self.inner
180                .free_ptr(data_ptr.cast::<SlotCell<AlignedBytes<N>>>());
181            value
182        }
183    }
184}
185
186// =============================================================================
187// Builder
188// =============================================================================
189
190/// Builder for [`Slab`].
191///
192/// Configures chunk capacity and optional pre-allocation before constructing
193/// the slab. The const generic `N` (slot size) only appears at the terminal
194/// [`build()`](Self::build) call.
195///
196/// # Example
197///
198/// ```
199/// use nexus_slab::byte::unbounded::Builder;
200///
201/// // SAFETY: caller guarantees slab contract (see Slab docs)
202/// let slab = unsafe {
203///     Builder::new()
204///         .chunk_capacity(64)
205///         .initial_chunks(2)
206///         .build::<256>()
207/// };
208/// let slot = slab.alloc(42u64);
209/// assert_eq!(*slot, 42);
210/// slab.free(slot);
211/// ```
212pub struct Builder {
213    chunk_capacity: usize,
214    initial_chunks: usize,
215}
216
217impl Builder {
218    /// Creates a new builder with default settings.
219    ///
220    /// Defaults: `chunk_capacity = 256`, `initial_chunks = 0` (lazy growth).
221    #[inline]
222    pub fn new() -> Self {
223        Self {
224            chunk_capacity: 256,
225            initial_chunks: 0,
226        }
227    }
228
229    /// Sets the capacity of each chunk.
230    ///
231    /// # Panics
232    ///
233    /// Panics at [`build()`](Self::build) if zero.
234    #[inline]
235    pub fn chunk_capacity(mut self, cap: usize) -> Self {
236        self.chunk_capacity = cap;
237        self
238    }
239
240    /// Sets the number of chunks to pre-allocate.
241    ///
242    /// Default is 0 (lazy growth — chunks allocated on first use).
243    #[inline]
244    pub fn initial_chunks(mut self, n: usize) -> Self {
245        self.initial_chunks = n;
246        self
247    }
248
249    /// Builds the byte slab.
250    ///
251    /// # Safety
252    ///
253    /// See [`Slab`] safety contract.
254    ///
255    /// # Panics
256    ///
257    /// Panics if `chunk_capacity` is zero.
258    #[inline]
259    pub unsafe fn build<const N: usize>(self) -> Slab<N> {
260        // SAFETY: caller upholds the slab contract.
261        let inner = unsafe {
262            crate::unbounded::Builder::new()
263                .chunk_capacity(self.chunk_capacity)
264                .initial_chunks(self.initial_chunks)
265                .build::<AlignedBytes<N>>()
266        };
267        Slab { inner }
268    }
269}
270
271impl Default for Builder {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl core::fmt::Debug for Builder {
278    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
279        f.debug_struct("Builder")
280            .field("chunk_capacity", &self.chunk_capacity)
281            .field("initial_chunks", &self.initial_chunks)
282            .finish()
283    }
284}
285
286/// Monomorphized free for `ByteClaim::Drop`.
287///
288/// Uses `free_ptr_in_chunk` for O(1) freelist return — no linear scan.
289unsafe fn free_raw_impl<const N: usize>(slab_ptr: *const u8, slot_ptr: *mut u8, chunk_idx: usize) {
290    let slab = unsafe { &*(slab_ptr as *const crate::unbounded::Slab<super::AlignedBytes<N>>) };
291    unsafe {
292        slab.free_ptr_in_chunk(slot_ptr.cast(), chunk_idx);
293    }
294}
295
296impl<const N: usize> core::fmt::Debug for Slab<N> {
297    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
298        f.debug_struct("byte::unbounded::Slab")
299            .field("slot_size", &N)
300            .finish()
301    }
302}
303
304#[cfg(test)]
305#[allow(clippy::float_cmp)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn basic_alloc_free() {
311        let slab: Slab<64> = unsafe { Slab::with_chunk_capacity(256) };
312        let ptr = slab.alloc(42u64);
313        assert_eq!(*ptr, 42);
314        slab.free(ptr);
315    }
316
317    #[test]
318    fn heterogeneous_types() {
319        let slab: Slab<128> = unsafe { Slab::with_chunk_capacity(256) };
320
321        let p1 = slab.alloc(42u64);
322        let p2 = slab.alloc(String::from("hello"));
323        let p3 = slab.alloc([1.0f64; 8]);
324
325        assert_eq!(*p1, 42);
326        assert_eq!(&*p2, "hello");
327        assert_eq!(p3[0], 1.0);
328
329        slab.free(p3);
330        slab.free(p2);
331        slab.free(p1);
332    }
333
334    #[test]
335    fn grows_automatically() {
336        let slab: Slab<16> = unsafe { Slab::with_chunk_capacity(2) };
337        let mut ptrs = alloc::vec::Vec::new();
338        for i in 0..100u64 {
339            ptrs.push(slab.alloc(i));
340        }
341        for (i, ptr) in ptrs.iter().enumerate() {
342            assert_eq!(**ptr, i as u64);
343        }
344        for ptr in ptrs {
345            slab.free(ptr);
346        }
347    }
348
349    #[test]
350    fn take_returns_value() {
351        let slab: Slab<64> = unsafe { Slab::with_chunk_capacity(256) };
352        let ptr = slab.alloc(String::from("taken"));
353        let val = slab.take(ptr);
354        assert_eq!(val, "taken");
355    }
356
357    // ========================================================================
358    // ByteClaim tests
359    // ========================================================================
360
361    #[test]
362    fn claim_write_typed() {
363        let slab: Slab<64> = unsafe { Slab::with_chunk_capacity(256) };
364        let claim = slab.claim();
365        let slot = claim.write(42u64);
366        assert_eq!(*slot, 42);
367        slab.free(slot);
368    }
369
370    #[test]
371    fn claim_drop_returns_to_freelist() {
372        let slab: Slab<64> = unsafe { Slab::with_chunk_capacity(1) };
373
374        // Claim, then abandon.
375        let claim = slab.claim();
376        drop(claim);
377
378        // Should be able to claim again.
379        let claim = slab.claim();
380        let slot = claim.write(99u64);
381        assert_eq!(*slot, 99);
382        slab.free(slot);
383    }
384
385    #[test]
386    fn claim_write_raw() {
387        let slab: Slab<64> = unsafe { Slab::with_chunk_capacity(256) };
388        let claim = slab.claim();
389        let val: u64 = 77;
390        let ptr = unsafe {
391            claim.write_raw(&val as *const u64 as *const u8, core::mem::size_of::<u64>())
392        };
393        assert_eq!(unsafe { *(ptr as *const u64) }, 77);
394        let slot = unsafe { super::Slot::<u64>::from_raw(ptr) };
395        slab.free(slot);
396    }
397
398    // ========================================================================
399    // Builder tests
400    // ========================================================================
401
402    #[test]
403    fn builder_defaults() {
404        let slab = unsafe { Builder::new().build::<64>() };
405        let slot = slab.alloc(42u64);
406        assert_eq!(*slot, 42);
407        slab.free(slot);
408    }
409
410    #[test]
411    fn builder_custom_chunk_capacity() {
412        let slab = unsafe { Builder::new().chunk_capacity(32).build::<64>() };
413        let slot = slab.alloc(1u64);
414        slab.free(slot);
415    }
416
417    #[test]
418    fn builder_initial_chunks() {
419        let slab = unsafe {
420            Builder::new()
421                .chunk_capacity(16)
422                .initial_chunks(3)
423                .build::<64>()
424        };
425        // 3 chunks × 16 slots = 48 total capacity via inner slab
426        let mut ptrs = alloc::vec::Vec::new();
427        for i in 0..48u64 {
428            ptrs.push(slab.alloc(i));
429        }
430        for ptr in ptrs {
431            slab.free(ptr);
432        }
433    }
434
435    #[test]
436    #[should_panic(expected = "chunk_capacity must be non-zero")]
437    fn builder_zero_chunk_capacity_panics() {
438        let _slab = unsafe { Builder::new().chunk_capacity(0).build::<64>() };
439    }
440}