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 and return an RAII guard that owns the slab.
94///
95/// The guard restores the previous TLS state on drop (in its manual
96/// Drop body), then releases the slab memory (via field drop). Caller
97/// is responsible for ensuring the guard outlives any code that might
98/// dispatch through TLS — typically by storing it as the LAST field
99/// on the type that owns the runtime state. See BUG-1 (#167) for the
100/// failure mode this prevents.
101pub(crate) fn install_slab(slab: Box<dyn std::any::Any>, config: &SlabTlsConfig) -> SlabGuard {
102    let prev_ptr = SLAB_PTR.with(|c| c.replace(config.slab_ptr));
103    let prev_claim = SLAB_CLAIM.with(|c| c.replace(config.claim_fn));
104    let prev_free = SLAB_FREE.with(|c| c.replace(config.free_fn));
105    let prev_try_claim = SLAB_TRY_CLAIM.with(|c| c.replace(config.try_claim_fn));
106    let prev_claim_free = SLAB_CLAIM_FREE.with(|c| c.replace(config.claim_free_fn));
107    let prev_slot_size = SLAB_SLOT_SIZE.with(|c| c.replace(config.slot_size));
108    SlabGuard {
109        prev_ptr,
110        prev_claim,
111        prev_free,
112        prev_try_claim,
113        prev_claim_free,
114        prev_slot_size,
115        _slab: slab,
116    }
117}
118
119#[allow(clippy::struct_field_names)]
120pub(crate) struct SlabGuard {
121    prev_ptr: *const u8,
122    prev_claim: ClaimFn,
123    prev_free: FreeFn,
124    prev_try_claim: TryClaimFn,
125    prev_claim_free: ClaimFreeFn,
126    prev_slot_size: usize,
127
128    // Owns the type-erased slab. Drops AFTER the manual Drop body
129    // returns, so TLS is already restored when slab memory is released.
130    // Slab's own Drop touches its own memory only — never the TLS
131    // dispatch path — so this ordering is safe even though prev_* may
132    // point to the no-slab panic stubs at that point.
133    _slab: Box<dyn std::any::Any>,
134}
135
136impl Drop for SlabGuard {
137    fn drop(&mut self) {
138        // Restore TLS to whatever was there before this install.
139        // After this body returns, _slab field drops and the slab is freed.
140        SLAB_PTR.with(|c| c.set(self.prev_ptr));
141        SLAB_CLAIM.with(|c| c.set(self.prev_claim));
142        SLAB_FREE.with(|c| c.set(self.prev_free));
143        SLAB_TRY_CLAIM.with(|c| c.set(self.prev_try_claim));
144        SLAB_CLAIM_FREE.with(|c| c.set(self.prev_claim_free));
145        SLAB_SLOT_SIZE.with(|c| c.set(self.prev_slot_size));
146    }
147}
148
149// =============================================================================
150// spawn_slab — allocate + enqueue in one step
151// =============================================================================
152
153/// Allocate a joinable task in the slab and return its raw pointer.
154///
155/// # Panics
156///
157/// - If no slab is configured.
158/// - If the slab is full (bounded slab).
159/// - If the task exceeds the slab's slot size.
160pub(crate) fn slab_spawn<F>(future: F, tracker_key: u32) -> *mut u8
161where
162    F: Future + 'static,
163    F::Output: 'static,
164{
165    // See Executor::spawn_boxed for the cross_wake_ctx contract.
166    let cross_wake_ctx = crate::cross_wake::current_runtime_ctx();
167    let task = crate::task::new_joinable_slab(future, tracker_key, slab_free_task, cross_wake_ctx);
168    let size = std::mem::size_of_val(&task);
169    let src = std::ptr::from_ref(&task).cast::<u8>();
170
171    let claim = SLAB_CLAIM.with(Cell::get);
172    // SAFETY: claim copies `size` bytes from `src` into a slab slot.
173    let ptr = unsafe { claim(src, size) };
174    assert!(!ptr.is_null(), "slab full — spawn_slab failed");
175
176    // Task was copied into the slab. Prevent stack drop.
177    std::mem::forget(task);
178
179    ptr
180}
181
182/// Free function stored in slab-allocated task headers.
183unsafe fn slab_free_task(ptr: *mut u8) {
184    let free = SLAB_FREE.with(Cell::get);
185    unsafe { free(ptr) };
186}
187
188// =============================================================================
189// SlabClaim — reserved slot handle (lifetime-free)
190// =============================================================================
191
192/// A reserved slab slot for the async runtime.
193///
194/// Call `.spawn(future)` to write a task and enqueue it, or drop to
195/// return the slot to the freelist. Nothing is lost on drop — the
196/// future was never constructed.
197///
198/// Lifetime-free — safe because the runtime owns the slab for the
199/// duration of `block_on`, and `SlabClaim` can only be created inside
200/// `block_on`.
201pub struct SlabClaim {
202    ptr: *mut u8,
203    slab_ptr: *const u8,
204    free: ClaimFreeFn,
205    chunk_idx: usize,
206    slot_size: usize,
207    // !Send + !Sync — must stay on the runtime thread.
208    _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
209}
210
211impl SlabClaim {
212    /// Write a task into the reserved slot and enqueue it.
213    ///
214    /// Consumes the claim. The future is constructed, placed in the
215    /// slab slot, and pushed to the executor's ready queue.
216    pub fn spawn<F>(self, future: F) -> crate::task::JoinHandle<F::Output>
217    where
218        F: Future + 'static,
219        F::Output: 'static,
220    {
221        crate::runtime::with_executor(|exec| {
222            let tracker_key = exec.next_tracker_key();
223            // See Executor::spawn_boxed for the cross_wake_ctx contract.
224            let cross_wake_ctx = crate::cross_wake::current_runtime_ctx();
225            let task =
226                crate::task::new_joinable_slab(future, tracker_key, slab_free_task, cross_wake_ctx);
227            let size = std::mem::size_of_val(&task);
228
229            assert!(
230                size <= self.slot_size,
231                "task size ({size} bytes) exceeds slab slot size ({} bytes)",
232                self.slot_size,
233            );
234
235            let src = std::ptr::from_ref(&task).cast::<u8>();
236            // SAFETY: ptr is a valid vacant slot, src has `size` valid bytes.
237            unsafe { std::ptr::copy_nonoverlapping(src, self.ptr, size) };
238            std::mem::forget(task);
239
240            let ptr = self.ptr;
241            // Don't run Drop — the slot is now occupied.
242            std::mem::forget(self);
243
244            exec.spawn_raw(ptr);
245            crate::task::JoinHandle::new(ptr)
246        })
247    }
248
249    /// Raw pointer to the reserved slot.
250    pub fn as_ptr(&self) -> *mut u8 {
251        self.ptr
252    }
253
254    /// Slot capacity in bytes.
255    pub fn slot_size(&self) -> usize {
256        self.slot_size
257    }
258}
259
260impl Drop for SlabClaim {
261    fn drop(&mut self) {
262        // Slot claimed but never written — return to freelist.
263        // SAFETY: free was captured at claim time from TLS.
264        // The slab is alive (runtime owns it for the duration of block_on).
265        unsafe { (self.free)(self.slab_ptr, self.ptr, self.chunk_idx) };
266    }
267}
268
269impl std::fmt::Debug for SlabClaim {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        f.debug_struct("SlabClaim")
272            .field("ptr", &self.ptr)
273            .field("slot_size", &self.slot_size)
274            .finish()
275    }
276}
277
278// =============================================================================
279// Public claim API
280// =============================================================================
281
282/// Try to reserve a slab slot. Returns `None` if the slab is full.
283///
284/// # Panics
285///
286/// - If called outside a runtime context.
287/// - If no slab is configured.
288pub(crate) fn try_claim() -> Option<SlabClaim> {
289    let try_claim_fn = SLAB_TRY_CLAIM.with(Cell::get);
290    // SAFETY: try_claim_fn claims a slot from the slab.
291    let (ptr, chunk_idx) = unsafe { try_claim_fn() };
292    if ptr.is_null() {
293        return None;
294    }
295
296    let slab_ptr = SLAB_PTR.with(Cell::get);
297    let free = SLAB_CLAIM_FREE.with(Cell::get);
298    let slot_size = SLAB_SLOT_SIZE.with(Cell::get);
299
300    Some(SlabClaim {
301        ptr,
302        slab_ptr,
303        free,
304        chunk_idx,
305        slot_size,
306        _not_send: std::marker::PhantomData,
307    })
308}
309
310/// Reserve a slab slot. Panics if full.
311///
312/// # Panics
313///
314/// - If called outside a runtime context.
315/// - If no slab is configured.
316/// - If the slab is full (bounded slab).
317pub(crate) fn claim() -> SlabClaim {
318    try_claim().expect("slab full — claim_slab failed")
319}
320
321// =============================================================================
322// Monomorphized fn pointers (created at builder time)
323// =============================================================================
324
325/// Build TLS config for an unbounded slab.
326pub(crate) fn make_unbounded_config<const S: usize>(slab_ptr: *const u8) -> SlabTlsConfig {
327    SlabTlsConfig {
328        slab_ptr,
329        claim_fn: unbounded_claim::<S>,
330        free_fn: unbounded_free::<S>,
331        try_claim_fn: unbounded_try_claim::<S>,
332        claim_free_fn: unbounded_claim_free::<S>,
333        slot_size: S,
334    }
335}
336
337/// Build TLS config for a bounded slab.
338pub(crate) fn make_bounded_config<const S: usize>(slab_ptr: *const u8) -> SlabTlsConfig {
339    SlabTlsConfig {
340        slab_ptr,
341        claim_fn: bounded_claim::<S>,
342        free_fn: bounded_free::<S>,
343        try_claim_fn: bounded_try_claim::<S>,
344        claim_free_fn: bounded_claim_free::<S>,
345        slot_size: S,
346    }
347}
348
349// -- Unbounded --
350
351unsafe fn unbounded_claim<const S: usize>(src: *const u8, size: usize) -> *mut u8 {
352    let slab_ptr = SLAB_PTR.with(Cell::get);
353    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
354    unsafe { slab.alloc_raw(src, size) }
355}
356
357unsafe fn unbounded_free<const S: usize>(ptr: *mut u8) {
358    let slab_ptr = SLAB_PTR.with(Cell::get);
359    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
360    let slot = unsafe { nexus_slab::byte::Slot::<u8>::from_raw(ptr) };
361    slab.free(slot);
362}
363
364unsafe fn unbounded_try_claim<const S: usize>() -> (*mut u8, usize) {
365    let slab_ptr = SLAB_PTR.with(Cell::get);
366    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
367    let claim = slab.claim();
368    let ptr = claim.as_ptr();
369    let chunk_idx = claim.chunk_idx();
370    // Consume without running Drop.
371    std::mem::forget(claim);
372    (ptr, chunk_idx)
373}
374
375unsafe fn unbounded_claim_free<const S: usize>(
376    slab_ptr: *const u8,
377    ptr: *mut u8,
378    chunk_idx: usize,
379) {
380    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::unbounded::Slab<S>) };
381    // O(1) — goes directly to the correct chunk's freelist.
382    unsafe { slab.free_raw_in_chunk(ptr, chunk_idx) };
383}
384
385// -- Bounded --
386
387unsafe fn bounded_claim<const S: usize>(src: *const u8, size: usize) -> *mut u8 {
388    let slab_ptr = SLAB_PTR.with(Cell::get);
389    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
390    unsafe { slab.alloc_raw(src, size) }
391}
392
393unsafe fn bounded_free<const S: usize>(ptr: *mut u8) {
394    let slab_ptr = SLAB_PTR.with(Cell::get);
395    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
396    let slot = unsafe { nexus_slab::byte::Slot::<u8>::from_raw(ptr) };
397    slab.free(slot);
398}
399
400unsafe fn bounded_try_claim<const S: usize>() -> (*mut u8, usize) {
401    let slab_ptr = SLAB_PTR.with(Cell::get);
402    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
403    slab.try_claim().map_or((std::ptr::null_mut(), 0), |claim| {
404        let ptr = claim.as_ptr();
405        std::mem::forget(claim);
406        (ptr, 0) // bounded = single chunk
407    })
408}
409
410unsafe fn bounded_claim_free<const S: usize>(slab_ptr: *const u8, ptr: *mut u8, _chunk_idx: usize) {
411    let slab = unsafe { &*(slab_ptr as *const nexus_slab::byte::bounded::Slab<S>) };
412    unsafe { slab.free_raw(ptr) };
413}