Skip to main content

libasync/
on_interrupt.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::callback_state::CallbackSharedState;
6use core::fmt;
7use core::pin::Pin;
8use core::ptr::NonNull;
9use core::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering};
10use core::task::{Context, Poll};
11use futures::Stream;
12use futures::task::AtomicWaker;
13use libasync_dispatcher::{DetectDispatcher, GetAsyncDispatcher};
14use libasync_sys::{
15    async_bind_irq, async_dispatcher_t, async_irq, async_irq_t, async_state_t, async_unbind_irq,
16};
17use std::sync::Arc;
18use zx::sys::{ZX_ERR_CANCELED, ZX_OK, zx_packet_interrupt_t, zx_status_t};
19use zx::{
20    AsHandleRef, BootTimeline, Instant, Interrupt, InterruptKind, RealInterruptKind, Status,
21    Timeline,
22};
23
24/// Internal state managed for an active IRQ binding.
25struct IrqState {
26    async_dispatcher: NonNull<async_dispatcher_t>,
27    waker: AtomicWaker,
28    status: AtomicI32,
29    timestamp: AtomicI64,
30    raw_ptr_released: AtomicBool,
31}
32
33// SAFETY: async_dispatcher_t is thread-safe per libasync API specification.
34unsafe impl Send for IrqState {}
35unsafe impl Sync for IrqState {}
36
37type SharedState = CallbackSharedState<async_irq, IrqState>;
38
39impl IrqState {
40    unsafe extern "C" fn call(
41        dispatcher: *mut async_dispatcher_t,
42        irq: *mut async_irq_t,
43        status: zx_status_t,
44        signal: *const zx_packet_interrupt_t,
45    ) {
46        // SAFETY: irq points to the async_irq at offset 0 of CallbackSharedState.
47        // Increment strong count for the duration of this call to ensure the shared
48        // state remains valid even if unbind runs concurrently on another thread.
49        unsafe { Arc::increment_strong_count(irq as *const SharedState) };
50        let state = unsafe { Arc::from_raw(irq as *const SharedState) };
51
52        debug_assert!(
53            dispatcher == state.async_dispatcher.as_ptr(),
54            "dispatcher pointer mismatch in irq callback"
55        );
56
57        if status == ZX_OK {
58            debug_assert!(!signal.is_null(), "signal must not be null when status is ZX_OK");
59            // SAFETY: signal is non-null and valid when status is ZX_OK per async_irq_handler_t contract.
60            let ts = unsafe { (*signal).timestamp };
61            state.timestamp.store(ts, Ordering::Relaxed);
62            state.status.store(ZX_OK, Ordering::Release);
63            state.waker.wake();
64        } else if status == ZX_ERR_CANCELED {
65            state.status.store(status, Ordering::Release);
66            state.waker.wake();
67
68            if state
69                .raw_ptr_released
70                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
71                .is_ok()
72            {
73                // SAFETY: The dispatcher will never invoke this callback again after ZX_ERR_CANCELED.
74                unsafe { SharedState::release_raw_ptr(irq) };
75            }
76        }
77    }
78}
79
80/// A stream that yields notifications whenever a Zircon interrupt fires on a libasync dispatcher.
81pub struct OnInterrupt<K: InterruptKind = RealInterruptKind, T: Timeline = BootTimeline> {
82    dispatcher: DetectDispatcher,
83    interrupt: Option<Interrupt<K, T>>,
84    state: Option<Arc<SharedState>>,
85}
86
87impl<K: InterruptKind, T: Timeline> OnInterrupt<K, T> {
88    /// Creates a new `OnInterrupt` stream bound to the current thread's active dispatcher.
89    pub fn new(interrupt: Interrupt<K, T>) -> Self {
90        Self { dispatcher: DetectDispatcher::default(), interrupt: Some(interrupt), state: None }
91    }
92
93    /// Creates a new `OnInterrupt` stream bound to the specified dispatcher.
94    pub fn new_on(dispatcher: impl GetAsyncDispatcher, interrupt: Interrupt<K, T>) -> Self {
95        Self {
96            dispatcher: DetectDispatcher::new(dispatcher.get_async_dispatcher()),
97            interrupt: Some(interrupt),
98            state: None,
99        }
100    }
101
102    /// Acknowledges the interrupt so that it can be triggered again.
103    ///
104    /// In Zircon, interrupts remain masked after firing until `ack()` is invoked.
105    pub fn ack(&self) -> Result<(), Status> {
106        self.interrupt.as_ref().ok_or(Status::BAD_STATE)?.ack()
107    }
108
109    /// Returns a reference to the underlying `Interrupt`, or `None` if it was taken.
110    pub fn interrupt(&self) -> Option<&Interrupt<K, T>> {
111        self.interrupt.as_ref()
112    }
113
114    /// Cancels interrupt listening and returns the underlying `Interrupt` object.
115    pub fn take_interrupt(&mut self) -> Option<Interrupt<K, T>> {
116        self.unbind();
117        self.interrupt.take()
118    }
119
120    fn bind(&mut self) -> Result<(), Status> {
121        let interrupt = self.interrupt.as_ref().ok_or(Status::BAD_STATE)?;
122        let dispatcher = self.dispatcher.get_or_detect()?;
123        let async_dispatcher = dispatcher.as_ptr();
124
125        let base = async_irq {
126            state: async_state_t::default(),
127            handler: Some(IrqState::call),
128            object: interrupt.raw_handle(),
129        };
130
131        let inner = IrqState {
132            async_dispatcher,
133            waker: AtomicWaker::new(),
134            status: AtomicI32::new(Status::SHOULD_WAIT.into_raw()),
135            timestamp: AtomicI64::new(0),
136            raw_ptr_released: AtomicBool::new(false),
137        };
138
139        let shared_state = SharedState::new(base, inner);
140        let raw_ptr = SharedState::make_raw_ptr(shared_state.clone());
141        // SAFETY: async_bind_irq is thread safe per libasync C API doc.
142        let status = unsafe { async_bind_irq(async_dispatcher.as_ptr(), raw_ptr) };
143        if let Err(err) = Status::ok(status) {
144            // SAFETY: Binding failed; callback will never run. Decrement raw ref.
145            unsafe { SharedState::release_raw_ptr(raw_ptr) };
146            return Err(err);
147        }
148
149        self.state = Some(shared_state);
150        Ok(())
151    }
152
153    fn unbind(&mut self) {
154        let Some(state) = self.state.take() else {
155            return;
156        };
157
158        let raw_ptr = SharedState::as_raw_ptr(&state);
159        // SAFETY: async_unbind_irq is thread-safe per libasync C API doc.
160        let status = unsafe { async_unbind_irq(state.async_dispatcher.as_ptr(), raw_ptr) };
161
162        if Status::ok(status).is_ok()
163            && state
164                .raw_ptr_released
165                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
166                .is_ok()
167        {
168            // SAFETY: Successfully unbound. C dispatcher will never invoke the callback again.
169            unsafe { SharedState::release_raw_ptr(raw_ptr) };
170        }
171        // If status == ZX_ERR_BAD_STATE, dispatcher is shutting down; the callback will receive
172        // ZX_ERR_CANCELED and decref there, or raw_ptr_released deduplicates if it already ran.
173    }
174}
175
176impl<K: InterruptKind, T: Timeline> Stream for OnInterrupt<K, T> {
177    type Item = Result<Instant<T>, Status>;
178
179    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
180        let this = self.get_mut();
181
182        if this.interrupt.is_none() {
183            return Poll::Ready(None);
184        }
185
186        if this.state.is_none()
187            && let Err(err) = this.bind()
188        {
189            return Poll::Ready(Some(Err(err)));
190        }
191
192        let state = this.state.as_ref().unwrap();
193        state.waker.register(cx.waker());
194
195        match state.status.load(Ordering::Acquire) {
196            ZX_OK => {
197                // Reset status to SHOULD_WAIT for the next event.
198                state.status.store(Status::SHOULD_WAIT.into_raw(), Ordering::Release);
199                let ts = state.timestamp.load(Ordering::Relaxed);
200                Poll::Ready(Some(Ok(Instant::from_nanos(ts))))
201            }
202            ZX_ERR_CANCELED => {
203                this.unbind();
204                this.interrupt = None;
205                Poll::Ready(Some(Err(Status::CANCELED)))
206            }
207            s if s == Status::SHOULD_WAIT.into_raw() => Poll::Pending,
208            s => Poll::Ready(Some(Err(Status::err_from_raw(s)))),
209        }
210    }
211}
212
213impl<K: InterruptKind, T: Timeline> Drop for OnInterrupt<K, T> {
214    fn drop(&mut self) {
215        self.unbind();
216    }
217}
218
219impl<K: InterruptKind, T: Timeline> AsHandleRef for OnInterrupt<K, T> {
220    fn as_handle_ref(&self) -> zx::HandleRef<'_> {
221        self.as_ref().as_handle_ref()
222    }
223}
224
225impl<K: InterruptKind, T: Timeline> AsRef<Interrupt<K, T>> for OnInterrupt<K, T> {
226    fn as_ref(&self) -> &Interrupt<K, T> {
227        self.interrupt.as_ref().expect("OnInterrupt dereferenced after interrupt taken")
228    }
229}
230
231impl<K: InterruptKind, T: Timeline> Unpin for OnInterrupt<K, T> {}
232
233impl<K: InterruptKind, T: Timeline> fmt::Debug for OnInterrupt<K, T> {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        f.debug_struct("OnInterrupt")
236            .field("interrupt", &self.interrupt.as_ref().map(|i| i.as_handle_ref()))
237            .finish()
238    }
239}
240
241/// Extension trait adding interrupt listening capabilities to types providing an async dispatcher.
242pub trait DispatcherInterruptExt {
243    /// Returns an `OnInterrupt` stream bound to this dispatcher.
244    fn on_interrupt<K: InterruptKind, T: Timeline>(
245        &self,
246        interrupt: Interrupt<K, T>,
247    ) -> OnInterrupt<K, T>;
248
249    /// Returns an `OnInterrupt` stream bound to this dispatcher, or `None` if no dispatcher is present.
250    fn try_on_interrupt<K: InterruptKind, T: Timeline>(
251        &self,
252        interrupt: Interrupt<K, T>,
253    ) -> Option<OnInterrupt<K, T>>;
254}
255
256impl<D: GetAsyncDispatcher> DispatcherInterruptExt for D {
257    fn on_interrupt<K: InterruptKind, T: Timeline>(
258        &self,
259        interrupt: Interrupt<K, T>,
260    ) -> OnInterrupt<K, T> {
261        self.try_on_interrupt(interrupt).expect("No current dispatcher")
262    }
263
264    fn try_on_interrupt<K: InterruptKind, T: Timeline>(
265        &self,
266        interrupt: Interrupt<K, T>,
267    ) -> Option<OnInterrupt<K, T>> {
268        let dispatcher = self.try_get_async_dispatcher()?;
269        Some(OnInterrupt::new_on(dispatcher, interrupt))
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use fdf_env::test::spawn_in_driver;
277    use futures::{StreamExt, poll};
278    use libasync_dispatcher::CurrentDispatcher;
279    use std::sync::mpsc;
280    use std::task::Waker;
281    use std::thread::sleep;
282    use std::time::Duration;
283    use zx::{
284        BootInstant, MonotonicInstant, MonotonicTimeline, VirtualInterrupt, VirtualInterruptKind,
285    };
286
287    #[test]
288    fn test_bind_and_receive_virtual_interrupt() {
289        spawn_in_driver("testing irq wait", async move {
290            let interrupt = VirtualInterrupt::create_virtual().unwrap();
291            let mut stream = CurrentDispatcher.on_interrupt(interrupt);
292            assert_eq!(poll!(stream.next()), Poll::Pending);
293
294            let trigger_time = BootInstant::from_nanos(12345);
295            stream.interrupt().unwrap().trigger(trigger_time).unwrap();
296
297            let res = stream.next().await;
298            assert_eq!(res, Some(Ok(trigger_time)));
299
300            stream.ack().unwrap();
301        });
302    }
303
304    #[test]
305    fn test_multiple_interrupts_sequential() {
306        spawn_in_driver("testing irq multi-shot", async move {
307            let interrupt = VirtualInterrupt::create_virtual().unwrap();
308            let mut stream = CurrentDispatcher.on_interrupt(interrupt);
309            assert_eq!(poll!(stream.next()), Poll::Pending);
310
311            for i in 1..=5 {
312                let trigger_time = BootInstant::from_nanos(i * 1000);
313                stream.interrupt().unwrap().trigger(trigger_time).unwrap();
314
315                let res = stream.next().await;
316                assert_eq!(res, Some(Ok(trigger_time)));
317
318                stream.ack().unwrap();
319            }
320        });
321    }
322
323    #[test]
324    fn test_take_interrupt() {
325        spawn_in_driver("testing take_interrupt", async move {
326            let interrupt = VirtualInterrupt::create_virtual().unwrap();
327            let mut stream = CurrentDispatcher.on_interrupt(interrupt);
328            assert_eq!(poll!(stream.next()), Poll::Pending);
329
330            let reclaimed_irq = stream.take_interrupt().unwrap();
331            assert_eq!(poll!(stream.next()), Poll::Ready(None));
332
333            let trigger_time = BootInstant::from_nanos(54321);
334            reclaimed_irq.trigger(trigger_time).unwrap();
335            assert_eq!(reclaimed_irq.wait().unwrap(), trigger_time);
336        });
337    }
338
339    #[test]
340    fn test_drop_while_bound() {
341        spawn_in_driver("testing drop while bound", async move {
342            let interrupt = VirtualInterrupt::create_virtual().unwrap();
343            let mut stream = CurrentDispatcher.on_interrupt(interrupt);
344            assert_eq!(poll!(stream.next()), Poll::Pending);
345            drop(stream);
346        });
347    }
348
349    #[test]
350    fn test_monotonic_timeline_interrupt() {
351        spawn_in_driver("testing monotonic timeline irq", async move {
352            let interrupt =
353                Interrupt::<VirtualInterruptKind, MonotonicTimeline>::create_virtual().unwrap();
354            let mut stream = CurrentDispatcher.on_interrupt(interrupt);
355            assert_eq!(poll!(stream.next()), Poll::Pending);
356
357            let trigger_time = MonotonicInstant::from_nanos(99999);
358            stream.interrupt().unwrap().trigger(trigger_time).unwrap();
359
360            let res = stream.next().await;
361            assert_eq!(res, Some(Ok(trigger_time)));
362
363            stream.ack().unwrap();
364        });
365    }
366
367    #[test]
368    fn test_dispatcher_shutdown_cancel() {
369        let (stream_tx, stream_rx) = mpsc::channel();
370        spawn_in_driver("testing irq shutdown", async move {
371            let interrupt = VirtualInterrupt::create_virtual().unwrap();
372            let mut stream = CurrentDispatcher.on_interrupt(interrupt);
373            assert_eq!(poll!(stream.next()), Poll::Pending);
374            stream_tx.send(stream).unwrap();
375        });
376
377        let mut stream = stream_rx.recv().unwrap();
378        let waker = Waker::noop();
379        let mut context = Context::from_waker(waker);
380        loop {
381            let Poll::Ready(res) = stream.poll_next_unpin(&mut context) else {
382                sleep(Duration::from_millis(10));
383                continue;
384            };
385            assert_eq!(res, Some(Err(Status::CANCELED)));
386            break;
387        }
388
389        // Subsequent poll should yield None
390        assert_eq!(stream.poll_next_unpin(&mut context), Poll::Ready(None));
391    }
392}