async_safe_defer/
lib.rs

1//! This library provides two implementations of RAII-style deferred execution:
2//! one using dynamic allocation (the default) and one that avoids allocation
3//! entirely (`no_alloc`), with a fixed-capacity array of deferred function pointers.
4
5#![cfg_attr(not(test), no_std)]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8extern crate alloc;
9use alloc::boxed::Box;
10use alloc::vec::Vec;
11use core::future::Future;
12use core::pin::Pin;
13
14/// RAII-style guard for executing a closure at the end of a scope.
15#[must_use = "Defer must be stored in a variable to execute the closure"]
16pub fn defer<F>(f: F) -> impl Drop
17where
18    F: FnOnce(),
19{
20    struct Defer<F: FnOnce()> {
21        f: Option<F>,
22    }
23
24    impl<F: FnOnce()> Drop for Defer<F> {
25        fn drop(&mut self) {
26            if let Some(f) = self.f.take() {
27                f();
28            }
29        }
30    }
31
32    Defer { f: Some(f) }
33}
34
35/// Macro for creating a synchronous defer guard.
36#[macro_export]
37macro_rules! defer {
38    ($e:expr) => {
39        let _guard = $crate::defer(|| $e);
40        let _ = &_guard;
41    };
42}
43
44/// An async-aware scope guard that stores deferred async closures (heap-based).
45pub struct AsyncScope {
46    defer: Vec<Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + 'static>> + 'static>>,
47}
48
49impl AsyncScope {
50    /// Creates a new `AsyncScope` for collecting async deferred tasks.
51    pub fn new() -> Self {
52        AsyncScope { defer: Vec::new() }
53    }
54
55    /// Registers an async closure to be executed later (LIFO).
56    pub fn defer<F>(&mut self, f: F)
57    where
58        F: FnOnce() -> Pin<Box<dyn Future<Output = ()> + 'static>> + 'static,
59    {
60        self.defer.push(Box::new(move || Box::pin(f())));
61    }
62
63    /// Runs all stored async tasks in reverse order.
64    pub async fn run(mut self) {
65        while let Some(f) = self.defer.pop() {
66            f().await;
67        }
68    }
69}
70
71/// Macro that creates an async scope to automatically await all defers.
72#[macro_export]
73macro_rules! async_scope {
74    ($scope:ident, $body:block) => {
75        async {
76            let mut $scope = $crate::AsyncScope::new();
77            $body
78            $scope.run().await;
79        }
80    };
81}
82
83/// A module for a no-alloc, fixed-capacity async scope.
84///
85/// Only compiled if `feature = "no_alloc"` is enabled or when tests run.
86#[cfg_attr(docsrs, doc(cfg(feature = "no_alloc")))]
87#[cfg(any(feature = "no_alloc", test, docsrs))]
88pub mod no_alloc {
89    use alloc::boxed::Box;
90    use core::{future::Future, pin::Pin};
91
92    /// Type alias for a `'static` function pointer returning a pinned async future.
93    pub type DeferredFn = fn() -> Pin<Box<dyn Future<Output = ()> + 'static>>;
94
95    /// A fixed-capacity async scope that does not use dynamic allocation.
96    pub struct AsyncScopeNoAlloc<const N: usize> {
97        tasks: [Option<DeferredFn>; N],
98        len: usize,
99    }
100
101    impl<const N: usize> AsyncScopeNoAlloc<N> {
102        /// Creates a new `AsyncScopeNoAlloc` with capacity `N`.
103        pub const fn new() -> Self {
104            Self {
105                tasks: [None; N],
106                len: 0,
107            }
108        }
109
110        /// Registers a `'static` function pointer to be called later.
111        ///
112        /// Panics if capacity is exceeded.
113        pub fn defer(&mut self, f: DeferredFn) {
114            if self.len >= N {
115                panic!("No space left for more tasks.");
116            }
117            self.tasks[self.len] = Some(f);
118            self.len += 1;
119        }
120
121        /// Executes all tasks in reverse order, awaiting each one.
122        pub async fn run(&mut self) {
123            while self.len > 0 {
124                self.len -= 1;
125                let task = self.tasks[self.len].take().unwrap();
126                (task)().await;
127            }
128        }
129    }
130
131    /// Macro to create a no-alloc async scope with fixed capacity.
132    #[cfg_attr(docsrs, doc(cfg(feature = "no_alloc")))]
133    #[macro_export]
134    macro_rules! no_alloc_async_scope {
135        ($scope:ident : $cap:expr, $body:block) => {
136            async {
137                let mut $scope = $crate::no_alloc::AsyncScopeNoAlloc::<$cap>::new();
138                $body
139                $scope.run().await;
140            }
141        };
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    extern crate std;
148    use self::std::sync::{
149        Arc, Mutex,
150        atomic::{AtomicUsize, Ordering},
151    };
152    use super::*;
153
154    #[test]
155    fn test_sync_defer() {
156        println!("test_sync_defer start");
157        let val = Arc::new(AtomicUsize::new(0));
158        {
159            println!("in scope, val={}", val.load(Ordering::SeqCst));
160            let v = val.clone();
161            defer!(v.store(42, Ordering::SeqCst));
162        }
163        println!("out of scope, val={}", val.load(Ordering::SeqCst));
164        assert_eq!(val.load(Ordering::SeqCst), 42);
165    }
166
167    #[tokio::test]
168    async fn test_async_scope_order() {
169        println!("test_async_scope_order start");
170        let log = Arc::new(Mutex::new(Vec::new()));
171        {
172            let mut scope = AsyncScope::new();
173
174            let l1 = log.clone();
175            scope.defer(move || {
176                println!("push(1) scheduled");
177                let l1 = l1.clone();
178                Box::pin(async move {
179                    println!("push(1) running");
180                    l1.lock().unwrap().push(1);
181                })
182            });
183
184            let l2 = log.clone();
185            scope.defer(move || {
186                println!("push(2) scheduled");
187                let l2 = l2.clone();
188                Box::pin(async move {
189                    println!("push(2) running");
190                    l2.lock().unwrap().push(2);
191                })
192            });
193
194            scope.run().await;
195        }
196        let result = log.lock().unwrap().clone();
197        println!("final log: {:?}", result);
198        assert_eq!(result, vec![2, 1]);
199    }
200
201    #[tokio::test]
202    async fn test_async_scope_macro() {
203        println!("test_async_scope_macro start");
204        use crate::async_scope;
205        let flag = Arc::new(AtomicUsize::new(0));
206        {
207            let f = Arc::clone(&flag);
208            async_scope!(scope, {
209                let f2 = Arc::clone(&f);
210                scope.defer(move || {
211                    println!("store(1) scheduled");
212                    Box::pin(async move {
213                        println!("store(1) running");
214                        f2.store(1, Ordering::SeqCst);
215                    })
216                });
217                println!("in scope, flag={}", f.load(Ordering::SeqCst));
218            })
219            .await;
220        }
221        println!("out of scope, flag={}", flag.load(Ordering::SeqCst));
222        assert_eq!(flag.load(Ordering::SeqCst), 1);
223    }
224
225    #[cfg(feature = "no_alloc")]
226    #[tokio::test]
227    async fn test_no_alloc_scope() {
228        println!("test_no_alloc_scope start");
229        use super::no_alloc::{AsyncScopeNoAlloc, DeferredFn};
230        use core::future::Future;
231        use core::pin::Pin;
232
233        fn task_one() -> Pin<Box<dyn Future<Output = ()> + 'static>> {
234            Box::pin(async {
235                println!("task_one running");
236            })
237        }
238        fn task_two() -> Pin<Box<dyn Future<Output = ()> + 'static>> {
239            Box::pin(async {
240                println!("task_two running");
241            })
242        }
243
244        let mut scope = AsyncScopeNoAlloc::<2>::new();
245        scope.defer(task_one as DeferredFn);
246        scope.defer(task_two as DeferredFn);
247        scope.run().await;
248    }
249}