Skip to main content

axdevice/
x86.rs

1//! Reusable x86 device package for OS-neutral x86 virtual devices.
2//!
3//! This module intentionally lives outside the architecture-neutral runtime
4//! core: it is compiled only for x86_64 targets and exposes narrow typed
5//! services consumed by AxVM's x86 architecture layer.
6
7use alloc::{boxed::Box, string::String};
8use core::marker::PhantomData;
9
10use axdevice_base::{AccessWidth, BusAccess, BusKind, BusResponse, Device, DeviceError, Resource};
11use x86_vlapic::{
12    EmulatedIoApic, EmulatedPit, EmulatedSerialPort, IoApicEoi, IoApicInterrupt, X86AccessWidth,
13    X86GuestPhysAddr, X86GuestPhysAddrRange, X86Port, X86PortRange, X86VlapicHostOps,
14};
15
16use crate::{ServiceCardinality, ServiceKey};
17
18/// Type-specific IOAPIC capability used by the x86 interrupt runtime.
19pub trait X86IoApicDeviceOps: Send + Sync {
20    /// Return the guest interrupt vector programmed for a GSI.
21    fn vector_for_gsi(&self, gsi: usize) -> Option<u8>;
22
23    /// Assert an IOAPIC GSI and return an interrupt to inject if one is unmasked.
24    fn assert_gsi(&self, gsi: usize) -> Option<IoApicInterrupt>;
25
26    /// Broadcast a local APIC EOI to the IOAPIC.
27    fn end_of_interrupt(&self, vector: u8) -> Option<IoApicEoi>;
28}
29
30/// Type-specific PIT capability used by the x86 interrupt runtime.
31pub trait X86PitDeviceOps: Send + Sync {
32    /// Consume a pending PIT IRQ0 tick if the deadline is due.
33    fn consume_irq0_if_due(&self, now_ns: u64) -> bool;
34}
35
36/// Type-specific COM1 capability used by the x86 interrupt runtime.
37pub trait X86SerialDeviceOps: Send + Sync {
38    /// Poll host input and return whether COM1 should assert an IRQ.
39    fn poll_irq(&self) -> bool;
40}
41
42/// x86 interrupt-controller operations needed by the VM interrupt runtime.
43///
44/// This is an adapter boundary rather than the IOAPIC device type itself:
45/// synthetic and forwarded sources only need to resolve a GSI, assert it, and
46/// process guest EOIs.
47pub trait X86InterruptDomainOps: Send + Sync {
48    /// Returns the guest vector currently programmed for a GSI.
49    fn vector_for_gsi(&self, gsi: usize) -> Option<u8>;
50
51    /// Asserts a GSI and returns an interrupt to inject when it is unmasked.
52    fn assert_gsi(&self, gsi: usize) -> Option<IoApicInterrupt>;
53
54    /// Processes a guest local-APIC EOI.
55    fn end_of_interrupt(&self, vector: u8) -> Option<IoApicEoi>;
56}
57
58/// Typed service key for the VM's x86 virtual I/O APIC.
59pub struct X86IoApicServiceKey;
60
61impl ServiceKey for X86IoApicServiceKey {
62    type Service = dyn X86IoApicDeviceOps;
63
64    const NAME: &'static str = "x86-ioapic";
65    const CARDINALITY: ServiceCardinality = ServiceCardinality::Single;
66}
67
68/// Typed service key for the VM's x86 interrupt-domain adapter.
69pub struct X86InterruptDomainKey;
70
71impl ServiceKey for X86InterruptDomainKey {
72    type Service = dyn X86InterruptDomainOps;
73
74    const NAME: &'static str = "x86-interrupt-domain";
75    const CARDINALITY: ServiceCardinality = ServiceCardinality::Single;
76}
77
78/// Typed service key for the VM's x86 virtual PIT.
79pub struct X86PitServiceKey;
80
81impl ServiceKey for X86PitServiceKey {
82    type Service = dyn X86PitDeviceOps;
83
84    const NAME: &'static str = "x86-pit";
85    const CARDINALITY: ServiceCardinality = ServiceCardinality::Single;
86}
87
88/// Typed service key for the VM's COM1 serial input capability.
89pub struct X86SerialServiceKey;
90
91impl ServiceKey for X86SerialServiceKey {
92    type Service = dyn X86SerialDeviceOps;
93
94    const NAME: &'static str = "x86-serial-com1";
95    const CARDINALITY: ServiceCardinality = ServiceCardinality::Single;
96}
97
98/// Unified-device adapter for [`EmulatedIoApic`].
99pub struct X86IoApicDevice {
100    inner: EmulatedIoApic,
101    name: String,
102    resources: Box<[Resource]>,
103}
104
105impl X86IoApicDevice {
106    /// Creates an IOAPIC adapter with the given guest MMIO range.
107    pub fn new(base: X86GuestPhysAddr, size: Option<usize>) -> Self {
108        let inner = EmulatedIoApic::new(base, size);
109        let resources = mmio_resources(inner.address_range());
110        Self {
111            inner,
112            name: String::from("x86-ioapic"),
113            resources,
114        }
115    }
116
117    /// Returns the wrapped OS-neutral IOAPIC core.
118    pub const fn inner(&self) -> &EmulatedIoApic {
119        &self.inner
120    }
121}
122
123impl X86IoApicDeviceOps for X86IoApicDevice {
124    fn vector_for_gsi(&self, gsi: usize) -> Option<u8> {
125        self.inner.vector_for_gsi(gsi)
126    }
127
128    fn assert_gsi(&self, gsi: usize) -> Option<IoApicInterrupt> {
129        self.inner.assert_gsi(gsi)
130    }
131
132    fn end_of_interrupt(&self, vector: u8) -> Option<IoApicEoi> {
133        self.inner.end_of_interrupt(vector)
134    }
135}
136
137impl Device for X86IoApicDevice {
138    fn name(&self) -> &str {
139        &self.name
140    }
141
142    fn resources(&self) -> &[Resource] {
143        &self.resources
144    }
145
146    fn access(
147        &self,
148        access: &BusAccess,
149        _context: &mut dyn axdevice_base::DeviceAccess,
150    ) -> Result<BusResponse, DeviceError> {
151        if access.kind != BusKind::Mmio {
152            return Err(DeviceError::OutOfRange { addr: access.addr });
153        }
154        let addr = X86GuestPhysAddr::from_usize(access.addr as usize);
155        let width = x86_access_width(access.width);
156        if access.is_read {
157            self.inner
158                .handle_read(addr, width)
159                .map(|value| BusResponse::Read {
160                    value: value as u64,
161                })
162                .map_err(|_| DeviceError::Internal)
163        } else {
164            self.inner
165                .handle_write(addr, width, access.data as usize)
166                .map(|_| BusResponse::Write)
167                .map_err(|_| DeviceError::Internal)
168        }
169    }
170}
171
172/// Unified-device adapter for [`EmulatedPit`].
173pub struct X86PitDevice<H: X86VlapicHostOps> {
174    inner: EmulatedPit<H>,
175    name: String,
176    resources: Box<[Resource]>,
177    _host: PhantomData<fn() -> H>,
178}
179
180impl<H: X86VlapicHostOps> X86PitDevice<H> {
181    /// Creates a PIT adapter.
182    pub fn new() -> Self {
183        let inner = EmulatedPit::<H>::new();
184        let resources = port_resources(inner.address_range());
185        Self {
186            inner,
187            name: String::from("x86-pit"),
188            resources,
189            _host: PhantomData,
190        }
191    }
192
193    /// Returns the wrapped OS-neutral PIT core.
194    pub const fn inner(&self) -> &EmulatedPit<H> {
195        &self.inner
196    }
197}
198
199impl<H: X86VlapicHostOps> Default for X86PitDevice<H> {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl<H: X86VlapicHostOps> X86PitDeviceOps for X86PitDevice<H> {
206    fn consume_irq0_if_due(&self, now_ns: u64) -> bool {
207        self.inner.consume_irq0_if_due(now_ns)
208    }
209}
210
211impl<H: X86VlapicHostOps + 'static> Device for X86PitDevice<H> {
212    fn name(&self) -> &str {
213        &self.name
214    }
215
216    fn resources(&self) -> &[Resource] {
217        &self.resources
218    }
219
220    fn access(
221        &self,
222        access: &BusAccess,
223        _context: &mut dyn axdevice_base::DeviceAccess,
224    ) -> Result<BusResponse, DeviceError> {
225        if access.kind != BusKind::Port {
226            return Err(DeviceError::OutOfRange { addr: access.addr });
227        }
228        let port = X86Port::new(
229            u16::try_from(access.addr)
230                .map_err(|_| DeviceError::OutOfRange { addr: access.addr })?,
231        );
232        let width = x86_access_width(access.width);
233        if access.is_read {
234            self.inner
235                .handle_read(port, width)
236                .map(|value| BusResponse::Read {
237                    value: value as u64,
238                })
239                .map_err(|_| DeviceError::Internal)
240        } else {
241            self.inner
242                .handle_write(port, width, access.data as usize)
243                .map(|_| BusResponse::Write)
244                .map_err(|_| DeviceError::Internal)
245        }
246    }
247}
248
249/// Unified-device adapter for [`EmulatedSerialPort`].
250pub struct X86SerialPortDevice<H: X86VlapicHostOps> {
251    inner: EmulatedSerialPort<H>,
252    name: String,
253    resources: Box<[Resource]>,
254    _host: PhantomData<fn() -> H>,
255}
256
257impl<H: X86VlapicHostOps> X86SerialPortDevice<H> {
258    /// Creates a COM1 adapter.
259    pub fn new() -> Self {
260        let inner = EmulatedSerialPort::<H>::new();
261        let resources = port_resources(inner.address_range());
262        Self {
263            inner,
264            name: String::from("x86-serial-com1"),
265            resources,
266            _host: PhantomData,
267        }
268    }
269
270    /// Returns the wrapped OS-neutral COM1 core.
271    pub const fn inner(&self) -> &EmulatedSerialPort<H> {
272        &self.inner
273    }
274}
275
276impl<H: X86VlapicHostOps> Default for X86SerialPortDevice<H> {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282impl<H: X86VlapicHostOps> X86SerialDeviceOps for X86SerialPortDevice<H> {
283    fn poll_irq(&self) -> bool {
284        self.inner.poll_irq()
285    }
286}
287
288impl<H: X86VlapicHostOps + 'static> Device for X86SerialPortDevice<H> {
289    fn name(&self) -> &str {
290        &self.name
291    }
292
293    fn resources(&self) -> &[Resource] {
294        &self.resources
295    }
296
297    fn access(
298        &self,
299        access: &BusAccess,
300        _context: &mut dyn axdevice_base::DeviceAccess,
301    ) -> Result<BusResponse, DeviceError> {
302        if access.kind != BusKind::Port {
303            return Err(DeviceError::OutOfRange { addr: access.addr });
304        }
305        let port = X86Port::new(
306            u16::try_from(access.addr)
307                .map_err(|_| DeviceError::OutOfRange { addr: access.addr })?,
308        );
309        let width = x86_access_width(access.width);
310        if access.is_read {
311            self.inner
312                .handle_read(port, width)
313                .map(|value| BusResponse::Read {
314                    value: value as u64,
315                })
316                .map_err(|_| DeviceError::Internal)
317        } else {
318            self.inner
319                .handle_write(port, width, access.data as usize)
320                .map(|_| BusResponse::Write)
321                .map_err(|_| DeviceError::Internal)
322        }
323    }
324}
325
326fn x86_access_width(width: AccessWidth) -> X86AccessWidth {
327    match width {
328        AccessWidth::Byte => X86AccessWidth::Byte,
329        AccessWidth::Word => X86AccessWidth::Word,
330        AccessWidth::Dword => X86AccessWidth::Dword,
331        AccessWidth::Qword => X86AccessWidth::Qword,
332    }
333}
334
335fn mmio_resources(range: X86GuestPhysAddrRange) -> Box<[Resource]> {
336    let base = range.start.as_usize() as u64;
337    let size = range.end.as_usize().saturating_sub(range.start.as_usize()) as u64;
338    alloc::vec![Resource::MmioRange { base, size }].into_boxed_slice()
339}
340
341fn port_resources(range: X86PortRange) -> Box<[Resource]> {
342    let base = range.start.number();
343    let size = range
344        .end
345        .number()
346        .saturating_sub(range.start.number())
347        .saturating_add(1);
348    alloc::vec![Resource::PortRange { base, size }].into_boxed_slice()
349}