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}