future_form_ffi 0.1.0

FFI support for future_form: host-driven polling, opaque handles, and effect slots
Documentation
//! A lock-free, thread-safe slot that holds at most one value.
//!
//! [`AtomicSlot`] encapsulates all [`AtomicPtr`] unsafety behind a safe
//! move-only API: [`put`](AtomicSlot::put) and [`take`](AtomicSlot::take).
//! It is the building block for [`EffectSlot`](crate::effect_slot::EffectSlot),
//! which layers the effect-protocol semantics on top.
//!
//! # Example
//!
//! ```rust
//! use future_form_ffi::atomic_slot::AtomicSlot;
//!
//! let slot: AtomicSlot<u64> = AtomicSlot::new();
//!
//! assert_eq!(slot.take(), None);
//!
//! slot.put(42);
//! assert_eq!(slot.take(), Some(42));
//! assert_eq!(slot.take(), None);
//! ```
//!
//! # Design constraints
//!
//! The API is intentionally limited to move-only operations. There is no
//! `peek(&self) -> &T` or similar borrowing method because the value
//! could be taken (and freed) on another thread while a reference exists.
//! _Do not add methods that return references to slot contents._
//!
//! # Safety invariants
//!
//! The following invariants are maintained internally and must be
//! preserved if this module is extended:
//!
//! 1. Every non-null pointer stored in the [`AtomicPtr`] was produced
//!    by [`Box::into_raw`]. This ensures [`Box::from_raw`] is valid.
//!
//! 2. [`AtomicPtr::swap`] with [`Ordering::AcqRel`] is the _only_
//!    way pointers are read from the slot. This guarantees exclusive
//!    access: exactly one caller receives a given pointer.
//!
//! 3. **No borrowing into the slot.** Only `take() -> Option<T>` and
//!    `put(T)` — move-only operations (see above).
//!
//! 4. [`Drop`] uses [`AtomicPtr::get_mut`] (which requires `&mut self`)
//!    to clean up, guaranteeing no concurrent access during teardown.

use alloc::boxed::Box;
use core::{
    ptr,
    sync::atomic::{AtomicPtr, Ordering},
};

/// A lock-free, thread-safe slot that holds at most one `T`.
///
/// Values are heap-allocated on [`put`](Self::put) and reclaimed on
/// [`take`](Self::take). The slot is either empty (null) or holds a
/// single boxed value.
pub struct AtomicSlot<T> {
    ptr: AtomicPtr<T>,
}

// SAFETY: `AtomicPtr` provides atomic access to the heap-allocated value.
// `T: Send` is required because values cross thread boundaries via the
// atomic swap — a value produced on one thread may be consumed on another.
//
// When all access is single-threaded (typical FFI polling), the `Send`
// bound is satisfied trivially.
unsafe impl<T: Send> Send for AtomicSlot<T> {}
unsafe impl<T: Send> Sync for AtomicSlot<T> {}

impl<T> AtomicSlot<T> {
    /// Create an empty slot.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            ptr: AtomicPtr::new(ptr::null_mut()),
        }
    }

    /// Store a value, dropping any previous occupant.
    pub fn put(&self, val: T) {
        let new_ptr = Box::into_raw(Box::new(val));
        let old_ptr = self.ptr.swap(new_ptr, Ordering::AcqRel);
        if !old_ptr.is_null() {
            // SAFETY: non-null pointer was produced by `Box::into_raw`
            // (invariant 1). `swap` guarantees exclusive access (invariant 2).
            drop(unsafe { Box::from_raw(old_ptr) });
        }
    }

    /// Take the current value, leaving the slot empty.
    ///
    /// Returns `Some(value)` if the slot was occupied, `None` otherwise.
    pub fn take(&self) -> Option<T> {
        let ptr = self.ptr.swap(ptr::null_mut(), Ordering::AcqRel);
        if ptr.is_null() {
            None
        } else {
            // SAFETY: non-null pointer was produced by `Box::into_raw`
            // (invariant 1). `swap` guarantees exclusive access (invariant 2).
            Some(*unsafe { Box::from_raw(ptr) })
        }
    }
}

impl<T> Default for AtomicSlot<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T> core::fmt::Debug for AtomicSlot<T> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("AtomicSlot").finish_non_exhaustive()
    }
}

impl<T> Drop for AtomicSlot<T> {
    fn drop(&mut self) {
        // `get_mut` takes `&mut self`, guaranteeing exclusive access —
        // no concurrent swap can be in flight (invariant 4).
        let ptr = *self.ptr.get_mut();
        if !ptr.is_null() {
            // SAFETY: non-null pointer was produced by `Box::into_raw`.
            drop(unsafe { Box::from_raw(ptr) });
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_slot_returns_none() {
        let slot: AtomicSlot<u64> = AtomicSlot::new();
        assert_eq!(slot.take(), None);
    }

    #[test]
    fn put_then_take() {
        let slot = AtomicSlot::new();
        slot.put(42u64);
        assert_eq!(slot.take(), Some(42));
    }

    #[test]
    fn take_empties_slot() {
        let slot = AtomicSlot::new();
        slot.put(42u64);
        assert_eq!(slot.take(), Some(42));
        assert_eq!(slot.take(), None);
    }

    #[test]
    fn put_overwrites_previous() {
        let slot = AtomicSlot::new();
        slot.put(1u64);
        slot.put(2);
        assert_eq!(slot.take(), Some(2));
        assert_eq!(slot.take(), None);
    }

    #[test]
    fn default_is_empty() {
        let slot: AtomicSlot<u64> = AtomicSlot::default();
        assert_eq!(slot.take(), None);
    }

    #[test]
    fn debug_is_opaque() {
        let slot: AtomicSlot<u64> = AtomicSlot::new();
        let dbg = alloc::format!("{slot:?}");
        assert!(dbg.contains("AtomicSlot"));
    }

    #[test]
    fn drop_cleans_up_occupied_slot() {
        use alloc::rc::Rc;

        let rc = Rc::new(());
        let slot = AtomicSlot::new();
        slot.put(rc.clone());
        assert_eq!(Rc::strong_count(&rc), 2);

        drop(slot);
        assert_eq!(Rc::strong_count(&rc), 1);
    }

    #[test]
    fn put_drops_overwritten_value() {
        use alloc::rc::Rc;

        let first = Rc::new(());
        let second = Rc::new(());
        let slot = AtomicSlot::new();

        slot.put(first.clone());
        assert_eq!(Rc::strong_count(&first), 2);

        slot.put(second.clone());
        assert_eq!(Rc::strong_count(&first), 1);
        assert_eq!(Rc::strong_count(&second), 2);
    }

    #[test]
    fn works_with_non_copy_types() {
        let slot = AtomicSlot::new();
        slot.put(alloc::string::String::from("hello"));
        assert_eq!(slot.take(), Some(alloc::string::String::from("hello")));
    }

    #[test]
    fn multiple_put_take_cycles() {
        let slot = AtomicSlot::new();
        for i in 0u64..10 {
            slot.put(i);
            assert_eq!(slot.take(), Some(i));
            assert_eq!(slot.take(), None);
        }
    }
}