housekeeping 0.0.3

A concurrent memory reclaimer for periodic cleanups.
Documentation
//! Implementations of batches.

use alloc::{boxed::Box, vec::Vec};

//----------- SimpleBatch ------------------------------------------------------

/// A batch of simple cleanup functions.
///
/// [`SimpleBatch`] is a collection of functions ([`FnOnce`] closures) that
/// clean up resources shared between threads. A batch can hold a limited number
/// of such functions.
///
/// See [the crate-level documentation](crate) for more information, including
/// examples and a usage guide.
pub struct SimpleBatch {
    /// The functions in the batch.
    //
    // TODO: Write a custom bump allocator so the closures don't need to be
    // individually heap-allocated. The bump allocator could also be reused.
    #[expect(clippy::type_complexity, reason = "pending simplification")]
    functions: Vec<(fn(*mut ()), *mut ())>,
}

impl SimpleBatch {
    /// The maximum number of functions in a [`SimpleBatch`].
    const MAX: usize = {
        if cfg!(feature = "loom") {
            // Use a smaller value to facilitate faster testing.
            4
        } else {
            32
        }
    };

    /// Construct a new [`SimpleBatch`].
    pub const fn new() -> Self {
        Self {
            functions: Vec::new(),
        }
    }

    /// Whether the batch is empty.
    pub const fn is_empty(&self) -> bool {
        self.functions.is_empty()
    }

    /// Whether the batch is full.
    pub const fn is_full(&self) -> bool {
        self.functions.len() >= Self::MAX
    }
}

impl Default for SimpleBatch {
    fn default() -> Self {
        Self::new()
    }
}

// SAFETY:
// - While 'functions' can hold non-'Send' items, such items are only added via
//   'Self::add_unchecked()', where the caller asserts that the items are safe
//   to send between threads.
unsafe impl Send for SimpleBatch {}

/// [`SimpleBatch`] executes cleanups when it is dropped.
impl Drop for SimpleBatch {
    fn drop(&mut self) {
        self.execute();
    }
}

impl SimpleBatch {
    /// Add a cleanup function to the batch.
    ///
    /// If the batch is full, the function will be returned.
    pub fn add<F>(&mut self, f: F) -> Result<(), F>
    where
        F: FnOnce() + Send + 'static,
    {
        // SAFETY: 'f' is known to be 'Send' by trait bound.
        unsafe { self.add_unchecked(f) }
    }

    /// Add a cleanup function to the batch, assuming it is [`Send`].
    ///
    /// If the batch is full, the function will be returned.
    ///
    /// This method is useful if the caller knows the function is [`Send`], but
    /// does not want to prove it by trait bound. This is useful because Rust
    /// can be conservative in which types are [`Send`]; for example, a closure
    /// that uses a raw pointer does not implement [`Send`].
    ///
    /// ## Safety
    ///
    /// `result = self.add_unchecked(f)` is sound if and only if:
    /// - `f` is sound to send (by value) between threads, i.e. it satisfies the
    ///   requirements of [`Send`],
    /// - `f` can be called at any time, i.e. it satisfies `'static`.
    pub unsafe fn add_unchecked<F: FnOnce()>(&mut self, f: F) -> Result<(), F> {
        if self.is_full() {
            return Err(f);
        }

        let data = Box::into_raw(Box::new(f)).cast::<()>();
        let func = |data: *mut ()| (unsafe { Box::from_raw(data.cast::<F>()) })();

        self.functions.push((func, data));
        Ok(())
    }

    /// Execute the functions in the batch.
    ///
    /// The batch will be cleared; all contained functions will be executed.
    /// The batch can then be reused.
    pub fn execute(&mut self) {
        for (func, data) in self.functions.drain(..) {
            (func)(data);
        }
    }
}