almost_enough/source.rs
1//! Zero-allocation cancellation primitives.
2//!
3//! This module provides stack-based cancellation using borrowed references.
4//! Works in `no_std` environments without an allocator.
5//!
6//! # Overview
7//!
8//! - [`StopSource`] - A cancellation source that owns an `AtomicBool` on the stack
9//! - [`StopRef`] - A borrowed reference to check cancellation
10//!
11//! # Example
12//!
13//! ```rust
14//! use almost_enough::{StopSource, Stop};
15//!
16//! let source = StopSource::new();
17//! let stop = source.as_ref();
18//!
19//! assert!(!stop.should_stop());
20//!
21//! source.cancel();
22//! assert!(stop.should_stop());
23//! ```
24//!
25//! # When to Use
26//!
27//! Use `StopSource`/`StopRef` when:
28//! - You need zero-allocation cancellation
29//! - The source outlives all references (stack-based usage)
30//! - You're in a `no_std` environment
31//!
32//! Use [`Stopper`](crate::Stopper) when:
33//! - You need to share ownership (clone instead of borrow)
34//! - You want to pass stops across thread boundaries without lifetimes
35
36use core::sync::atomic::{AtomicBool, Ordering};
37
38use crate::{Stop, StopReason};
39
40/// A stack-based cancellation source.
41///
42/// This is a zero-allocation cancellation primitive. The source owns the
43/// atomic and can issue borrowed references via [`as_ref()`](Self::as_ref).
44///
45/// # Example
46///
47/// ```rust
48/// use almost_enough::{StopSource, Stop};
49///
50/// let source = StopSource::new();
51/// let stop = source.as_ref();
52///
53/// // Check in your operation
54/// assert!(!stop.should_stop());
55///
56/// // Cancel when needed
57/// source.cancel();
58/// assert!(stop.should_stop());
59/// ```
60///
61/// # Const Construction
62///
63/// `StopSource` can be created in const context:
64///
65/// ```rust
66/// use almost_enough::StopSource;
67///
68/// static GLOBAL_STOP: StopSource = StopSource::new();
69/// ```
70#[derive(Debug)]
71pub struct StopSource {
72 cancelled: AtomicBool,
73}
74
75impl StopSource {
76 /// Create a new cancellation source.
77 #[inline]
78 pub const fn new() -> Self {
79 Self {
80 cancelled: AtomicBool::new(false),
81 }
82 }
83
84 /// Create a source that is already cancelled.
85 ///
86 /// Useful for testing or when you want to signal immediate stop.
87 #[inline]
88 pub const fn cancelled() -> Self {
89 Self {
90 cancelled: AtomicBool::new(true),
91 }
92 }
93
94 /// Signal all references to stop.
95 ///
96 /// A Release swap: writes made before a `cancel()` are visible to a
97 /// thread whose [`is_cancelled`](Self::is_cancelled) returns `true` after
98 /// it, even when several threads cancel. On targets without atomic swap
99 /// (`thumbv6m`, say) it is a Release store, which guarantees that only
100 /// for a single canceller. Calling it again changes nothing else.
101 #[inline]
102 pub fn cancel(&self) {
103 // A swap: see `Stopper::cancel`.
104 #[cfg(target_has_atomic = "8")]
105 self.cancelled.swap(true, Ordering::Release);
106 #[cfg(not(target_has_atomic = "8"))]
107 self.cancelled.store(true, Ordering::Release);
108 }
109
110 /// Whether this source has been cancelled, as an Acquire load.
111 ///
112 /// When it returns `true`, this thread also sees every write made before
113 /// [`cancel`](Self::cancel). Checks through a [`StopRef`] are Relaxed
114 /// loads, which promise nothing about other writes.
115 #[inline]
116 pub fn is_cancelled(&self) -> bool {
117 self.cancelled.load(Ordering::Acquire)
118 }
119
120 /// Get a borrowed reference to pass to operations.
121 ///
122 /// The reference borrows from this source, so it cannot outlive it.
123 /// For owned stops, use [`Stopper`](crate::Stopper).
124 #[inline]
125 pub fn as_ref(&self) -> StopRef<'_> {
126 StopRef {
127 cancelled: &self.cancelled,
128 }
129 }
130
131 /// Alias for [`as_ref()`](Self::as_ref) for migration from AtomicStop.
132 #[inline]
133 #[deprecated(since = "0.1.0", note = "use as_ref() instead")]
134 pub fn token(&self) -> StopRef<'_> {
135 self.as_ref()
136 }
137}
138
139impl Default for StopSource {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl Stop for StopSource {
146 #[inline]
147 #[track_caller]
148 fn check(&self) -> Result<(), StopReason> {
149 if self.cancelled.load(Ordering::Relaxed) {
150 Err(StopReason::Cancelled)
151 } else {
152 Ok(())
153 }
154 }
155
156 #[inline]
157 #[track_caller]
158 fn should_stop(&self) -> bool {
159 self.cancelled.load(Ordering::Relaxed)
160 }
161}
162
163/// A borrowed reference to a [`StopSource`].
164///
165/// This is a lightweight reference that can only check for cancellation -
166/// it cannot trigger it. Use the source to cancel.
167///
168/// # Example
169///
170/// ```rust
171/// use almost_enough::{StopSource, Stop};
172///
173/// fn process(data: &[u8], stop: impl Stop) {
174/// for (i, chunk) in data.chunks(100).enumerate() {
175/// if i % 10 == 0 && stop.should_stop() {
176/// return;
177/// }
178/// // process chunk...
179/// }
180/// }
181///
182/// let source = StopSource::new();
183/// process(&[0u8; 1000], source.as_ref());
184/// ```
185///
186/// # Copy Semantics
187///
188/// `StopRef` is `Copy`, so you can freely copy it without cloning:
189///
190/// ```rust
191/// use almost_enough::{StopSource, Stop};
192///
193/// let source = StopSource::new();
194/// let r1 = source.as_ref();
195/// let r2 = r1; // Copy
196/// let r3 = r1; // Still valid
197///
198/// source.cancel();
199/// assert!(r1.should_stop());
200/// assert!(r2.should_stop());
201/// assert!(r3.should_stop());
202/// ```
203#[derive(Debug, Clone, Copy)]
204pub struct StopRef<'a> {
205 cancelled: &'a AtomicBool,
206}
207
208impl Stop for StopRef<'_> {
209 #[inline]
210 #[track_caller]
211 fn check(&self) -> Result<(), StopReason> {
212 if self.cancelled.load(Ordering::Relaxed) {
213 Err(StopReason::Cancelled)
214 } else {
215 Ok(())
216 }
217 }
218
219 #[inline]
220 #[track_caller]
221 fn should_stop(&self) -> bool {
222 self.cancelled.load(Ordering::Relaxed)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 /// See `stopper::tests`: a `StopRef`'s checks are Relaxed, the source's
231 /// `is_cancelled()` is the Acquire query.
232 #[cfg(feature = "std")]
233 #[test]
234 fn is_cancelled_after_the_stop_sees_writes_made_before_cancel() {
235 use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
236 for _ in 0..if cfg!(miri) { 30 } else { 1_000 } {
237 let source = StopSource::new();
238 let data = AtomicU32::new(0);
239 std::thread::scope(|scope| {
240 let reader = scope.spawn(|| {
241 let stop = source.as_ref();
242 while !stop.should_stop() {
243 core::hint::spin_loop();
244 }
245 assert!(source.is_cancelled());
246 data.load(Relaxed)
247 });
248 data.store(42, Relaxed);
249 source.cancel();
250 assert_eq!(reader.join().unwrap(), 42);
251 });
252 }
253 }
254
255 #[cfg(feature = "std")]
256 #[test]
257 fn is_cancelled_after_a_second_cancel_sees_the_first_cancellers_writes() {
258 use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
259 for _ in 0..if cfg!(miri) { 30 } else { 1_000 } {
260 let source = StopSource::new();
261 let data = AtomicU32::new(0);
262 std::thread::scope(|scope| {
263 let reader = scope.spawn(|| {
264 while !source.should_stop() {
265 core::hint::spin_loop();
266 }
267 assert!(source.is_cancelled());
268 data.load(Relaxed)
269 });
270 scope.spawn(|| {
271 while !source.should_stop() {
272 core::hint::spin_loop();
273 }
274 source.cancel();
275 });
276 data.store(42, Relaxed);
277 source.cancel();
278 assert_eq!(reader.join().unwrap(), 42);
279 });
280 }
281 }
282
283 #[test]
284 fn stop_source_basic() {
285 let source = StopSource::new();
286 assert!(!source.is_cancelled());
287 assert!(!source.should_stop());
288 assert!(source.check().is_ok());
289
290 source.cancel();
291
292 assert!(source.is_cancelled());
293 assert!(source.should_stop());
294 assert_eq!(source.check(), Err(StopReason::Cancelled));
295 }
296
297 #[test]
298 fn stop_source_cancelled_constructor() {
299 let source = StopSource::cancelled();
300 assert!(source.is_cancelled());
301 assert!(source.should_stop());
302 }
303
304 #[test]
305 fn stop_ref_basic() {
306 let source = StopSource::new();
307 let stop = source.as_ref();
308
309 assert!(!stop.should_stop());
310 assert!(stop.check().is_ok());
311
312 source.cancel();
313
314 assert!(stop.should_stop());
315 assert_eq!(stop.check(), Err(StopReason::Cancelled));
316 }
317
318 #[test]
319 fn stop_ref_is_copy() {
320 let source = StopSource::new();
321 let r1 = source.as_ref();
322 let r2 = r1; // Copy
323 let _ = r1; // Still valid
324 let _ = r2;
325 }
326
327 #[test]
328 fn stop_source_is_default() {
329 let source: StopSource = Default::default();
330 assert!(!source.is_cancelled());
331 }
332
333 #[test]
334 fn stop_source_is_send_sync() {
335 fn assert_send_sync<T: Send + Sync>() {}
336 assert_send_sync::<StopSource>();
337 assert_send_sync::<StopRef<'_>>();
338 }
339
340 #[test]
341 fn cancel_is_idempotent() {
342 let source = StopSource::new();
343 source.cancel();
344 source.cancel();
345 source.cancel();
346 assert!(source.is_cancelled());
347 }
348
349 #[test]
350 fn const_construction() {
351 static SOURCE: StopSource = StopSource::new();
352 assert!(!SOURCE.is_cancelled());
353 }
354}