future_form_ffi 0.1.0

FFI support for future_form: host-driven polling, opaque handles, and effect slots
Documentation
//! Shared effect channel between async state machines and FFI bridges.
//!
//! An [`EffectSlot`] mediates the sans-IO effect protocol: async code
//! _requests_ services from the host (timestamps, logging, I/O) by
//! writing an effect, and the host _fulfills_ it by writing a response.
//!
//! ```text
//! Async code                    Host (via FFI bridge)
//!     │                              │
//!     │  request(&effect)            │
//!     │──────────────────────────>   │
//!     │  Poll::Pending               │
//!     │                              │
//!     │              take_effect()   │
//!     │   <──────────────────────────│
//!     │              Some(effect)    │
//!     │                              │
//!     │              fulfill(resp)   │
//!     │   <──────────────────────────│
//!     │                              │
//!     │  request(&effect)            │
//!     │──────────────────────────>   │
//!     │  Poll::Ready(response)       │
//!     │                              │
//! ```
//!
//! # Example
//!
//! ```rust
//! use core::task::Poll;
//! use future_form_ffi::effect_slot::EffectSlot;
//!
//! #[derive(Debug, Clone, PartialEq)]
//! enum Effect { GetTimestamp }
//!
//! #[derive(Debug, PartialEq)]
//! enum Response { Timestamp(u64) }
//!
//! let slot: EffectSlot<Effect, Response> = EffectSlot::new();
//!
//! // Async code requests an effect (returns Pending on first call).
//! assert_eq!(slot.request(&Effect::GetTimestamp), Poll::Pending);
//!
//! // Host reads the pending effect.
//! assert_eq!(slot.take_effect(), Some(Effect::GetTimestamp));
//!
//! // Host fulfills the effect.
//! slot.fulfill(Response::Timestamp(1234567890));
//!
//! // Async code retries — now gets the response.
//! assert_eq!(
//!     slot.request(&Effect::GetTimestamp),
//!     Poll::Ready(Response::Timestamp(1234567890)),
//! );
//! ```

use core::task::Poll;

use crate::atomic_slot::AtomicSlot;

/// Shared-state channel for the effect protocol between async code and
/// an FFI host.
///
/// `EffectSlot` is single-slot: one pending effect, one response. This
/// matches the [`Future::poll`] model naturally — one poll yields at most
/// one effect. If you need batching, compose multiple `EffectSlot`s or
/// build a queue on top.
///
/// # Slot ownership
///
/// Each `EffectSlot` is a single conversation channel. If multiple
/// futures share one slot, they will overwrite each other's pending
/// effects and receive responses meant for other futures. This is
/// memory-safe but semantically incorrect.
///
/// For **one future at a time**, embed the slot in the struct.
///
/// For **unlimited concurrent futures**, create a fresh
/// `Arc<EffectSlot>` per future, sharing it between the async closure
/// and the [`EffectHandle`](crate::effect_handle::EffectHandle).
/// The host stores handles however its concurrency model demands
/// (a map, inline in goroutines/threads, or a vec in an event loop).
/// See the [crate-level docs](crate#slot-ownership) for ownership
/// examples and [handle storage](crate#handle-storage) for host-side
/// patterns.
///
/// # Implementation
///
/// Thread safety is provided by [`AtomicSlot`], which uses lock-free
/// [`AtomicPtr`](core::sync::atomic::AtomicPtr) swaps internally.
/// This avoids mutex poisoning, spinlocks, global lock contention,
/// and external dependencies.
///
/// The per-value allocation cost is negligible in FFI contexts where
/// the future itself is already heap-allocated via [`Box::pin`](alloc::boxed::Box::pin).
pub struct EffectSlot<E, R> {
    pending_effect: AtomicSlot<E>,
    response: AtomicSlot<R>,
}

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

impl<E: Clone, R> EffectSlot<E, R> {
    /// Create an empty effect slot.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            pending_effect: AtomicSlot::new(),
            response: AtomicSlot::new(),
        }
    }

    /// Called by async code to request an effect.
    ///
    /// On the first call (no response available), the effect is posted and
    /// [`Poll::Pending`] is returned. Once the host has called
    /// [`fulfill`](Self::fulfill), the next call returns
    /// [`Poll::Ready(response)`](Poll::Ready).
    ///
    /// The effect is cloned into the slot, so `poll_fn` can safely call
    /// this multiple times with the same effect reference.
    pub fn request(&self, effect: &E) -> Poll<R> {
        if let Some(resp) = self.response.take() {
            return Poll::Ready(resp);
        }

        self.pending_effect.put(effect.clone());
        Poll::Pending
    }

    /// Called by the FFI bridge to read what the async code needs.
    ///
    /// Returns `Some(effect)` if async code has posted an effect, or
    /// `None` if no effect is pending. Takes the effect, clearing the slot.
    pub fn take_effect(&self) -> Option<E> {
        self.pending_effect.take()
    }

    /// Called by the FFI bridge to provide the host's response.
    ///
    /// The response will be returned to async code on the next
    /// [`request`](Self::request) call.
    pub fn fulfill(&self, response: R) {
        self.response.put(response);
    }
}

impl<E: Clone, R> Default for EffectSlot<E, R> {
    fn default() -> Self {
        Self::new()
    }
}