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//! **Prefer [`StopToken`](crate::StopToken)** which is `Clone` (via `Arc`).
9//! `BoxedStop` is retained for cases where unique ownership is required.
10//!
11//! Generic functions like `fn process(stop: impl Stop)` are monomorphized
12//! for each concrete type, increasing binary size. `BoxedStop` provides a
13//! single concrete type for dynamic dispatch:
14//!
15//! ```rust
16//! use almost_enough::{BoxedStop, Stop};
17//!
18//! // Single implementation - no monomorphization bloat
19//! fn process_boxed(stop: BoxedStop) {
20//! // ...
21//! }
22//! ```
23//!
24//! # Alternatives
25//!
26//! For borrowed dynamic dispatch with zero allocation, use `&dyn Stop`:
27//!
28//! ```rust
29//! use almost_enough::{StopSource, Stop};
30//!
31//! fn process(stop: &dyn Stop) {
32//! if stop.should_stop() {
33//! return;
34//! }
35//! // ...
36//! }
37//!
38//! let source = StopSource::new();
39//! process(&source);
40//! ```
41
42use alloc::boxed::Box;
43
44use crate::{Stop, StopReason};
45
46/// A heap-allocated [`Stop`] implementation.
47///
48/// **Prefer [`StopToken`](crate::StopToken)** which is `Clone` (via `Arc`) and
49/// supports indirection collapsing. `BoxedStop` is retained for cases where
50/// unique ownership is required.
51///
52/// No-op stops (like `Unstoppable`) are optimized away at construction —
53/// `check()` short-circuits without any vtable dispatch.
54///
55/// # Example
56///
57/// ```rust
58/// use almost_enough::{BoxedStop, StopSource, Stopper, Unstoppable, Stop};
59///
60/// fn process(stop: BoxedStop) {
61/// for i in 0..1000 {
62/// if i % 100 == 0 && stop.should_stop() {
63/// return;
64/// }
65/// // process...
66/// }
67/// }
68///
69/// // Works with any Stop implementation
70/// process(BoxedStop::new(Unstoppable));
71/// process(BoxedStop::new(StopSource::new()));
72/// process(BoxedStop::new(Stopper::new()));
73/// ```
74pub struct BoxedStop(Option<Box<dyn Stop + Send + Sync>>);
75
76impl BoxedStop {
77 /// Create a new boxed stop from any [`Stop`] implementation.
78 ///
79 /// No-op stops (where `may_stop()` returns false) are not allocated —
80 /// `check()` will short-circuit to `Ok(())`.
81 #[inline]
82 pub fn new<T: Stop + 'static>(stop: T) -> Self {
83 if !stop.may_stop() {
84 return Self(None);
85 }
86 Self(Some(Box::new(stop)))
87 }
88}
89
90impl Stop for BoxedStop {
91 #[inline]
92 fn check(&self) -> Result<(), StopReason> {
93 match &self.0 {
94 Some(inner) => inner.check(),
95 None => Ok(()),
96 }
97 }
98
99 #[inline]
100 fn should_stop(&self) -> bool {
101 match &self.0 {
102 Some(inner) => inner.should_stop(),
103 None => false,
104 }
105 }
106
107 #[inline]
108 fn may_stop(&self) -> bool {
109 self.0.is_some()
110 }
111}
112
113impl core::fmt::Debug for BoxedStop {
114 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115 f.debug_tuple("BoxedStop").finish()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::{StopSource, Stopper, Unstoppable};
123
124 #[test]
125 fn boxed_stop_from_unstoppable() {
126 let stop = BoxedStop::new(Unstoppable);
127 assert!(!stop.should_stop());
128 assert!(stop.check().is_ok());
129 assert!(!stop.may_stop());
130 }
131
132 #[test]
133 fn boxed_stop_from_stopper() {
134 let stopper = Stopper::new();
135 let stop = BoxedStop::new(stopper.clone());
136
137 assert!(!stop.should_stop());
138
139 stopper.cancel();
140
141 assert!(stop.should_stop());
142 assert_eq!(stop.check(), Err(StopReason::Cancelled));
143 }
144
145 #[test]
146 fn boxed_stop_is_send_sync() {
147 fn assert_send_sync<T: Send + Sync>() {}
148 assert_send_sync::<BoxedStop>();
149 }
150
151 #[test]
152 fn boxed_stop_debug() {
153 let stop = BoxedStop::new(Unstoppable);
154 let debug = alloc::format!("{:?}", stop);
155 assert!(debug.contains("BoxedStop"));
156 }
157
158 #[test]
159 fn boxed_stop_avoids_monomorphization() {
160 fn process(stop: BoxedStop) -> bool {
161 stop.should_stop()
162 }
163
164 assert!(!process(BoxedStop::new(Unstoppable)));
165 assert!(!process(BoxedStop::new(StopSource::new())));
166 assert!(!process(BoxedStop::new(Stopper::new())));
167 }
168
169 #[test]
170 fn may_stop_delegates_through_boxed() {
171 assert!(!BoxedStop::new(Unstoppable).may_stop());
172 assert!(BoxedStop::new(Stopper::new()).may_stop());
173 }
174
175 #[test]
176 fn unstoppable_no_allocation() {
177 // Unstoppable wraps to None — no heap allocation
178 let stop = BoxedStop::new(Unstoppable);
179 assert!(!stop.may_stop());
180 assert!(stop.check().is_ok());
181 }
182}