veridian_kernel/irq/mod.rs
1//! Architecture-independent IRQ abstraction layer
2//!
3//! Provides a generic interface for interrupt management that delegates to
4//! per-architecture interrupt controllers:
5//! - x86_64: Local APIC + I/O APIC
6//! - AArch64: GICv2 (Generic Interrupt Controller)
7//! - RISC-V: PLIC (Platform-Level Interrupt Controller)
8//!
9//! This module implements the IRQ object abstraction (H-003) from the
10//! remediation backlog, providing a unified API for registering handlers,
11//! enabling/disabling IRQ lines, and dispatching interrupts.
12
13// IRQ management
14
15#[cfg(feature = "alloc")]
16extern crate alloc;
17
18#[cfg(feature = "alloc")]
19use alloc::collections::BTreeMap;
20
21use spin::Mutex;
22
23use crate::{
24 error::{KernelError, KernelResult},
25 sync::once_lock::GlobalState,
26};
27
28// ---------------------------------------------------------------------------
29// IRQ number newtype
30// ---------------------------------------------------------------------------
31
32/// Architecture-independent IRQ number.
33///
34/// Wraps a `u32` to provide type safety and prevent accidental misuse of
35/// raw integer values as IRQ numbers.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct IrqNumber(pub u32);
38
39impl IrqNumber {
40 /// Create a new IRQ number.
41 pub const fn new(irq: u32) -> Self {
42 Self(irq)
43 }
44
45 /// Get the raw IRQ number as a `u32`.
46 #[inline]
47 pub fn as_u32(self) -> u32 {
48 self.0
49 }
50}
51
52impl From<u32> for IrqNumber {
53 fn from(value: u32) -> Self {
54 Self(value)
55 }
56}
57
58impl From<IrqNumber> for u32 {
59 fn from(irq: IrqNumber) -> u32 {
60 irq.0
61 }
62}
63
64impl core::fmt::Display for IrqNumber {
65 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66 write!(f, "IRQ#{}", self.0)
67 }
68}
69
70// ---------------------------------------------------------------------------
71// IRQ handler type
72// ---------------------------------------------------------------------------
73
74/// Type alias for IRQ handler functions.
75///
76/// An IRQ handler is a function that takes the IRQ number that triggered the
77/// interrupt. Handlers are registered via [`register_handler`] and invoked
78/// by [`dispatch`] when the corresponding interrupt fires.
79pub type IrqHandler = fn(IrqNumber);
80
81// ---------------------------------------------------------------------------
82// IRQ controller trait
83// ---------------------------------------------------------------------------
84
85/// Architecture-independent interrupt controller interface.
86///
87/// Each architecture implements this trait for its hardware interrupt
88/// controller (APIC, GIC, PLIC). The [`IrqManager`] delegates hardware
89/// operations through this trait.
90pub trait IrqController {
91 /// Enable an interrupt line so it can be delivered to the CPU.
92 fn enable(&self, irq: IrqNumber) -> KernelResult<()>;
93
94 /// Disable an interrupt line to prevent delivery.
95 fn disable(&self, irq: IrqNumber) -> KernelResult<()>;
96
97 /// Acknowledge receipt of an interrupt.
98 ///
99 /// On some architectures this is a separate step from EOI.
100 fn acknowledge(&self, irq: IrqNumber) -> KernelResult<()>;
101
102 /// Signal end-of-interrupt to the controller.
103 ///
104 /// Must be called after the interrupt handler has finished processing.
105 fn eoi(&self, irq: IrqNumber) -> KernelResult<()>;
106
107 /// Set the priority of an interrupt line.
108 ///
109 /// The meaning of `priority` is architecture-dependent:
110 /// - x86_64 APIC: Not directly supported per-IRQ (no-op)
111 /// - AArch64 GIC: 0x00 = highest, 0xFF = lowest
112 /// - RISC-V PLIC: 0 = disabled, 1-7 = priority levels
113 fn set_priority(&self, irq: IrqNumber, priority: u8) -> KernelResult<()>;
114
115 /// Check whether an interrupt is pending.
116 fn is_pending(&self, irq: IrqNumber) -> KernelResult<bool>;
117}
118
119// ---------------------------------------------------------------------------
120// IRQ manager
121// ---------------------------------------------------------------------------
122
123/// Maximum number of IRQ lines supported.
124///
125/// This covers the common range across all supported architectures:
126/// - x86_64 I/O APIC: typically 24 lines
127/// - AArch64 GIC: up to 1020 interrupt IDs
128/// - RISC-V PLIC: up to 127 sources
129const MAX_IRQ: u32 = 256;
130
131/// Central IRQ manager that maintains registered handlers and delegates
132/// hardware operations to the architecture-specific controller.
133///
134/// The manager stores a mapping from IRQ numbers to handler functions.
135/// When an interrupt fires, the architecture-specific entry point calls
136/// [`dispatch`] which looks up and invokes the registered handler.
137pub struct IrqManager {
138 /// Registered IRQ handlers, keyed by raw IRQ number.
139 #[cfg(feature = "alloc")]
140 handlers: BTreeMap<u32, IrqHandler>,
141
142 /// Number of interrupts dispatched (for statistics).
143 dispatch_count: u64,
144}
145
146impl IrqManager {
147 /// Create a new IRQ manager with no registered handlers.
148 fn new() -> Self {
149 Self {
150 #[cfg(feature = "alloc")]
151 handlers: BTreeMap::new(),
152 dispatch_count: 0,
153 }
154 }
155
156 /// Register a handler for the given IRQ number.
157 ///
158 /// Returns an error if a handler is already registered for this IRQ.
159 #[cfg(feature = "alloc")]
160 fn register(&mut self, irq: IrqNumber, handler: IrqHandler) -> KernelResult<()> {
161 if irq.0 >= MAX_IRQ {
162 return Err(KernelError::InvalidArgument {
163 name: "irq",
164 value: "IRQ number exceeds maximum",
165 });
166 }
167
168 if self.handlers.contains_key(&irq.0) {
169 return Err(KernelError::AlreadyExists {
170 resource: "IRQ handler",
171 id: irq.0 as u64,
172 });
173 }
174
175 self.handlers.insert(irq.0, handler);
176 Ok(())
177 }
178
179 /// Unregister the handler for the given IRQ number.
180 ///
181 /// Returns an error if no handler is registered for this IRQ.
182 #[cfg(feature = "alloc")]
183 fn unregister(&mut self, irq: IrqNumber) -> KernelResult<()> {
184 if self.handlers.remove(&irq.0).is_none() {
185 return Err(KernelError::NotFound {
186 resource: "IRQ handler",
187 id: irq.0 as u64,
188 });
189 }
190 Ok(())
191 }
192
193 /// Dispatch an interrupt to the registered handler.
194 ///
195 /// If no handler is registered for the given IRQ, this is a no-op
196 /// (spurious interrupts are silently ignored).
197 #[cfg(feature = "alloc")]
198 fn dispatch(&mut self, irq: IrqNumber) {
199 self.dispatch_count += 1;
200 if let Some(&handler) = self.handlers.get(&irq.0) {
201 handler(irq);
202 }
203 }
204
205 /// Get the number of dispatched interrupts.
206 fn dispatch_count(&self) -> u64 {
207 self.dispatch_count
208 }
209}
210
211// ---------------------------------------------------------------------------
212// Global state
213// ---------------------------------------------------------------------------
214
215/// Global IRQ manager instance, protected by a spin::Mutex.
216///
217/// Initialized once by [`init`]. Uses the GlobalState pattern for safe
218/// global access without `static mut`.
219static IRQ_MANAGER: GlobalState<Mutex<IrqManager>> = GlobalState::new();
220
221// ---------------------------------------------------------------------------
222// Architecture-specific delegation
223// ---------------------------------------------------------------------------
224
225/// Enable an IRQ line on the architecture-specific interrupt controller.
226#[cfg(target_arch = "x86_64")]
227fn arch_enable_irq(irq: u32) -> KernelResult<()> {
228 crate::arch::x86_64::apic::unmask_irq(irq as u8)
229}
230
231/// Enable an IRQ line on the architecture-specific interrupt controller.
232#[cfg(target_arch = "aarch64")]
233fn arch_enable_irq(irq: u32) -> KernelResult<()> {
234 crate::arch::aarch64::gic::enable_irq(irq)
235}
236
237/// Enable an IRQ line on the architecture-specific interrupt controller.
238#[cfg(target_arch = "riscv64")]
239fn arch_enable_irq(irq: u32) -> KernelResult<()> {
240 crate::arch::riscv::plic::enable(irq)
241}
242
243/// Disable an IRQ line on the architecture-specific interrupt controller.
244#[cfg(target_arch = "x86_64")]
245fn arch_disable_irq(irq: u32) -> KernelResult<()> {
246 crate::arch::x86_64::apic::mask_irq(irq as u8)
247}
248
249/// Disable an IRQ line on the architecture-specific interrupt controller.
250#[cfg(target_arch = "aarch64")]
251fn arch_disable_irq(irq: u32) -> KernelResult<()> {
252 crate::arch::aarch64::gic::disable_irq(irq)
253}
254
255/// Disable an IRQ line on the architecture-specific interrupt controller.
256#[cfg(target_arch = "riscv64")]
257fn arch_disable_irq(irq: u32) -> KernelResult<()> {
258 crate::arch::riscv::plic::disable(irq)
259}
260
261/// Send end-of-interrupt to the architecture-specific controller.
262#[cfg(target_arch = "x86_64")]
263fn arch_eoi(_irq: u32) -> KernelResult<()> {
264 crate::arch::x86_64::apic::send_eoi();
265 Ok(())
266}
267
268/// Send end-of-interrupt to the architecture-specific controller.
269#[cfg(target_arch = "aarch64")]
270fn arch_eoi(irq: u32) -> KernelResult<()> {
271 crate::arch::aarch64::gic::eoi(irq);
272 Ok(())
273}
274
275/// Send end-of-interrupt to the architecture-specific controller.
276#[cfg(target_arch = "riscv64")]
277fn arch_eoi(irq: u32) -> KernelResult<()> {
278 crate::arch::riscv::plic::complete(irq)
279}
280
281/// Set IRQ priority on the architecture-specific controller.
282#[cfg(target_arch = "x86_64")]
283fn arch_set_priority(_irq: u32, _priority: u8) -> KernelResult<()> {
284 // x86_64 I/O APIC does not support per-IRQ priority in the same way;
285 // priority is managed via the Task Priority Register. This is a no-op.
286 Ok(())
287}
288
289/// Set IRQ priority on the architecture-specific controller.
290#[cfg(target_arch = "aarch64")]
291fn arch_set_priority(irq: u32, priority: u8) -> KernelResult<()> {
292 crate::arch::aarch64::gic::set_irq_priority(irq, priority)
293}
294
295/// Set IRQ priority on the architecture-specific controller.
296#[cfg(target_arch = "riscv64")]
297fn arch_set_priority(irq: u32, priority: u8) -> KernelResult<()> {
298 crate::arch::riscv::plic::set_priority(irq, priority as u32)
299}
300
301/// Check if an IRQ is pending on the architecture-specific controller.
302#[cfg(target_arch = "x86_64")]
303fn arch_is_pending(_irq: u32) -> KernelResult<bool> {
304 // x86_64: Checking individual IRQ pending status requires reading the
305 // IRR (Interrupt Request Register) from the Local APIC, which is not
306 // yet exposed. Return false as a safe default.
307 Ok(false)
308}
309
310/// Check if an IRQ is pending on the architecture-specific controller.
311#[cfg(target_arch = "aarch64")]
312fn arch_is_pending(_irq: u32) -> KernelResult<bool> {
313 // AArch64 GIC: Checking individual pending bits via GICD_ISPENDR is
314 // not yet exposed in the public GIC API. Return false as a safe default.
315 Ok(false)
316}
317
318/// Check if an IRQ is pending on the architecture-specific controller.
319#[cfg(target_arch = "riscv64")]
320fn arch_is_pending(irq: u32) -> KernelResult<bool> {
321 crate::arch::riscv::plic::is_pending(irq)
322}
323
324// ---------------------------------------------------------------------------
325// Public API
326// ---------------------------------------------------------------------------
327
328/// Initialize the IRQ manager subsystem.
329///
330/// Creates the global IRQ manager instance. Must be called after the
331/// architecture-specific interrupt controller has been initialized
332/// (APIC, GIC, or PLIC).
333///
334/// # Errors
335///
336/// Returns `KernelError::AlreadyExists` if the IRQ manager has already
337/// been initialized.
338pub fn init() -> KernelResult<()> {
339 IRQ_MANAGER
340 .init(Mutex::new(IrqManager::new()))
341 .map_err(|_| KernelError::AlreadyExists {
342 resource: "IRQ manager",
343 id: 0,
344 })?;
345
346 kprintln!("[IRQ] IRQ manager initialized");
347 Ok(())
348}
349
350/// Register an IRQ handler for the given interrupt number.
351///
352/// The handler will be invoked when the interrupt fires and [`dispatch`]
353/// is called. Only one handler may be registered per IRQ number.
354///
355/// # Errors
356///
357/// - `KernelError::NotInitialized` if the IRQ manager has not been initialized.
358/// - `KernelError::AlreadyExists` if a handler is already registered for this
359/// IRQ.
360/// - `KernelError::InvalidArgument` if the IRQ number exceeds the maximum.
361#[cfg(feature = "alloc")]
362pub fn register_handler(irq: IrqNumber, handler: IrqHandler) -> KernelResult<()> {
363 IRQ_MANAGER
364 .with_mut(|mtx| {
365 let mut mgr = mtx.lock();
366 mgr.register(irq, handler)
367 })
368 .unwrap_or(Err(KernelError::NotInitialized {
369 subsystem: "IRQ manager",
370 }))
371}
372
373/// Unregister the IRQ handler for the given interrupt number.
374///
375/// # Errors
376///
377/// - `KernelError::NotInitialized` if the IRQ manager has not been initialized.
378/// - `KernelError::NotFound` if no handler is registered for this IRQ.
379#[cfg(feature = "alloc")]
380pub fn unregister_handler(irq: IrqNumber) -> KernelResult<()> {
381 IRQ_MANAGER
382 .with_mut(|mtx| {
383 let mut mgr = mtx.lock();
384 mgr.unregister(irq)
385 })
386 .unwrap_or(Err(KernelError::NotInitialized {
387 subsystem: "IRQ manager",
388 }))
389}
390
391/// Dispatch an interrupt to the registered handler.
392///
393/// Called by the architecture-specific interrupt entry point when an
394/// external interrupt is received. Looks up the handler for the given
395/// IRQ number and invokes it. If no handler is registered, the interrupt
396/// is silently ignored (spurious).
397#[cfg(feature = "alloc")]
398pub fn dispatch(irq: IrqNumber) {
399 IRQ_MANAGER.with_mut(|mtx| {
400 let mut mgr = mtx.lock();
401 mgr.dispatch(irq);
402 });
403}
404
405/// Enable an IRQ line on the hardware interrupt controller.
406///
407/// Delegates to the architecture-specific controller:
408/// - x86_64: unmasks the IRQ in the I/O APIC
409/// - AArch64: enables the interrupt in the GIC distributor
410/// - RISC-V: enables the interrupt source in the PLIC
411///
412/// # Errors
413///
414/// - `KernelError::NotInitialized` if the interrupt controller has not been
415/// initialized.
416pub fn enable_irq(irq: IrqNumber) -> KernelResult<()> {
417 arch_enable_irq(irq.0)
418}
419
420/// Disable an IRQ line on the hardware interrupt controller.
421///
422/// Delegates to the architecture-specific controller:
423/// - x86_64: masks the IRQ in the I/O APIC
424/// - AArch64: disables the interrupt in the GIC distributor
425/// - RISC-V: disables the interrupt source in the PLIC
426///
427/// # Errors
428///
429/// - `KernelError::NotInitialized` if the interrupt controller has not been
430/// initialized.
431pub fn disable_irq(irq: IrqNumber) -> KernelResult<()> {
432 arch_disable_irq(irq.0)
433}
434
435/// Send end-of-interrupt to the hardware controller.
436///
437/// Must be called after the interrupt handler has finished processing.
438///
439/// # Errors
440///
441/// - `KernelError::NotInitialized` if the interrupt controller has not been
442/// initialized.
443pub fn eoi(irq: IrqNumber) -> KernelResult<()> {
444 arch_eoi(irq.0)
445}
446
447/// Set the priority of an IRQ line.
448///
449/// The interpretation of `priority` is architecture-dependent. See
450/// [`IrqController::set_priority`] for details.
451///
452/// # Errors
453///
454/// - `KernelError::NotInitialized` if the interrupt controller has not been
455/// initialized.
456/// - `KernelError::InvalidArgument` if the priority value is out of range.
457pub fn set_priority(irq: IrqNumber, priority: u8) -> KernelResult<()> {
458 arch_set_priority(irq.0, priority)
459}
460
461/// Check whether an IRQ is pending.
462///
463/// # Errors
464///
465/// - `KernelError::NotInitialized` if the interrupt controller has not been
466/// initialized.
467pub fn is_pending(irq: IrqNumber) -> KernelResult<bool> {
468 arch_is_pending(irq.0)
469}
470
471/// Get the number of interrupts dispatched since initialization.
472pub fn dispatch_count() -> u64 {
473 IRQ_MANAGER
474 .with(|mtx| {
475 let mgr = mtx.lock();
476 mgr.dispatch_count()
477 })
478 .unwrap_or(0)
479}
480
481// ---------------------------------------------------------------------------
482// Tests
483// ---------------------------------------------------------------------------
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use crate::error::KernelError;
489
490 // -- IrqNumber tests --
491
492 #[test]
493 fn irq_number_new_and_as_u32() {
494 let irq = IrqNumber::new(42);
495 assert_eq!(irq.as_u32(), 42);
496 }
497
498 #[test]
499 fn irq_number_from_u32() {
500 let irq = IrqNumber::from(7u32);
501 assert_eq!(irq.0, 7);
502 }
503
504 #[test]
505 fn irq_number_into_u32() {
506 let irq = IrqNumber::new(99);
507 let raw: u32 = irq.into();
508 assert_eq!(raw, 99);
509 }
510
511 #[test]
512 fn irq_number_display() {
513 use core::fmt::Write;
514 let irq = IrqNumber::new(13);
515 let mut buf = alloc::string::String::new();
516 write!(buf, "{}", irq).unwrap();
517 assert_eq!(buf, "IRQ#13");
518 }
519
520 #[test]
521 fn irq_number_equality() {
522 assert_eq!(IrqNumber::new(5), IrqNumber::new(5));
523 assert_ne!(IrqNumber::new(5), IrqNumber::new(6));
524 }
525
526 #[test]
527 fn irq_number_ordering() {
528 assert!(IrqNumber::new(1) < IrqNumber::new(2));
529 assert!(IrqNumber::new(255) > IrqNumber::new(0));
530 }
531
532 #[test]
533 fn irq_number_clone_copy() {
534 let a = IrqNumber::new(10);
535 let b = a; // Copy
536 let c = a.clone();
537 assert_eq!(a, b);
538 assert_eq!(a, c);
539 }
540
541 // -- IrqManager tests --
542
543 #[test]
544 fn manager_new_has_zero_dispatch_count() {
545 let mgr = IrqManager::new();
546 assert_eq!(mgr.dispatch_count(), 0);
547 }
548
549 #[test]
550 fn manager_register_handler_success() {
551 let mut mgr = IrqManager::new();
552 fn dummy(_irq: IrqNumber) {}
553 let result = mgr.register(IrqNumber::new(1), dummy);
554 assert!(result.is_ok());
555 }
556
557 #[test]
558 fn manager_register_duplicate_returns_error() {
559 let mut mgr = IrqManager::new();
560 fn dummy(_irq: IrqNumber) {}
561 mgr.register(IrqNumber::new(5), dummy).unwrap();
562 let result = mgr.register(IrqNumber::new(5), dummy);
563 assert_eq!(
564 result,
565 Err(KernelError::AlreadyExists {
566 resource: "IRQ handler",
567 id: 5,
568 })
569 );
570 }
571
572 #[test]
573 fn manager_register_invalid_irq_number() {
574 let mut mgr = IrqManager::new();
575 fn dummy(_irq: IrqNumber) {}
576 // MAX_IRQ is 256, so 256 should be rejected
577 let result = mgr.register(IrqNumber::new(256), dummy);
578 assert_eq!(
579 result,
580 Err(KernelError::InvalidArgument {
581 name: "irq",
582 value: "IRQ number exceeds maximum",
583 })
584 );
585 }
586
587 #[test]
588 fn manager_register_max_valid_irq() {
589 let mut mgr = IrqManager::new();
590 fn dummy(_irq: IrqNumber) {}
591 // 255 is the last valid IRQ (MAX_IRQ - 1)
592 let result = mgr.register(IrqNumber::new(255), dummy);
593 assert!(result.is_ok());
594 }
595
596 #[test]
597 fn manager_unregister_success() {
598 let mut mgr = IrqManager::new();
599 fn dummy(_irq: IrqNumber) {}
600 mgr.register(IrqNumber::new(10), dummy).unwrap();
601 let result = mgr.unregister(IrqNumber::new(10));
602 assert!(result.is_ok());
603 }
604
605 #[test]
606 fn manager_unregister_nonexistent_returns_error() {
607 let mut mgr = IrqManager::new();
608 let result = mgr.unregister(IrqNumber::new(99));
609 assert_eq!(
610 result,
611 Err(KernelError::NotFound {
612 resource: "IRQ handler",
613 id: 99,
614 })
615 );
616 }
617
618 #[test]
619 fn manager_unregister_then_reregister() {
620 let mut mgr = IrqManager::new();
621 fn dummy(_irq: IrqNumber) {}
622 mgr.register(IrqNumber::new(3), dummy).unwrap();
623 mgr.unregister(IrqNumber::new(3)).unwrap();
624 // Should be able to register again after unregistering
625 let result = mgr.register(IrqNumber::new(3), dummy);
626 assert!(result.is_ok());
627 }
628
629 #[test]
630 fn manager_dispatch_increments_count() {
631 let mut mgr = IrqManager::new();
632 fn dummy(_irq: IrqNumber) {}
633 mgr.register(IrqNumber::new(1), dummy).unwrap();
634 assert_eq!(mgr.dispatch_count(), 0);
635 mgr.dispatch(IrqNumber::new(1));
636 assert_eq!(mgr.dispatch_count(), 1);
637 mgr.dispatch(IrqNumber::new(1));
638 mgr.dispatch(IrqNumber::new(1));
639 assert_eq!(mgr.dispatch_count(), 3);
640 }
641
642 #[test]
643 fn manager_dispatch_spurious_irq_increments_count() {
644 // Dispatching an IRQ with no handler should still increment the
645 // dispatch count (it counts total dispatches, not handled ones).
646 let mut mgr = IrqManager::new();
647 mgr.dispatch(IrqNumber::new(42));
648 assert_eq!(mgr.dispatch_count(), 1);
649 }
650
651 #[test]
652 fn manager_dispatch_calls_correct_handler() {
653 use core::sync::atomic::{AtomicU32, Ordering};
654 static CALLED_WITH: AtomicU32 = AtomicU32::new(0);
655
656 fn handler(irq: IrqNumber) {
657 CALLED_WITH.store(irq.as_u32(), Ordering::SeqCst);
658 }
659
660 let mut mgr = IrqManager::new();
661 mgr.register(IrqNumber::new(7), handler).unwrap();
662 mgr.dispatch(IrqNumber::new(7));
663 assert_eq!(CALLED_WITH.load(Ordering::SeqCst), 7);
664 }
665
666 #[test]
667 fn manager_multiple_handlers_independent() {
668 use core::sync::atomic::{AtomicU32, Ordering};
669 static HANDLER_A_COUNT: AtomicU32 = AtomicU32::new(0);
670 static HANDLER_B_COUNT: AtomicU32 = AtomicU32::new(0);
671
672 fn handler_a(_irq: IrqNumber) {
673 HANDLER_A_COUNT.fetch_add(1, Ordering::SeqCst);
674 }
675 fn handler_b(_irq: IrqNumber) {
676 HANDLER_B_COUNT.fetch_add(1, Ordering::SeqCst);
677 }
678
679 let mut mgr = IrqManager::new();
680 mgr.register(IrqNumber::new(10), handler_a).unwrap();
681 mgr.register(IrqNumber::new(20), handler_b).unwrap();
682
683 mgr.dispatch(IrqNumber::new(10));
684 mgr.dispatch(IrqNumber::new(10));
685 mgr.dispatch(IrqNumber::new(20));
686
687 assert_eq!(HANDLER_A_COUNT.load(Ordering::SeqCst), 2);
688 assert_eq!(HANDLER_B_COUNT.load(Ordering::SeqCst), 1);
689 assert_eq!(mgr.dispatch_count(), 3);
690 }
691
692 #[test]
693 fn manager_register_irq_zero() {
694 let mut mgr = IrqManager::new();
695 fn dummy(_irq: IrqNumber) {}
696 // IRQ 0 is valid (timer on x86_64)
697 let result = mgr.register(IrqNumber::new(0), dummy);
698 assert!(result.is_ok());
699 }
700}