Skip to main content

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}