Skip to main content

almost_enough/
boxed.rs

1//! Boxed dynamic dispatch for Stop.
2//!
3//! This module provides [`BoxedStop`], a heap-allocated wrapper that enables
4//! dynamic dispatch without monomorphization bloat.
5//!
6//! # When to Use
7//!
8//! Generic functions like `fn process(stop: impl Stop)` are monomorphized
9//! for each concrete type, increasing binary size. `BoxedStop` provides a
10//! single concrete type for dynamic dispatch:
11//!
12//! ```rust
13//! use almost_enough::{BoxedStop, Stop};
14//!
15//! // Monomorphized for each Stop type - increases binary size
16//! fn process_generic(stop: impl Stop) {
17//!     // ...
18//! }
19//!
20//! // Single implementation - no monomorphization bloat
21//! fn process_boxed(stop: BoxedStop) {
22//!     // ...
23//! }
24//! ```
25//!
26//! # Alternatives
27//!
28//! For borrowed dynamic dispatch with zero allocation, use `&dyn Stop`:
29//!
30//! ```rust
31//! use almost_enough::{StopSource, Stop};
32//!
33//! fn process(stop: &dyn Stop) {
34//!     if stop.should_stop() {
35//!         return;
36//!     }
37//!     // ...
38//! }
39//!
40//! let source = StopSource::new();
41//! process(&source);
42//! ```
43
44use alloc::boxed::Box;
45
46use crate::{Stop, StopReason};
47
48/// A heap-allocated [`Stop`] implementation.
49///
50/// This type provides dynamic dispatch for `Stop`, avoiding monomorphization
51/// bloat when you don't need the performance of generics.
52///
53/// # Example
54///
55/// ```rust
56/// use almost_enough::{BoxedStop, StopSource, Stopper, Unstoppable, Stop};
57///
58/// fn process(stop: BoxedStop) {
59///     for i in 0..1000 {
60///         if i % 100 == 0 && stop.should_stop() {
61///             return;
62///         }
63///         // process...
64///     }
65/// }
66///
67/// // Works with any Stop implementation
68/// process(BoxedStop::new(Unstoppable));
69/// process(BoxedStop::new(StopSource::new()));
70/// process(BoxedStop::new(Stopper::new()));
71/// ```
72pub struct BoxedStop(Box<dyn Stop + Send + Sync>);
73
74impl BoxedStop {
75    /// Create a new boxed stop from any [`Stop`] implementation.
76    #[inline]
77    pub fn new<T: Stop + 'static>(stop: T) -> Self {
78        Self(Box::new(stop))
79    }
80
81    /// Returns the effective inner stop if it may stop, collapsing indirection.
82    ///
83    /// The returned `&dyn Stop` points directly to the concrete type inside
84    /// the box, bypassing the `BoxedStop` wrapper. In a hot loop, subsequent
85    /// `check()` calls go through one vtable dispatch instead of two.
86    ///
87    /// Returns `None` if the inner stop is a no-op (e.g., `Unstoppable`).
88    ///
89    /// # Example
90    ///
91    /// ```rust
92    /// use almost_enough::{BoxedStop, Stopper, Unstoppable, Stop, StopReason};
93    ///
94    /// fn hot_loop(stop: &BoxedStop) -> Result<(), StopReason> {
95    ///     let stop = stop.active_stop(); // Option<&dyn Stop>, collapsed
96    ///     for i in 0..1000 {
97    ///         stop.check()?;
98    ///     }
99    ///     Ok(())
100    /// }
101    ///
102    /// // Unstoppable: returns None, check() is always Ok(())
103    /// assert!(hot_loop(&BoxedStop::new(Unstoppable)).is_ok());
104    ///
105    /// // Stopper: returns Some(&Stopper), one vtable dispatch per check()
106    /// assert!(hot_loop(&BoxedStop::new(Stopper::new())).is_ok());
107    /// ```
108    #[inline]
109    pub fn active_stop(&self) -> Option<&dyn Stop> {
110        let inner: &dyn Stop = &*self.0;
111        if inner.may_stop() { Some(inner) } else { None }
112    }
113}
114
115impl Stop for BoxedStop {
116    #[inline]
117    fn check(&self) -> Result<(), StopReason> {
118        self.0.check()
119    }
120
121    #[inline]
122    fn should_stop(&self) -> bool {
123        self.0.should_stop()
124    }
125
126    #[inline]
127    fn may_stop(&self) -> bool {
128        self.0.may_stop()
129    }
130}
131
132impl core::fmt::Debug for BoxedStop {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134        f.debug_tuple("BoxedStop").finish()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::{StopSource, Stopper, Unstoppable};
142
143    #[test]
144    fn boxed_stop_from_unstoppable() {
145        let stop = BoxedStop::new(Unstoppable);
146        assert!(!stop.should_stop());
147        assert!(stop.check().is_ok());
148    }
149
150    #[test]
151    fn boxed_stop_from_stopper() {
152        let stopper = Stopper::new();
153        let stop = BoxedStop::new(stopper.clone());
154
155        assert!(!stop.should_stop());
156
157        stopper.cancel();
158
159        assert!(stop.should_stop());
160        assert_eq!(stop.check(), Err(StopReason::Cancelled));
161    }
162
163    #[test]
164    fn boxed_stop_is_send_sync() {
165        fn assert_send_sync<T: Send + Sync>() {}
166        assert_send_sync::<BoxedStop>();
167    }
168
169    #[test]
170    fn boxed_stop_debug() {
171        let stop = BoxedStop::new(Unstoppable);
172        let debug = alloc::format!("{:?}", stop);
173        assert!(debug.contains("BoxedStop"));
174    }
175
176    #[test]
177    fn boxed_stop_avoids_monomorphization() {
178        // This function has a single concrete implementation
179        fn process(stop: BoxedStop) -> bool {
180            stop.should_stop()
181        }
182
183        // All these use the same process function
184        assert!(!process(BoxedStop::new(Unstoppable)));
185        assert!(!process(BoxedStop::new(StopSource::new())));
186        assert!(!process(BoxedStop::new(Stopper::new())));
187    }
188
189    #[test]
190    fn may_stop_delegates_through_boxed() {
191        assert!(!BoxedStop::new(Unstoppable).may_stop());
192        assert!(BoxedStop::new(Stopper::new()).may_stop());
193    }
194
195    #[test]
196    fn active_stop_collapses_unstoppable() {
197        let stop = BoxedStop::new(Unstoppable);
198        assert!(stop.active_stop().is_none());
199    }
200
201    #[test]
202    fn active_stop_collapses_nested() {
203        let inner = BoxedStop::new(Unstoppable);
204        let outer = BoxedStop::new(inner);
205        assert!(outer.active_stop().is_none());
206    }
207
208    #[test]
209    fn active_stop_returns_inner_for_stopper() {
210        let stopper = Stopper::new();
211        let stop = BoxedStop::new(stopper.clone());
212
213        let active = stop.active_stop();
214        assert!(active.is_some());
215        assert!(active.unwrap().check().is_ok());
216
217        stopper.cancel();
218        assert!(active.unwrap().should_stop());
219    }
220
221    #[test]
222    fn active_stop_hot_loop_pattern() {
223        let stop = BoxedStop::new(Unstoppable);
224        let active = stop.active_stop();
225        for _ in 0..1000 {
226            assert!(active.check().is_ok());
227        }
228    }
229}