Skip to main content

nexus_async_rt/
alloc.rs

1//! Slab task allocator — optional, power-user feature.
2//!
3//! By default, tasks are Box-allocated. For zero-alloc hot-path spawning,
4//! configure a slab via [`RuntimeBuilder::slab_unbounded`] or
5//! [`RuntimeBuilder::slab_bounded`].
6//!
7//! Three levels of control:
8//! - **`spawn_slab(future)`** — allocate and enqueue in one call. Panics if full.
9//! - **`claim_slab()`** — reserve a slot, then `.spawn(future)` later. Panics if full.
10//! - **`try_claim_slab()`** — reserve if space available. Nothing lost on failure.
11
12use std::cell::Cell;
13use std::future::Future;
14
15// Task construction goes through crate::task::{new_joinable_slab, ...}
16
17// =============================================================================
18// TLS slots
19// =============================================================================
20
21/// Claim a slab slot, copy `size` bytes from `src`, return raw pointer.
22/// Returns null if the slab is full (bounded only).
23type ClaimFn = unsafe fn(src: *const u8, size: usize) -> *mut u8;
24
25/// Try to claim a vacant slab slot without writing.
26/// Returns (ptr, chunk_idx) or (null, 0) if full.
27type TryClaimFn = unsafe fn() -> (*mut u8, usize);
28
29/// Free a slab slot (used by task header free_fn).
30type FreeFn = unsafe fn(ptr: *mut u8);
31
32/// Free a slab slot with full context (used by SlabClaim::Drop).
33type ClaimFreeFn = unsafe fn(slab_ptr: *const u8, ptr: *mut u8, chunk_idx: usize);
34
35thread_local! {
36    /// Raw pointer to the slab instance.
37    static SLAB_PTR: Cell<*const u8> = const { Cell::new(std::ptr::null()) };
38
39    /// Fn pointer: claim a slot and copy task bytes into it.
40    static SLAB_CLAIM: Cell<ClaimFn> = const { Cell::new(no_slab_claim) };
41
42    /// Fn pointer: free a slab slot (task header path).
43    static SLAB_FREE: Cell<FreeFn> = const { Cell::new(no_slab_free) };
44
45    /// Fn pointer: try to claim a vacant slot (returns ptr + chunk_idx).
46    static SLAB_TRY_CLAIM: Cell<TryClaimFn> = const { Cell::new(no_slab_try_claim) };
47
48    /// Fn pointer: free a claimed slot (SlabClaim::Drop path).
49    static SLAB_CLAIM_FREE: Cell<ClaimFreeFn> = const { Cell::new(no_slab_claim_free) };
50
51    /// Configured slot size in bytes.
52    static SLAB_SLOT_SIZE: Cell<usize> = const { Cell::new(0) };
53}
54
55// -- Panic stubs --
56
57unsafe fn no_slab_claim(_src: *const u8, _size: usize) -> *mut u8 {
58    panic!(
59        "spawn_slab() called without a slab configured — \
60         use Runtime::builder().slab_unbounded(slab) or .slab_bounded(slab)"
61    )
62}
63
64unsafe fn no_slab_free(_ptr: *mut u8) {
65    panic!("slab free called without a slab configured")
66}
67
68unsafe fn no_slab_try_claim() -> (*mut u8, usize) {
69    panic!(
70        "try_claim_slab()/claim_slab() called without a slab configured — \
71         use Runtime::builder().slab_unbounded(slab) or .slab_bounded(slab)"
72    )
73}
74
75unsafe fn no_slab_claim_free(_slab_ptr: *const u8, _ptr: *mut u8, _chunk_idx: usize) {
76    panic!("slab claim free called without a slab configured")
77}
78
79// =============================================================================
80// TLS install/guard
81// =============================================================================
82
83/// Configuration for slab TLS installation.
84pub(crate) struct SlabTlsConfig {
85    pub(crate) slab_ptr: *const u8,
86    pub(crate) claim_fn: ClaimFn,
87    pub(crate) free_fn: FreeFn,
88    pub(crate) try_claim_fn: TryClaimFn,
89    pub(crate) claim_free_fn: ClaimFreeFn,
90    pub(crate) slot_size: usize,
91}
92
93/// Install slab TLS from a config. Returns RAII guard.
94pub(crate) fn install_slab(config: &SlabTlsConfig) -> SlabGuard {
95    let prev_ptr = SLAB_PTR.with(|c| c.replace(config.slab_ptr));
96    let prev_claim = SLAB_CLAIM.with(|c| c.replace(config.claim_fn));
97    let prev_free = SLAB_FREE.with(|c| c.replace(config.free_fn));
98    let prev_try_claim = SLAB_TRY_CLAIM.with(|c| c.replace(config.try_claim_fn));
99    let prev_claim_free = SLAB_CLAIM_FREE.with(|c| c.replace(config.claim_free_fn));
100    let prev_slot_size = SLAB_SLOT_SIZE.with(|c| c.replace(config.slot_size));
101    SlabGuard {
102        prev_ptr,
103        prev_claim,
104        prev_free,
105        prev_try_claim,
106        prev_claim_free,
107        prev_slot_size,
108    }
109}
110
111#[allow(clippy::struct_field_names)]
112pub(crate) struct SlabGuard {
113    prev_ptr: *const u8,
114    prev_claim: ClaimFn,
115    prev_free: FreeFn,
116    prev_try_claim: TryClaimFn,
117    prev_claim_free: ClaimFreeFn,
118    prev_slot_size: usize,
119}
120
121impl Drop for SlabGuard {
122    fn drop(&mut self) {
123        SLAB_PTR.with(|c| c.set(self.prev_ptr));
124        SLAB_CLAIM.with(|c| c.set(self.prev_claim));
125        SLAB_FREE.with(|c| c.set(self.prev_free));
126        SLAB_TRY_CLAIM.with(|c| c.set(self.prev_try_claim));
127        SLAB_CLAIM_FREE.with(|c| c.set(self.prev_claim_free));
128        SLAB_SLOT_SIZE.with(|c| c.set(self.prev_slot_size));
129    }
130}
131
132// =============================================================================
133// spawn_slab — allocate + enqueue in one step
134// =============================================================================
135
136/// Allocate a joinable task in the slab and return its raw pointer.
137///
138/// # Panics
139///
140/// - If no slab is configured.
141/// - If the slab is full (bounded slab).
142/// - If the task exceeds the slab's slot size.
143pub(crate) fn slab_spawn<F>(future: F, tracker_key: u32) -> *mut u8
144where
145    F: Future + 'static,
146    F::Output: 'static,
147{
148    let task = crate::task::new_joinable_slab(future, tracker_key, slab_free_task);
149    let size = std::mem::size_of_val(&task);
150    let src = std::ptr::from_ref(&task).cast::<u8>();
151
152    let claim = SLAB_CLAIM.with(Cell::get);
153    // SAFETY: claim copies `size` bytes from `src` into a slab slot.
154    let ptr = unsafe { claim(src, size) };
155    assert!(!ptr.is_null(), "slab full — spawn_slab failed");
156
157    // Task was copied into the slab. Prevent stack drop.
158    std::mem::forget(task);
159
160    ptr
161}
162
163/// Free function stored in slab-allocated task headers.
164unsafe fn slab_free_task(ptr: *mut u8) {
165    let free = SLAB_FREE.with(Cell::get);
166    unsafe { free(ptr) };
167}
168
169// =============================================================================
170// SlabClaim — reserved slot handle (lifetime-free)
171// =============================================================================
172
173/// A reserved slab slot for the async runtime.
174///
175/// Call `.spawn(future)` to write a task and enqueue it, or drop to
176/// return the slot to the freelist. Nothing is lost on drop — the
177/// future was never constructed.
178///
179/// Lifetime-free — safe because the runtime owns the slab for the
180/// duration of `block_on`, and `SlabClaim` can only be created inside
181/// `block_on`.
182pub struct SlabClaim {
183    ptr: *mut u8,
184    slab_ptr: *const u8,
185    free: ClaimFreeFn,
186    chunk_idx: usize,
187    slot_size: usize,
188    // !Send + !Sync — must stay on the runtime thread.
189    _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
190}
191
192impl SlabClaim {
193    /// Write a task into the reserved slot and enqueue it.
194    ///
195    /// Consumes the claim. The future is constructed, placed in the
196    /// slab slot, and pushed to the executor's ready queue.
197    pub fn spawn<F>(self, future: F) -> crate::task::JoinHandle<F::Output>
198    where
199        F: Future + 'static,
200        F::Output: 'static,
201    {
202        crate::runtime::with_executor(|exec| {
203            let tracker_key = exec.next_tracker_key();
204            let task = crate::task::new_joinable_slab(future, tracker_key, slab_free_task);
205            let size = std::mem::size_of_val(&task);
206
207            assert!(
208                size <= self.slot_size,
209                "task size ({size} bytes) exceeds slab slot size ({} bytes)",
210                self.slot_size,
211            );
212
213            let src = std::ptr::from_ref(&task).cast::<u8>();
214            // SAFETY: ptr is a valid vacant slot, src has `size` valid bytes.
215            unsafe { std::ptr::copy_nonoverlapping(src, self.ptr, size) };
216            std::mem::forget(task);
217
218            let ptr = self.ptr;
219            // Don't run Drop — the slot is now occupied.
220            std::mem::forget(self);
221
222            exec.spawn_raw(ptr);
223            crate::task::JoinHandle::new(ptr)
224        })
225    }
226
227    /// Raw pointer to the reserved slot.
228    pub fn as_ptr(&self) -> *mut u8 {
229        self.ptr
230    }
231
232    /// Slot capacity in bytes.
233    pub fn slot_size(&self) -> usize {
234        self.slot_size
235    }
236}
237
238impl Drop for SlabClaim {
239    fn drop(&mut self) {
240        // Slot claimed but never written — return to freelist.
241        // SAFETY: free was captured at claim time from TLS.
242        // The slab is alive (runtime owns it for the duration of block_on).
243        unsafe { (self.free)(self.slab_ptr, self.ptr, self.chunk_idx) };
244    }
245}
246
247impl std::fmt::Debug for SlabClaim {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        f.debug_struct("SlabClaim")
250            .field("ptr", &self.ptr)
251            .field("slot_size", &self.slot_size)
252            .finish()
253    }
254}
255
256// =============================================================================
257// Public claim API
258// =============================================================================
259
260/// Try to reserve a slab slot. Returns `None` if the slab is full.
261///
262/// # Panics
263///
264/// - If called outside a runtime context.
265/// - If no slab is configured.
266pub(crate) fn try_claim() -> Option<SlabClaim> {
267    let try_claim_fn = SLAB_TRY_CLAIM.with(Cell::get);
268    // SAFETY: try_claim_fn claims a slot from the slab.
269    let (ptr, chunk_idx) = unsafe { try_claim_fn() };
270    if ptr.is_null() {
271        return None;
272    }
273
274    let slab_ptr = SLAB_PTR.with(Cell::get);
275    let free = SLAB_CLAIM_FREE.with(Cell::get);
276    let slot_size = SLAB_SLOT_SIZE.with(Cell::get);
277
278    Some(SlabClaim {
279        ptr,
280        slab_ptr,
281        free,
282        chunk_idx,
283        slot_size,
284        _not_send: std::marker::PhantomData,
285    })
286}
287
288/// Reserve a slab slot. Panics if full.
289///
290/// # Panics
291///
292/// - If called outside a runtime context.
293/// - If no slab is configured.
294/// - If the slab is full (bounded slab).
295pub(crate) fn claim() -> SlabClaim {
296    try_claim().expect("slab full — claim_slab failed")
297}
298
299// =============================================================================
300// Monomorphized fn pointers (created at builder time)
301// =============================================================================
302
303/// Build TLS config for an unbounded slab.
304pub(crate) fn make_unbounded_config<const S: usize>(slab_ptr: *const u8) -> SlabTlsConfig {
305    SlabTlsConfig {
306        slab_ptr,
307        claim_fn: unbounded_claim::<S>,
308        free_fn: unbounded_free::<S>,
309        try_claim_fn: unbounded_try_claim::<S>,
310        claim_free_fn: unbounded_claim_free::<S>,
311        slot_size: S,
312    }
313}
314
315/// Build TLS config for a bounded slab.
316pub(crate) fn make_bounded_config<const S: usize>(slab_ptr: *const u8) -> SlabTlsConfig {
317    SlabTlsConfig {
318        slab_ptr,
319        claim_fn: bounded_claim::<S>,
320        free_fn: bounded_free::<S>,
321        try_claim_fn: bounded_try_claim::<S>,
322        claim_free_fn: bounded_claim_free::<S>,
323        slot_size: S,
324    }
325}
326
327// -- Unbounded --
328
329unsafe fn unbounded_claim<const S: usize>(src: *const u8, size: usize) -> *mut u8 {
330    let slab_ptr = SLAB_PTR.with(Cell::get);
331    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
332    unsafe { slab.alloc_raw(src, size) }
333}
334
335unsafe fn unbounded_free<const S: usize>(ptr: *mut u8) {
336    let slab_ptr = SLAB_PTR.with(Cell::get);
337    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
338    let slot = unsafe { nexus_slab::byte::Slot::<u8>::from_raw(ptr) };
339    slab.free(slot);
340}
341
342unsafe fn unbounded_try_claim<const S: usize>() -> (*mut u8, usize) {
343    let slab_ptr = SLAB_PTR.with(Cell::get);
344    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
345    let claim = slab.claim();
346    let ptr = claim.as_ptr();
347    let chunk_idx = claim.chunk_idx();
348    // Consume without running Drop.
349    std::mem::forget(claim);
350    (ptr, chunk_idx)
351}
352
353unsafe fn unbounded_claim_free<const S: usize>(
354    slab_ptr: *const u8,
355    ptr: *mut u8,
356    chunk_idx: usize,
357) {
358    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
359    // O(1) — goes directly to the correct chunk's freelist.
360    unsafe { slab.free_raw_in_chunk(ptr, chunk_idx) };
361}
362
363// -- Bounded --
364
365unsafe fn bounded_claim<const S: usize>(src: *const u8, size: usize) -> *mut u8 {
366    let slab_ptr = SLAB_PTR.with(Cell::get);
367    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
368    unsafe { slab.alloc_raw(src, size) }
369}
370
371unsafe fn bounded_free<const S: usize>(ptr: *mut u8) {
372    let slab_ptr = SLAB_PTR.with(Cell::get);
373    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
374    let slot = unsafe { nexus_slab::byte::Slot::<u8>::from_raw(ptr) };
375    slab.free(slot);
376}
377
378unsafe fn bounded_try_claim<const S: usize>() -> (*mut u8, usize) {
379    let slab_ptr = SLAB_PTR.with(Cell::get);
380    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
381    slab.try_claim().map_or((std::ptr::null_mut(), 0), |claim| {
382        let ptr = claim.as_ptr();
383        std::mem::forget(claim);
384        (ptr, 0) // bounded = single chunk
385    })
386}
387
388unsafe fn bounded_claim_free<const S: usize>(slab_ptr: *const u8, ptr: *mut u8, _chunk_idx: usize) {
389    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
390    unsafe { slab.free_raw(ptr) };
391}