Skip to main content

ntex_rt/
task.rs

1use std::{future::Future, sync::Arc, sync::atomic::AtomicUsize, sync::atomic::Ordering};
2
3// The Callbacks static holds a pointer to the global callbacks. It is protected by
4// the STATE static which determines whether `CBS` has been initialized yet.
5static mut CBS: Option<Arc<dyn CallbacksApi>> = None;
6
7static STATE: AtomicUsize = AtomicUsize::new(0);
8
9// There are three different states that we care about: the callback's
10// uninitialized, the callback's initializing (set_cbs's been called but
11// CBS hasn't actually been set yet), or the callbacks's active.
12const UNINITIALIZED: usize = 0;
13const INITIALIZING: usize = 1;
14const INITIALIZED: usize = 2;
15
16trait CallbacksApi {
17    fn before(&self) -> Option<*const ()>;
18    fn enter(&self, _: *const ()) -> *const ();
19    fn exit(&self, _: *const ());
20    fn after(&self, _: *const ());
21}
22
23#[allow(clippy::struct_field_names)]
24struct Callbacks<A, B, C, D> {
25    f_before: A,
26    f_enter: B,
27    f_exit: C,
28    f_after: D,
29}
30
31impl<A, B, C, D> CallbacksApi for Callbacks<A, B, C, D>
32where
33    A: Fn() -> Option<*const ()> + 'static,
34    B: Fn(*const ()) -> *const () + 'static,
35    C: Fn(*const ()) + 'static,
36    D: Fn(*const ()) + 'static,
37{
38    fn before(&self) -> Option<*const ()> {
39        (self.f_before)()
40    }
41    fn enter(&self, d: *const ()) -> *const () {
42        (self.f_enter)(d)
43    }
44    fn exit(&self, d: *const ()) {
45        (self.f_exit)(d);
46    }
47    fn after(&self, d: *const ()) {
48        (self.f_after)(d);
49    }
50}
51
52pub(crate) struct Data {
53    cb: &'static dyn CallbacksApi,
54    ptr: *const (),
55}
56
57impl Data {
58    #[allow(clippy::if_not_else)]
59    pub(crate) fn load() -> Option<Data> {
60        // `Acquire` pairs with the `Release` store in `set_cbs`, so `CBS` is visible
61        let cb = if STATE.load(Ordering::Acquire) != INITIALIZED {
62            None
63        } else {
64            #[allow(static_mut_refs)]
65            unsafe {
66                Some(CBS.as_ref().map(AsRef::as_ref).unwrap())
67            }
68        };
69
70        if let Some(cb) = cb
71            && let Some(ptr) = cb.before()
72        {
73            return Some(Data { cb, ptr });
74        }
75        None
76    }
77
78    pub(crate) fn run<F, R>(&mut self, f: F) -> R
79    where
80        F: FnOnce() -> R,
81    {
82        struct Exit(&'static dyn CallbacksApi, *const ());
83
84        impl Drop for Exit {
85            fn drop(&mut self) {
86                self.0.exit(self.1);
87            }
88        }
89
90        // `exit` also runs if `f` panics, so the context set by `enter` is restored
91        let _exit = Exit(self.cb, self.cb.enter(self.ptr));
92        f()
93    }
94}
95
96/// Wraps a future so that task callbacks, if registered, run around each poll.
97///
98/// Always returns the same future type, so spawning code is generated once
99/// per future rather than once per callback mode.
100pub(crate) fn wrap<F: Future>(fut: F) -> impl Future<Output = F::Output> {
101    let mut data = Data::load();
102    async move {
103        let mut f = std::pin::pin!(fut);
104        std::future::poll_fn(|cx| match data.as_mut() {
105            Some(data) => data.run(|| f.as_mut().poll(cx)),
106            None => f.as_mut().poll(cx),
107        })
108        .await
109    }
110}
111
112impl Drop for Data {
113    fn drop(&mut self) {
114        self.cb.after(self.ptr);
115    }
116}
117
118/// # Safety
119///
120/// The user must ensure that the pointer returned by `before` has a `'static` lifetime.
121/// This pointer will be owned by the spawned task for the duration of that task, and
122/// ownership will be returned to the user at the end of the task via `after`.
123/// The pointer remains opaque to the runtime.
124///
125/// Does nothing if task callbacks have already been set, use
126/// [`task_opt_callbacks`] to check whether they were set.
127pub unsafe fn task_callbacks<FBefore, FEnter, FExit, FAfter>(
128    f_before: FBefore,
129    f_enter: FEnter,
130    f_exit: FExit,
131    f_after: FAfter,
132) where
133    FBefore: Fn() -> Option<*const ()> + 'static + Sync,
134    FEnter: Fn(*const ()) -> *const () + 'static + Sync,
135    FExit: Fn(*const ()) + 'static + Sync,
136    FAfter: Fn(*const ()) + 'static + Sync,
137{
138    let new = Arc::new(Callbacks {
139        f_before,
140        f_enter,
141        f_exit,
142        f_after,
143    });
144    let _ = set_cbs(new);
145}
146
147/// # Safety
148///
149/// The user must ensure that the pointer returned by `before` has a `'static` lifetime.
150/// This pointer will be owned by the spawned task for the duration of that task, and
151/// ownership will be returned to the user at the end of the task via `after`.
152/// The pointer remains opaque to the runtime.
153///
154/// Returns false if task callbacks have already been set.
155pub unsafe fn task_opt_callbacks<FBefore, FEnter, FExit, FAfter>(
156    f_before: FBefore,
157    f_enter: FEnter,
158    f_exit: FExit,
159    f_after: FAfter,
160) -> bool
161where
162    FBefore: Fn() -> Option<*const ()> + Sync + 'static,
163    FEnter: Fn(*const ()) -> *const () + Sync + 'static,
164    FExit: Fn(*const ()) + Sync + 'static,
165    FAfter: Fn(*const ()) + Sync + 'static,
166{
167    let new = Arc::new(Callbacks {
168        f_before,
169        f_enter,
170        f_exit,
171        f_after,
172    });
173    set_cbs(new).is_ok()
174}
175
176fn set_cbs(cbs: Arc<dyn CallbacksApi>) -> Result<(), ()> {
177    match STATE.compare_exchange(
178        UNINITIALIZED,
179        INITIALIZING,
180        Ordering::Acquire,
181        Ordering::Relaxed,
182    ) {
183        Ok(UNINITIALIZED) => {
184            unsafe {
185                CBS = Some(cbs);
186            }
187            STATE.store(INITIALIZED, Ordering::Release);
188            Ok(())
189        }
190        Err(INITIALIZING) => {
191            while STATE.load(Ordering::Relaxed) == INITIALIZING {
192                std::hint::spin_loop();
193            }
194            Err(())
195        }
196        _ => Err(()),
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn load_callbacks_set_by_other_thread() {
206        let hnd = std::thread::spawn(|| unsafe {
207            task_opt_callbacks(|| Some(std::ptr::null()), |p| p, |_| {}, |_| {})
208        });
209        // observes callbacks set by another thread without synchronization
210        while Data::load().is_none() {
211            std::hint::spin_loop();
212        }
213        assert!(hnd.join().unwrap());
214    }
215
216    #[test]
217    fn exit_runs_when_poll_panics() {
218        use std::{cell::Cell, panic, pin::pin, task::Context, task::Waker};
219
220        // callbacks can be set once per process, so check them in a child process
221        const CHILD: &str = "NTEX_RT_TASK_EXIT_CHILD";
222        if std::env::var_os(CHILD).is_none() {
223            let out = std::process::Command::new(std::env::current_exe().unwrap())
224                .args(["--exact", "task::tests::exit_runs_when_poll_panics"])
225                .env(CHILD, "1")
226                .output()
227                .unwrap();
228            assert!(
229                out.status.success(),
230                "{}",
231                String::from_utf8_lossy(&out.stdout)
232            );
233            return;
234        }
235
236        thread_local! {
237            static DEPTH: Cell<isize> = const { Cell::new(0) };
238            static AFTER: Cell<usize> = const { Cell::new(0) };
239        }
240        assert!(unsafe {
241            task_opt_callbacks(
242                || Some(std::ptr::null()),
243                |p| {
244                    DEPTH.set(DEPTH.get() + 1);
245                    p
246                },
247                |_| DEPTH.set(DEPTH.get() - 1),
248                |_| AFTER.set(AFTER.get() + 1),
249            )
250        });
251
252        let mut fut = Box::pin(wrap(async { panic!("poll panic") }));
253        let res = panic::catch_unwind(panic::AssertUnwindSafe(|| {
254            let _ = fut.as_mut().poll(&mut Context::from_waker(Waker::noop()));
255        }));
256        assert!(res.is_err());
257        assert_eq!(DEPTH.get(), 0);
258        drop(fut);
259        assert_eq!(AFTER.get(), 1);
260
261        let mut fut = pin!(wrap(async { 1 }));
262        let res = fut.as_mut().poll(&mut Context::from_waker(Waker::noop()));
263        assert_eq!(res, std::task::Poll::Ready(1));
264        assert_eq!(DEPTH.get(), 0);
265    }
266
267    #[test]
268    fn callbacks_set_once_and_wrap_spawned_tasks() {
269        use std::{cell::Cell, sync::atomic::AtomicUsize};
270
271        use crate::{System, testing::TestRunner};
272
273        const CHILD: &str = "NTEX_RT_TASK_CALLBACKS_CHILD";
274        static DATA: u8 = 0;
275        static CTX: u8 = 0;
276        static BEFORE: AtomicUsize = AtomicUsize::new(0);
277        static AFTER: AtomicUsize = AtomicUsize::new(0);
278        thread_local! {
279            static TRACK: Cell<bool> = const { Cell::new(false) };
280            static INSIDE: Cell<bool> = const { Cell::new(false) };
281        }
282        fn data() -> *const () {
283            (&raw const DATA).cast()
284        }
285        fn ctx() -> *const () {
286            (&raw const CTX).cast()
287        }
288
289        // callbacks can be set once per process, so check them in a child process
290        if std::env::var_os(CHILD).is_none() {
291            let out = std::process::Command::new(std::env::current_exe().unwrap())
292                .args([
293                    "--exact",
294                    "task::tests::callbacks_set_once_and_wrap_spawned_tasks",
295                ])
296                .env(CHILD, "1")
297                .output()
298                .unwrap();
299            assert!(
300                out.status.success(),
301                "{}",
302                String::from_utf8_lossy(&out.stdout)
303            );
304            return;
305        }
306
307        unsafe {
308            task_callbacks(
309                || {
310                    TRACK.get().then(|| {
311                        BEFORE.fetch_add(1, Ordering::Relaxed);
312                        data()
313                    })
314                },
315                |p| {
316                    assert_eq!(p, data());
317                    INSIDE.set(true);
318                    ctx()
319                },
320                |p| {
321                    // receives the value returned by `enter`
322                    assert_eq!(p, ctx());
323                    INSIDE.set(false);
324                },
325                |p| {
326                    assert_eq!(p, data());
327                    AFTER.fetch_add(1, Ordering::Relaxed);
328                },
329            );
330        }
331        // callbacks are set once, later registrations are ignored
332        unsafe {
333            task_callbacks(
334                || unreachable!(),
335                |_| unreachable!(),
336                |_| unreachable!(),
337                |_| unreachable!(),
338            );
339            assert!(!task_opt_callbacks(
340                || unreachable!(),
341                |_| unreachable!(),
342                |_| unreachable!(),
343                |_| unreachable!(),
344            ));
345        }
346
347        System::new("callbacks", TestRunner).block_on(async {
348            TRACK.set(true);
349            let hnd = crate::spawn(async { INSIDE.get() });
350            TRACK.set(false);
351            assert_eq!(BEFORE.load(Ordering::Relaxed), 1);
352            // polled with the context set by `enter`
353            assert!(hnd.await.unwrap());
354            assert!(!INSIDE.get());
355            assert_eq!(AFTER.load(Ordering::Relaxed), 1);
356
357            // `before` returned `None`, the task runs without callbacks
358            assert!(!crate::spawn(async { INSIDE.get() }).await.unwrap());
359            assert_eq!(BEFORE.load(Ordering::Relaxed), 1);
360            assert_eq!(AFTER.load(Ordering::Relaxed), 1);
361        });
362    }
363}