From d17483f2cd26f7e89cca38665460ff2cc9d6faf2 Mon Sep 17 00:00:00 2001 From: baitwo02 Date: Mon, 15 Jun 2026 13:21:25 +0000 Subject: [PATCH 1/5] feat(axdevice): add unified device registry --- virtualization/axdevice/src/bus.rs | 150 ++++++ virtualization/axdevice/src/device.rs | 64 ++- virtualization/axdevice/src/irq.rs | 99 ++++ virtualization/axdevice/src/legacy.rs | 190 +++++++ virtualization/axdevice/src/lib.rs | 12 + virtualization/axdevice/src/model.rs | 184 +++++++ virtualization/axdevice/src/registry.rs | 211 ++++++++ virtualization/axdevice/src/resource.rs | 101 ++++ virtualization/axdevice/tests/test.rs | 645 +++++++++++++++++++++++- virtualization/axvm/src/vm.rs | 94 +++- 10 files changed, 1726 insertions(+), 24 deletions(-) create mode 100644 virtualization/axdevice/src/bus.rs create mode 100644 virtualization/axdevice/src/irq.rs create mode 100644 virtualization/axdevice/src/legacy.rs create mode 100644 virtualization/axdevice/src/model.rs create mode 100644 virtualization/axdevice/src/registry.rs create mode 100644 virtualization/axdevice/src/resource.rs diff --git a/virtualization/axdevice/src/bus.rs b/virtualization/axdevice/src/bus.rs new file mode 100644 index 0000000000..f9e370e811 --- /dev/null +++ b/virtualization/axdevice/src/bus.rs @@ -0,0 +1,150 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Unified bus transaction types for emulated device access. + +use axdevice_base::{AccessWidth, Port, SysRegAddr}; +use axvm_types::GuestPhysAddr; + +/// The kind of guest-visible bus or register namespace used for a device access. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BusKind { + /// Memory-mapped I/O in the guest physical address space. + Mmio, + /// Port I/O, primarily used by x86 `in`/`out` instructions. + Pio, + /// Architecture system register namespace such as MSR, CSR, or AArch64 sysreg. + SysReg, +} + +/// A bus address tagged with the namespace it belongs to. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum BusAddress { + /// A guest physical MMIO address. + Mmio(GuestPhysAddr), + /// A port I/O address. + Pio(Port), + /// A system register address. + SysReg(SysRegAddr), +} + +impl BusAddress { + /// Returns the bus kind implied by this address. + pub const fn kind(self) -> BusKind { + match self { + Self::Mmio(_) => BusKind::Mmio, + Self::Pio(_) => BusKind::Pio, + Self::SysReg(_) => BusKind::SysReg, + } + } +} + +/// The operation requested by a bus access. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BusOp { + /// Read from the addressed device register. + Read, + /// Write a value to the addressed device register. + Write { + /// The value to write. Only the low bits selected by [`BusAccess::width`] are significant. + value: usize, + }, +} + +/// A normalized device access generated from a VM exit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BusAccess { + /// The bus namespace used by the access. + pub kind: BusKind, + /// The address in the selected bus namespace. + pub addr: BusAddress, + /// The access width. + pub width: AccessWidth, + /// The requested operation. + pub op: BusOp, +} + +impl BusAccess { + /// Creates a new bus access. + pub const fn new(kind: BusKind, addr: BusAddress, width: AccessWidth, op: BusOp) -> Self { + Self { + kind, + addr, + width, + op, + } + } + + /// Creates an MMIO read access. + pub const fn mmio_read(addr: GuestPhysAddr, width: AccessWidth) -> Self { + Self::new(BusKind::Mmio, BusAddress::Mmio(addr), width, BusOp::Read) + } + + /// Creates an MMIO write access. + pub const fn mmio_write(addr: GuestPhysAddr, width: AccessWidth, value: usize) -> Self { + Self::new( + BusKind::Mmio, + BusAddress::Mmio(addr), + width, + BusOp::Write { value }, + ) + } + + /// Creates a port I/O read access. + pub const fn pio_read(port: Port, width: AccessWidth) -> Self { + Self::new(BusKind::Pio, BusAddress::Pio(port), width, BusOp::Read) + } + + /// Creates a port I/O write access. + pub const fn pio_write(port: Port, width: AccessWidth, value: usize) -> Self { + Self::new( + BusKind::Pio, + BusAddress::Pio(port), + width, + BusOp::Write { value }, + ) + } + + /// Creates a system register read access. + pub const fn sysreg_read(addr: SysRegAddr, width: AccessWidth) -> Self { + Self::new( + BusKind::SysReg, + BusAddress::SysReg(addr), + width, + BusOp::Read, + ) + } + + /// Creates a system register write access. + pub const fn sysreg_write(addr: SysRegAddr, width: AccessWidth, value: usize) -> Self { + Self::new( + BusKind::SysReg, + BusAddress::SysReg(addr), + width, + BusOp::Write { value }, + ) + } +} + +/// The result returned by a device for a bus access. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BusResponse { + /// A read completed and returned a value. + Read { + /// The read value, zero-extended by the device implementation when needed. + value: usize, + }, + /// A write completed. + Write, +} diff --git a/virtualization/axdevice/src/device.rs b/virtualization/axdevice/src/device.rs index d65bc9baa6..4c8131aa4d 100644 --- a/virtualization/axdevice/src/device.rs +++ b/virtualization/axdevice/src/device.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::{sync::Arc, vec::Vec}; +use alloc::{format, rc::Rc, sync::Arc, vec, vec::Vec}; use core::ops::Range; #[cfg(target_arch = "aarch64")] @@ -32,7 +32,10 @@ use riscv_vplic::VPlicGlobal; #[cfg(target_arch = "x86_64")] use x86_vlapic::{EmulatedIoApic, EmulatedPit, EmulatedSerialPort, IoApicInterrupt}; -use crate::{AxVmDeviceConfig, range_alloc::RangeAllocator}; +use crate::{ + AxVmDeviceConfig, BusAccess, BusResponse, DeviceCapabilities, DeviceId, DeviceRegistry, + DeviceResult, LegacyDeviceAdapter, Resource, range_alloc::RangeAllocator, +}; /// A set of emulated device types that can be accessed by a specific address range type. pub struct AxEmuDevices { @@ -93,6 +96,8 @@ pub struct AxVmDevices { emu_mmio_devices: AxEmuMmioDevices, emu_sys_reg_devices: AxEmuSysRegDevices, emu_port_devices: AxEmuPortDevices, + registry: DeviceRegistry, + next_device_id: usize, #[cfg(target_arch = "x86_64")] x86_ioapic: Option>, #[cfg(target_arch = "x86_64")] @@ -137,6 +142,8 @@ impl AxVmDevices { emu_mmio_devices: AxEmuMmioDevices::new(), emu_sys_reg_devices: AxEmuSysRegDevices::new(), emu_port_devices: AxEmuPortDevices::new(), + registry: DeviceRegistry::new(), + next_device_id: 0, #[cfg(target_arch = "x86_64")] x86_ioapic: None, #[cfg(target_arch = "x86_64")] @@ -428,21 +435,68 @@ impl AxVmDevices { } } - /// Add a MMIO device to the device list + fn alloc_device_id(&mut self) -> DeviceId { + let id = DeviceId::new(self.next_device_id); + self.next_device_id += 1; + id + } + + fn register_legacy_device(&mut self, adapter: LegacyDeviceAdapter) { + if let Err(err) = self.registry.register_device(Rc::new(adapter)) { + panic!("failed to register legacy device in DeviceRegistry: {err}"); + } + } + + /// Add a MMIO device to the device list. pub fn add_mmio_dev(&mut self, dev: Arc) { + let id = self.alloc_device_id(); + let name = format!("legacy-mmio-{}", dev.emu_type()); + let resources = vec![Resource::Mmio(dev.address_range())]; + self.register_legacy_device(LegacyDeviceAdapter::mmio( + id, + name, + resources, + DeviceCapabilities::none(), + Arc::clone(&dev), + )); self.emu_mmio_devices.add_dev(dev); } - /// Add a system register device to the device list + /// Add a system register device to the device list. pub fn add_sys_reg_dev(&mut self, dev: Arc) { + let id = self.alloc_device_id(); + let name = format!("legacy-sysreg-{}", dev.emu_type()); + let resources = vec![Resource::SysReg(dev.address_range())]; + self.register_legacy_device(LegacyDeviceAdapter::sysreg( + id, + name, + resources, + DeviceCapabilities::none(), + Arc::clone(&dev), + )); self.emu_sys_reg_devices.add_dev(dev); } - /// Add a port device to the device list + /// Add a port device to the device list. pub fn add_port_dev(&mut self, dev: Arc) { + let id = self.alloc_device_id(); + let name = format!("legacy-pio-{}", dev.emu_type()); + let resources = vec![Resource::Pio(dev.address_range())]; + self.register_legacy_device(LegacyDeviceAdapter::pio( + id, + name, + resources, + DeviceCapabilities::none(), + Arc::clone(&dev), + )); self.emu_port_devices.add_dev(dev); } + /// Dispatches a normalized bus access through the new device registry. + pub fn dispatch_bus_access(&self, access: BusAccess) -> DeviceResult { + self.registry.dispatch(access) + } + /// Iterates over the MMIO devices in the set. pub fn iter_mmio_dev(&self) -> impl Iterator> { self.emu_mmio_devices.iter() diff --git a/virtualization/axdevice/src/irq.rs b/virtualization/axdevice/src/irq.rs new file mode 100644 index 0000000000..945bbca3de --- /dev/null +++ b/virtualization/axdevice/src/irq.rs @@ -0,0 +1,99 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Architecture-neutral interrupt signaling traits for emulated devices. + +use crate::model::DeviceError; + +/// A guest-visible interrupt line used by an emulated device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct IrqLine(usize); + +impl IrqLine { + /// Creates a new interrupt line identifier. + pub const fn new(line: usize) -> Self { + Self(line) + } + + /// Returns the raw interrupt line number. + pub const fn number(self) -> usize { + self.0 + } +} + +/// The target of an interrupt operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IrqTarget { + /// Route to the architecture-defined default target. + Default, + /// Broadcast to all active virtual CPUs. + Broadcast, + /// Route to one virtual CPU. + Vcpu(usize), + /// Route to a mask of virtual CPUs. + VcpuMask(u64), +} + +/// A message-signaled interrupt payload. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MsiMessage { + /// Guest-programmed MSI address. + pub address: u64, + /// Guest-programmed MSI data. + pub data: u32, + /// Optional logical target interpreted by the interrupt router. + pub target: Option, +} + +impl MsiMessage { + /// Creates a new MSI message with no explicit target override. + pub const fn new(address: u64, data: u32) -> Self { + Self { + address, + data, + target: None, + } + } + + /// Creates a new MSI message with an explicit target. + pub const fn with_target(address: u64, data: u32, target: IrqTarget) -> Self { + Self { + address, + data, + target: Some(target), + } + } +} + +/// Device-facing interrupt sink. +/// +/// Implementations translate these semantic operations into the architecture-specific +/// interrupt controller backend, such as vIOAPIC/vLAPIC, VGIC, vPLIC/AIA, or LoongArch +/// virtual interrupt state. +pub trait IrqSink { + /// Assert a level interrupt line. + fn raise(&self, line: IrqLine) -> Result<(), DeviceError>; + + /// Deassert a level interrupt line. + fn lower(&self, line: IrqLine) -> Result<(), DeviceError>; + + /// Generate an edge-style interrupt pulse. + fn pulse(&self, line: IrqLine) -> Result<(), DeviceError>; + + /// Deliver a message-signaled interrupt. + fn msi(&self, message: MsiMessage) -> Result<(), DeviceError>; + + /// Notify end-of-interrupt for a line when the backend needs it. + fn eoi(&self, line: IrqLine) -> Result<(), DeviceError>; +} diff --git a/virtualization/axdevice/src/legacy.rs b/virtualization/axdevice/src/legacy.rs new file mode 100644 index 0000000000..e00ca66909 --- /dev/null +++ b/virtualization/axdevice/src/legacy.rs @@ -0,0 +1,190 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Adapters from the existing `BaseDeviceOps` traits into the new device model. + +use alloc::{string::String, sync::Arc, vec::Vec}; + +use axdevice_base::{BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps}; + +use crate::{ + bus::{BusAccess, BusAddress, BusKind, BusOp, BusResponse}, + model::{DeviceError, DeviceId, DeviceOps, DeviceResult}, + resource::{DeviceCapabilities, Resource}, +}; + +/// The old single-bus device object carried by a legacy adapter. +pub enum LegacyDeviceInner { + /// Existing MMIO device implementation. + Mmio(Arc), + /// Existing port I/O device implementation. + Pio(Arc), + /// Existing system register device implementation. + SysReg(Arc), +} + +impl LegacyDeviceInner { + fn kind(&self) -> BusKind { + match self { + Self::Mmio(_) => BusKind::Mmio, + Self::Pio(_) => BusKind::Pio, + Self::SysReg(_) => BusKind::SysReg, + } + } +} + +/// Adapter that exposes an existing `BaseDeviceOps` object as [`DeviceOps`]. +pub struct LegacyDeviceAdapter { + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + inner: LegacyDeviceInner, +} + +impl LegacyDeviceAdapter { + /// Creates an adapter from raw parts. + pub fn new( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + inner: LegacyDeviceInner, + ) -> Self { + Self { + id, + name, + resources, + capabilities, + inner, + } + } + + /// Creates an MMIO legacy adapter. + pub fn mmio( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + device: Arc, + ) -> Self { + Self::new( + id, + name, + resources, + capabilities, + LegacyDeviceInner::Mmio(device), + ) + } + + /// Creates a port I/O legacy adapter. + pub fn pio( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + device: Arc, + ) -> Self { + Self::new( + id, + name, + resources, + capabilities, + LegacyDeviceInner::Pio(device), + ) + } + + /// Creates a system register legacy adapter. + pub fn sysreg( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + device: Arc, + ) -> Self { + Self::new( + id, + name, + resources, + capabilities, + LegacyDeviceInner::SysReg(device), + ) + } +} + +impl DeviceOps for LegacyDeviceAdapter { + fn id(&self) -> DeviceId { + self.id + } + + fn name(&self) -> &str { + &self.name + } + + fn resources(&self) -> &[Resource] { + &self.resources + } + + fn capabilities(&self) -> DeviceCapabilities { + self.capabilities + } + + fn access(&self, access: BusAccess) -> DeviceResult { + if access.kind != access.addr.kind() || access.kind != self.inner.kind() { + return Err(DeviceError::BusAddressMismatch { + kind: self.inner.kind(), + address: access.addr, + }); + } + + match (&self.inner, access.addr, access.op) { + (LegacyDeviceInner::Mmio(device), BusAddress::Mmio(addr), BusOp::Read) => device + .handle_read(addr, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + (LegacyDeviceInner::Mmio(device), BusAddress::Mmio(addr), BusOp::Write { value }) => { + device + .handle_write(addr, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from) + } + (LegacyDeviceInner::Pio(device), BusAddress::Pio(port), BusOp::Read) => device + .handle_read(port, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + (LegacyDeviceInner::Pio(device), BusAddress::Pio(port), BusOp::Write { value }) => { + device + .handle_write(port, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from) + } + (LegacyDeviceInner::SysReg(device), BusAddress::SysReg(addr), BusOp::Read) => device + .handle_read(addr, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + ( + LegacyDeviceInner::SysReg(device), + BusAddress::SysReg(addr), + BusOp::Write { value }, + ) => device + .handle_write(addr, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from), + _ => Err(DeviceError::BusAddressMismatch { + kind: self.inner.kind(), + address: access.addr, + }), + } + } +} diff --git a/virtualization/axdevice/src/lib.rs b/virtualization/axdevice/src/lib.rs index cb8de1fe40..7d7165f3d2 100644 --- a/virtualization/axdevice/src/lib.rs +++ b/virtualization/axdevice/src/lib.rs @@ -26,17 +26,29 @@ extern crate alloc; #[macro_use] extern crate log; +mod bus; mod config; mod device; +mod irq; +mod legacy; +mod model; mod range_alloc; +mod registry; +mod resource; pub use axdevice_base::{ AccessWidth, BaseDeviceOps, BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps, Port, SysRegAddr, }; pub use axvm_types::GuestPhysAddr; +pub use bus::{BusAccess, BusAddress, BusKind, BusOp, BusResponse}; pub use config::AxVmDeviceConfig; pub use device::{AxEmuDevices, AxVmDevices}; +pub use irq::{IrqLine, IrqSink, IrqTarget, MsiMessage}; +pub use legacy::{LegacyDeviceAdapter, LegacyDeviceInner}; +pub use model::{DeviceError, DeviceId, DeviceOps, DeviceResult}; +pub use registry::DeviceRegistry; +pub use resource::{DeviceCapabilities, PciBarKind, Resource}; #[cfg(target_arch = "x86_64")] pub use x86_vlapic::IoApicInterrupt; // pub use virtio_dev::*; diff --git a/virtualization/axdevice/src/model.rs b/virtualization/axdevice/src/model.rs new file mode 100644 index 0000000000..eb578d310c --- /dev/null +++ b/virtualization/axdevice/src/model.rs @@ -0,0 +1,184 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Core emulated device model traits and errors. + +use core::{any::Any, fmt}; + +use ax_errno::AxError; +use axdevice_base::AccessWidth; + +use crate::{ + bus::{BusAccess, BusAddress, BusKind, BusResponse}, + resource::{DeviceCapabilities, Resource}, +}; + +/// Unique identifier for a device instance inside one registry. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(usize); + +impl DeviceId { + /// Creates a device identifier from a raw numeric value. + pub const fn new(id: usize) -> Self { + Self(id) + } + + /// Returns the raw numeric identifier. + pub const fn raw(self) -> usize { + self.0 + } +} + +/// Error type used by the new device abstraction layer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceError { + /// No device was registered for the requested bus access. + DeviceNotFound { + /// Bus namespace searched by the router. + kind: BusKind, + /// Address that missed all registered device resources. + address: BusAddress, + }, + /// A device with the same registry-local identifier already exists. + DuplicateDeviceId { + /// Duplicated identifier. + id: DeviceId, + }, + /// The bus kind and concrete bus address do not describe the same namespace. + BusAddressMismatch { + /// Expected or requested bus namespace. + kind: BusKind, + /// Address carrying a different bus namespace. + address: BusAddress, + }, + /// A resource conflicts with an already registered resource. + ResourceConflict { + /// Existing registered resource. + existing: Resource, + /// Newly requested resource. + requested: Resource, + }, + /// The access width is not accepted by the device or bus. + InvalidAccessWidth { + /// Rejected access width. + width: AccessWidth, + }, + /// The address does not belong to the resource handled by the selected device. + AddressOutOfRange { + /// Bus namespace used by the access. + kind: BusKind, + /// Rejected address. + address: BusAddress, + }, + /// A write was attempted on a read-only register. + ReadOnly { + /// Bus namespace used by the access. + kind: BusKind, + /// Rejected address. + address: BusAddress, + }, + /// A read was attempted on a write-only register. + WriteOnly { + /// Bus namespace used by the access. + kind: BusKind, + /// Rejected address. + address: BusAddress, + }, + /// The operation is validly formed but unsupported by this device/backend. + UnsupportedOperation, + /// Error returned by an existing backend while it is adapted into the new model. + Backend(AxError), +} + +impl From for DeviceError { + fn from(error: AxError) -> Self { + Self::Backend(error) + } +} + +impl fmt::Display for DeviceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DeviceNotFound { kind, address } => { + write!(f, "device not found for {kind:?} address {address:?}") + } + Self::DuplicateDeviceId { id } => { + write!(f, "duplicate device id {:?}", id) + } + Self::BusAddressMismatch { kind, address } => { + write!(f, "bus kind {kind:?} does not match address {address:?}") + } + Self::ResourceConflict { + existing, + requested, + } => write!( + f, + "device resource conflict: existing {existing:?}, requested {requested:?}" + ), + Self::InvalidAccessWidth { width } => { + write!(f, "invalid device access width {width:?}") + } + Self::AddressOutOfRange { kind, address } => { + write!( + f, + "{kind:?} address {address:?} is outside the selected device resource" + ) + } + Self::ReadOnly { kind, address } => { + write!(f, "write to read-only {kind:?} address {address:?}") + } + Self::WriteOnly { kind, address } => { + write!(f, "read from write-only {kind:?} address {address:?}") + } + Self::UnsupportedOperation => write!(f, "unsupported device operation"), + Self::Backend(error) => write!(f, "device backend error: {error}"), + } + } +} + +/// Result type for the new device abstraction layer. +pub type DeviceResult = Result; + +/// Unified interface for an emulated device instance. +pub trait DeviceOps: Any { + /// Returns the registry-local device identifier. + fn id(&self) -> DeviceId; + + /// Returns a human-readable device instance name. + fn name(&self) -> &str; + + /// Returns all resources occupied or requested by this device. + fn resources(&self) -> &[Resource]; + + /// Returns optional capabilities supported by this device. + fn capabilities(&self) -> DeviceCapabilities; + + /// Handles a normalized bus access. + fn access(&self, access: BusAccess) -> DeviceResult; + + /// Resets the device state. + fn reset(&self) -> DeviceResult { + Ok(()) + } + + /// Suspends the device state. + fn suspend(&self) -> DeviceResult { + Ok(()) + } + + /// Resumes the device state. + fn resume(&self) -> DeviceResult { + Ok(()) + } +} diff --git a/virtualization/axdevice/src/registry.rs b/virtualization/axdevice/src/registry.rs new file mode 100644 index 0000000000..b9ff0832fd --- /dev/null +++ b/virtualization/axdevice/src/registry.rs @@ -0,0 +1,211 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device registry and first-stage bus routing tables. + +use alloc::{rc::Rc, vec::Vec}; + +use axdevice_base::{DeviceAddrRange, PortRange, SysRegAddrRange}; +use axvm_types::GuestPhysAddrRange; + +use crate::{ + bus::{BusAccess, BusAddress, BusResponse}, + model::{DeviceError, DeviceId, DeviceOps, DeviceResult}, + resource::Resource, +}; + +/// A route entry for MMIO accesses. +type MmioRoute = (GuestPhysAddrRange, DeviceId); +/// A route entry for port I/O accesses. +type PioRoute = (PortRange, DeviceId); +/// A route entry for system register accesses. +type SysRegRoute = (SysRegAddrRange, DeviceId); + +/// Registry for emulated devices and their bus-visible resources. +#[derive(Default)] +pub struct DeviceRegistry { + devices: Vec>, + mmio_routes: Vec, + pio_routes: Vec, + sysreg_routes: Vec, +} + +impl DeviceRegistry { + /// Creates an empty registry. + pub const fn new() -> Self { + Self { + devices: Vec::new(), + mmio_routes: Vec::new(), + pio_routes: Vec::new(), + sysreg_routes: Vec::new(), + } + } + + /// Registers a device and indexes its bus resources. + pub fn register_device(&mut self, device: Rc) -> DeviceResult { + let id = device.id(); + if self.devices.iter().any(|existing| existing.id() == id) { + return Err(DeviceError::DuplicateDeviceId { id }); + } + + self.check_resource_conflicts(device.resources())?; + + for resource in device.resources() { + match *resource { + Resource::Mmio(range) => self.mmio_routes.push((range, id)), + Resource::Pio(range) => self.pio_routes.push((range, id)), + Resource::SysReg(range) => self.sysreg_routes.push((range, id)), + Resource::Irq(_) + | Resource::Msi { .. } + | Resource::Dma + | Resource::PciBar { .. } => {} + } + } + + self.devices.push(device); + Ok(id) + } + + /// Finds a device by identifier. + pub fn find_device(&self, id: DeviceId) -> Option> { + self.devices + .iter() + .find(|device| device.id() == id) + .cloned() + } + + /// Dispatches a normalized bus access to the registered device that owns the route. + pub fn dispatch(&self, access: BusAccess) -> DeviceResult { + if access.kind != access.addr.kind() { + return Err(DeviceError::BusAddressMismatch { + kind: access.kind, + address: access.addr, + }); + } + + let device_id = match access.addr { + BusAddress::Mmio(addr) => self + .mmio_routes + .iter() + .find(|(range, _)| range.contains(addr)) + .map(|(_, id)| *id), + BusAddress::Pio(port) => self + .pio_routes + .iter() + .find(|(range, _)| range.contains(port)) + .map(|(_, id)| *id), + BusAddress::SysReg(addr) => self + .sysreg_routes + .iter() + .find(|(range, _)| range.contains(addr)) + .map(|(_, id)| *id), + } + .ok_or(DeviceError::DeviceNotFound { + kind: access.kind, + address: access.addr, + })?; + + let device = self + .find_device(device_id) + .ok_or(DeviceError::DeviceNotFound { + kind: access.kind, + address: access.addr, + })?; + + device.access(access) + } + + /// Returns the number of registered devices. + pub fn device_count(&self) -> usize { + self.devices.len() + } + + /// Returns the number of MMIO routes. + pub fn mmio_route_count(&self) -> usize { + self.mmio_routes.len() + } + + /// Returns the number of port I/O routes. + pub fn pio_route_count(&self) -> usize { + self.pio_routes.len() + } + + /// Returns the number of system register routes. + pub fn sysreg_route_count(&self) -> usize { + self.sysreg_routes.len() + } + + fn check_resource_conflicts(&self, resources: &[Resource]) -> DeviceResult { + for resource in resources { + match *resource { + Resource::Mmio(requested) => { + if let Some((existing, _)) = self + .mmio_routes + .iter() + .find(|(existing, _)| mmio_ranges_overlap(*existing, requested)) + { + return Err(DeviceError::ResourceConflict { + existing: Resource::Mmio(*existing), + requested: *resource, + }); + } + } + Resource::Pio(requested) => { + if let Some((existing, _)) = self + .pio_routes + .iter() + .find(|(existing, _)| port_ranges_overlap(*existing, requested)) + { + return Err(DeviceError::ResourceConflict { + existing: Resource::Pio(*existing), + requested: *resource, + }); + } + } + Resource::SysReg(requested) => { + if let Some((existing, _)) = self + .sysreg_routes + .iter() + .find(|(existing, _)| sysreg_ranges_overlap(*existing, requested)) + { + return Err(DeviceError::ResourceConflict { + existing: Resource::SysReg(*existing), + requested: *resource, + }); + } + } + Resource::Irq(_) + | Resource::Msi { .. } + | Resource::Dma + | Resource::PciBar { .. } => {} + } + } + Ok(()) + } +} + +#[inline] +fn mmio_ranges_overlap(left: GuestPhysAddrRange, right: GuestPhysAddrRange) -> bool { + left.start < right.end && right.start < left.end +} + +#[inline] +fn port_ranges_overlap(left: PortRange, right: PortRange) -> bool { + left.start <= right.end && right.start <= left.end +} + +#[inline] +fn sysreg_ranges_overlap(left: SysRegAddrRange, right: SysRegAddrRange) -> bool { + left.start <= right.end && right.start <= left.end +} diff --git a/virtualization/axdevice/src/resource.rs b/virtualization/axdevice/src/resource.rs new file mode 100644 index 0000000000..9c927ece67 --- /dev/null +++ b/virtualization/axdevice/src/resource.rs @@ -0,0 +1,101 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Resource and capability declarations for emulated devices. + +use axdevice_base::{PortRange, SysRegAddrRange}; +use axvm_types::GuestPhysAddrRange; + +use crate::irq::IrqLine; + +/// The kind of PCI BAR exposed by a device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PciBarKind { + /// A 32-bit memory BAR. + Mmio32 { + /// Whether the BAR is prefetchable. + prefetchable: bool, + }, + /// A 64-bit memory BAR. + Mmio64 { + /// Whether the BAR is prefetchable. + prefetchable: bool, + }, + /// A port I/O BAR. + Pio, +} + +/// A resource occupied or requested by an emulated device. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Resource { + /// A guest physical MMIO range. + Mmio(GuestPhysAddrRange), + /// A port I/O range. + Pio(PortRange), + /// A system register range. + SysReg(SysRegAddrRange), + /// A legacy interrupt line. + Irq(IrqLine), + /// A message-signaled interrupt vector allocation request. + Msi { + /// Number of MSI/MSI-X vectors requested or supported. + vectors: u16, + }, + /// The device can initiate DMA to guest memory. + Dma, + /// A PCI BAR exposed by the device. + PciBar { + /// BAR index in PCI configuration space. + index: u8, + /// BAR type and attributes. + kind: PciBarKind, + }, +} + +/// Capability flags exposed by an emulated device. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DeviceCapabilities { + /// The device supports MSI. + pub msi: bool, + /// The device supports MSI-X. + pub msix: bool, + /// The device may initiate DMA. + pub dma: bool, + /// The device exposes one or more PCI BARs. + pub pci_bar: bool, + /// The device has a meaningful reset operation. + pub reset: bool, + /// The device has a meaningful suspend operation. + pub suspend: bool, + /// The device has a meaningful resume operation. + pub resume: bool, +} + +impl DeviceCapabilities { + /// No optional capabilities. + pub const NONE: Self = Self { + msi: false, + msix: false, + dma: false, + pci_bar: false, + reset: false, + suspend: false, + resume: false, + }; + + /// Returns an empty capability set. + pub const fn none() -> Self { + Self::NONE + } +} diff --git a/virtualization/axdevice/tests/test.rs b/virtualization/axdevice/tests/test.rs index ea225d200c..5e858c58cf 100644 --- a/virtualization/axdevice/tests/test.rs +++ b/virtualization/axdevice/tests/test.rs @@ -12,12 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::{Arc, Mutex}; +use std::{ + rc::Rc, + sync::{Arc, Mutex}, +}; use ax_errno::AxResult; use ax_memory_addr::{PhysAddr, VirtAddr}; -use axdevice::{AxVmDeviceConfig, AxVmDevices}; -use axdevice_base::{AccessWidth, BaseDeviceOps}; +use axdevice::{ + AxVmDeviceConfig, AxVmDevices, BusAccess, BusAddress, BusKind, BusOp, BusResponse, + DeviceCapabilities, DeviceError, DeviceId, DeviceOps, DeviceRegistry, IrqLine, IrqSink, + IrqTarget, LegacyDeviceAdapter, MsiMessage, PciBarKind, Resource, +}; +use axdevice_base::{AccessWidth, BaseDeviceOps, Port, PortRange, SysRegAddr, SysRegAddrRange}; use axvm_types::{ EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange, InterruptVector, VCpuId, VMId, }; @@ -181,3 +188,635 @@ impl X86VlapicHostIf for MockX86VlapicHostIfImpl { Ok(()) } } + +#[derive(Debug)] +struct AbstractMockDevice { + id: DeviceId, + name: &'static str, + resources: Vec, + capabilities: DeviceCapabilities, +} + +impl DeviceOps for AbstractMockDevice { + fn id(&self) -> DeviceId { + self.id + } + + fn name(&self) -> &str { + self.name + } + + fn resources(&self) -> &[Resource] { + &self.resources + } + + fn capabilities(&self) -> DeviceCapabilities { + self.capabilities + } + + fn access(&self, access: BusAccess) -> Result { + match access.op { + BusOp::Read => Ok(BusResponse::Read { value: 0x55aa }), + BusOp::Write { .. } => Ok(BusResponse::Write), + } + } +} + +#[test] +fn device_ops_exposes_identity_resources_and_capabilities() { + let mmio_range = GuestPhysAddrRange::new( + GuestPhysAddr::from(0x1000_0000), + GuestPhysAddr::from(0x1000_1000), + ); + let device = AbstractMockDevice { + id: DeviceId::new(7), + name: "mock-net", + resources: vec![ + Resource::Mmio(mmio_range), + Resource::Irq(IrqLine::new(32)), + Resource::Msi { vectors: 4 }, + Resource::Dma, + Resource::PciBar { + index: 0, + kind: PciBarKind::Mmio64 { + prefetchable: false, + }, + }, + ], + capabilities: DeviceCapabilities { + msi: true, + msix: false, + dma: true, + pci_bar: true, + reset: true, + suspend: false, + resume: false, + }, + }; + + assert_eq!(device.id().raw(), 7); + assert_eq!(device.name(), "mock-net"); + assert!(matches!(device.resources()[0], Resource::Mmio(range) if range == mmio_range)); + assert!(matches!(device.resources()[1], Resource::Irq(line) if line.number() == 32)); + assert!(device.capabilities().msi); + assert!(device.capabilities().dma); + assert!(device.reset().is_ok()); + assert!(device.suspend().is_ok()); + assert!(device.resume().is_ok()); +} + +#[test] +fn bus_access_constructors_cover_all_current_exit_buses() { + let mmio_addr = GuestPhysAddr::from(0x2000_0000); + let mmio_read = BusAccess::mmio_read(mmio_addr, AccessWidth::Dword); + assert_eq!(mmio_read.kind, BusKind::Mmio); + assert_eq!(mmio_read.addr, BusAddress::Mmio(mmio_addr)); + assert_eq!(mmio_read.addr.kind(), BusKind::Mmio); + assert_eq!(mmio_read.op, BusOp::Read); + + let mmio_write = BusAccess::mmio_write(mmio_addr, AccessWidth::Qword, 0x1234); + assert!(matches!(mmio_write.op, BusOp::Write { value: 0x1234 })); + + let pio_read = BusAccess::pio_read(axdevice_base::Port::new(0x3f8), AccessWidth::Byte); + assert_eq!(pio_read.kind, BusKind::Pio); + assert_eq!(pio_read.addr.kind(), BusKind::Pio); + + let pio_write = BusAccess::pio_write(axdevice_base::Port::new(0x40), AccessWidth::Word, 0x12); + assert!(matches!(pio_write.op, BusOp::Write { value: 0x12 })); + + let sysreg = axdevice_base::SysRegAddr::new(0x10); + let sysreg_read = BusAccess::sysreg_read(sysreg, AccessWidth::Qword); + assert_eq!(sysreg_read.kind, BusKind::SysReg); + assert_eq!(sysreg_read.addr, BusAddress::SysReg(sysreg)); + + let sysreg_write = BusAccess::sysreg_write(sysreg, AccessWidth::Qword, 0x88); + assert!(matches!(sysreg_write.op, BusOp::Write { value: 0x88 })); +} + +#[test] +fn resources_cover_mmio_pio_sysreg_irq_msi_dma_and_pci_bar() { + let resources = [ + Resource::Mmio(GuestPhysAddrRange::new( + GuestPhysAddr::from(0x3000_0000), + GuestPhysAddr::from(0x3000_1000), + )), + Resource::Pio(axdevice_base::PortRange::new( + axdevice_base::Port::new(0x40), + axdevice_base::Port::new(0x43), + )), + Resource::SysReg(axdevice_base::SysRegAddrRange::new( + axdevice_base::SysRegAddr::new(0x20), + axdevice_base::SysRegAddr::new(0x23), + )), + Resource::Irq(IrqLine::new(4)), + Resource::Msi { vectors: 8 }, + Resource::Dma, + Resource::PciBar { + index: 1, + kind: PciBarKind::Pio, + }, + ]; + + assert!(matches!(resources[0], Resource::Mmio(_))); + assert!(matches!(resources[1], Resource::Pio(_))); + assert!(matches!(resources[2], Resource::SysReg(_))); + assert!(matches!(resources[3], Resource::Irq(line) if line.number() == 4)); + assert!(matches!(resources[4], Resource::Msi { vectors: 8 })); + assert!(matches!(resources[5], Resource::Dma)); + assert!(matches!( + resources[6], + Resource::PciBar { + index: 1, + kind: PciBarKind::Pio + } + )); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum IrqEvent { + Raise(IrqLine), + Lower(IrqLine), + Pulse(IrqLine), + Msi(MsiMessage), + Eoi(IrqLine), +} + +#[derive(Default)] +struct RecordingIrqSink { + events: Mutex>, +} + +impl RecordingIrqSink { + fn events(&self) -> Vec { + self.events.lock().unwrap().clone() + } +} + +impl IrqSink for RecordingIrqSink { + fn raise(&self, line: IrqLine) -> Result<(), DeviceError> { + self.events.lock().unwrap().push(IrqEvent::Raise(line)); + Ok(()) + } + + fn lower(&self, line: IrqLine) -> Result<(), DeviceError> { + self.events.lock().unwrap().push(IrqEvent::Lower(line)); + Ok(()) + } + + fn pulse(&self, line: IrqLine) -> Result<(), DeviceError> { + self.events.lock().unwrap().push(IrqEvent::Pulse(line)); + Ok(()) + } + + fn msi(&self, message: MsiMessage) -> Result<(), DeviceError> { + self.events.lock().unwrap().push(IrqEvent::Msi(message)); + Ok(()) + } + + fn eoi(&self, line: IrqLine) -> Result<(), DeviceError> { + self.events.lock().unwrap().push(IrqEvent::Eoi(line)); + Ok(()) + } +} + +#[test] +fn irq_sink_records_semantic_interrupt_operations() { + let sink = RecordingIrqSink::default(); + let line = IrqLine::new(9); + let msi = MsiMessage::with_target(0xfee0_0000, 0x45, IrqTarget::Vcpu(0)); + + sink.raise(line).unwrap(); + sink.lower(line).unwrap(); + sink.pulse(line).unwrap(); + sink.msi(msi).unwrap(); + sink.eoi(line).unwrap(); + + assert_eq!( + sink.events(), + vec![ + IrqEvent::Raise(line), + IrqEvent::Lower(line), + IrqEvent::Pulse(line), + IrqEvent::Msi(msi), + IrqEvent::Eoi(line), + ] + ); +} + +#[test] +fn device_error_variants_describe_access_failures() { + let width_error = DeviceError::InvalidAccessWidth { + width: AccessWidth::Qword, + }; + assert!(matches!( + width_error, + DeviceError::InvalidAccessWidth { .. } + )); + + let unsupported = DeviceError::UnsupportedOperation; + assert_eq!(unsupported.to_string(), "unsupported device operation"); + + let backend: DeviceError = ax_errno::AxError::Unsupported.into(); + assert!(matches!(backend, DeviceError::Backend(_))); +} + +struct MockPortDevice { + range: PortRange, + last_write: Mutex>, +} + +impl MockPortDevice { + fn new(start: u16, end: u16) -> Self { + Self { + range: PortRange::new(Port::new(start), Port::new(end)), + last_write: Mutex::new(None), + } + } + + fn get_last_write(&self) -> Option<(u16, usize)> { + *self.last_write.lock().unwrap() + } +} + +impl BaseDeviceOps for MockPortDevice { + fn emu_type(&self) -> EmulatedDeviceType { + EmulatedDeviceType::Console + } + + fn address_range(&self) -> PortRange { + self.range + } + + fn handle_read(&self, _addr: Port, _width: AccessWidth) -> AxResult { + Ok(0x44) + } + + fn handle_write(&self, port: Port, _width: AccessWidth, val: usize) -> AxResult { + *self.last_write.lock().unwrap() = Some((port.number(), val)); + Ok(()) + } +} + +struct MockSysRegDevice { + range: SysRegAddrRange, + last_write: Mutex>, +} + +impl MockSysRegDevice { + fn new(start: usize, end: usize) -> Self { + Self { + range: SysRegAddrRange::new(SysRegAddr::new(start), SysRegAddr::new(end)), + last_write: Mutex::new(None), + } + } + + fn get_last_write(&self) -> Option<(usize, usize)> { + *self.last_write.lock().unwrap() + } +} + +impl BaseDeviceOps for MockSysRegDevice { + fn emu_type(&self) -> EmulatedDeviceType { + EmulatedDeviceType::InterruptController + } + + fn address_range(&self) -> SysRegAddrRange { + self.range + } + + fn handle_read(&self, _addr: SysRegAddr, _width: AccessWidth) -> AxResult { + Ok(0x99) + } + + fn handle_write(&self, addr: SysRegAddr, _width: AccessWidth, val: usize) -> AxResult { + *self.last_write.lock().unwrap() = Some((addr.addr(), val)); + Ok(()) + } +} + +fn mmio_resource(start: usize, end: usize) -> Resource { + Resource::Mmio(GuestPhysAddrRange::new( + GuestPhysAddr::from(start), + GuestPhysAddr::from(end), + )) +} + +fn mock_device(id: usize, resources: Vec) -> Rc { + Rc::new(AbstractMockDevice { + id: DeviceId::new(id), + name: "registry-mock", + resources, + capabilities: DeviceCapabilities::none(), + }) +} + +#[test] +fn registry_registers_routes_and_dispatches_mock_devices() { + let mut registry = DeviceRegistry::new(); + registry + .register_device(mock_device( + 1, + vec![mmio_resource(0x4000_0000, 0x4000_1000)], + )) + .unwrap(); + registry + .register_device(mock_device( + 2, + vec![Resource::Pio(PortRange::new( + Port::new(0x80), + Port::new(0x8f), + ))], + )) + .unwrap(); + registry + .register_device(mock_device( + 3, + vec![Resource::SysReg(SysRegAddrRange::new( + SysRegAddr::new(0x100), + SysRegAddr::new(0x10f), + ))], + )) + .unwrap(); + registry + .register_device(mock_device( + 4, + vec![Resource::Irq(IrqLine::new(10)), Resource::Dma], + )) + .unwrap(); + + assert_eq!(registry.device_count(), 4); + assert_eq!(registry.mmio_route_count(), 1); + assert_eq!(registry.pio_route_count(), 1); + assert_eq!(registry.sysreg_route_count(), 1); + assert!(registry.find_device(DeviceId::new(3)).is_some()); + + let mmio = registry + .dispatch(BusAccess::mmio_read( + GuestPhysAddr::from(0x4000_0004), + AccessWidth::Dword, + )) + .unwrap(); + assert_eq!(mmio, BusResponse::Read { value: 0x55aa }); + + let pio = registry + .dispatch(BusAccess::pio_write( + Port::new(0x81), + AccessWidth::Byte, + 0xaa, + )) + .unwrap(); + assert_eq!(pio, BusResponse::Write); + + let sysreg = registry + .dispatch(BusAccess::sysreg_read( + SysRegAddr::new(0x101), + AccessWidth::Qword, + )) + .unwrap(); + assert_eq!(sysreg, BusResponse::Read { value: 0x55aa }); +} + +#[test] +fn registry_rejects_duplicate_device_ids() { + let mut registry = DeviceRegistry::new(); + registry + .register_device(mock_device( + 1, + vec![mmio_resource(0x5000_0000, 0x5000_1000)], + )) + .unwrap(); + + let err = registry + .register_device(mock_device( + 1, + vec![mmio_resource(0x5000_1000, 0x5000_2000)], + )) + .unwrap_err(); + assert!(matches!( + err, + DeviceError::DuplicateDeviceId { id } if id == DeviceId::new(1) + )); +} + +#[test] +fn registry_rejects_overlapping_bus_resources() { + let mut registry = DeviceRegistry::new(); + registry + .register_device(mock_device( + 1, + vec![mmio_resource(0x6000_0000, 0x6000_1000)], + )) + .unwrap(); + registry + .register_device(mock_device( + 2, + vec![mmio_resource(0x6000_1000, 0x6000_2000)], + )) + .unwrap(); + + let err = registry + .register_device(mock_device( + 3, + vec![mmio_resource(0x6000_0800, 0x6000_1800)], + )) + .unwrap_err(); + assert!(matches!(err, DeviceError::ResourceConflict { .. })); + + registry + .register_device(mock_device( + 4, + vec![Resource::Pio(PortRange::new( + Port::new(0x200), + Port::new(0x20f), + ))], + )) + .unwrap(); + let err = registry + .register_device(mock_device( + 5, + vec![Resource::Pio(PortRange::new( + Port::new(0x20f), + Port::new(0x210), + ))], + )) + .unwrap_err(); + assert!(matches!(err, DeviceError::ResourceConflict { .. })); + + registry + .register_device(mock_device( + 6, + vec![Resource::SysReg(SysRegAddrRange::new( + SysRegAddr::new(0x300), + SysRegAddr::new(0x30f), + ))], + )) + .unwrap(); + let err = registry + .register_device(mock_device( + 7, + vec![Resource::SysReg(SysRegAddrRange::new( + SysRegAddr::new(0x30f), + SysRegAddr::new(0x310), + ))], + )) + .unwrap_err(); + assert!(matches!(err, DeviceError::ResourceConflict { .. })); +} + +#[test] +fn registry_reports_misses_and_bus_address_mismatch_without_panicking() { + let mut registry = DeviceRegistry::new(); + registry + .register_device(mock_device( + 1, + vec![mmio_resource(0x7000_0000, 0x7000_1000)], + )) + .unwrap(); + + let miss = registry + .dispatch(BusAccess::mmio_read( + GuestPhysAddr::from(0x7000_2000), + AccessWidth::Dword, + )) + .unwrap_err(); + assert!(matches!(miss, DeviceError::DeviceNotFound { .. })); + + let mismatch = registry + .dispatch(BusAccess::new( + BusKind::Mmio, + BusAddress::Pio(Port::new(0x70)), + AccessWidth::Byte, + BusOp::Read, + )) + .unwrap_err(); + assert!(matches!(mismatch, DeviceError::BusAddressMismatch { .. })); +} + +#[test] +fn legacy_device_adapter_forwards_to_existing_base_device_ops() { + let mmio = Arc::new(MockMmioDevice::new("legacy-mmio", 0x8000_0000, 0x1000)); + let mmio_adapter = LegacyDeviceAdapter::mmio( + DeviceId::new(1), + "legacy-mmio".into(), + vec![Resource::Mmio(mmio.address_range())], + DeviceCapabilities::none(), + mmio.clone(), + ); + assert_eq!( + mmio_adapter + .access(BusAccess::mmio_read( + GuestPhysAddr::from(0x8000_0000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0xDEAD_BEEF } + ); + mmio_adapter + .access(BusAccess::mmio_write( + GuestPhysAddr::from(0x8000_0040), + AccessWidth::Dword, + 0xbeef, + )) + .unwrap(); + assert_eq!(mmio.get_last_write(), Some((0x40, 0xbeef))); + + let pio = Arc::new(MockPortDevice::new(0x90, 0x9f)); + let pio_adapter = LegacyDeviceAdapter::pio( + DeviceId::new(2), + "legacy-pio".into(), + vec![Resource::Pio(pio.address_range())], + DeviceCapabilities::none(), + pio.clone(), + ); + assert_eq!( + pio_adapter + .access(BusAccess::pio_read(Port::new(0x90), AccessWidth::Byte)) + .unwrap(), + BusResponse::Read { value: 0x44 } + ); + pio_adapter + .access(BusAccess::pio_write( + Port::new(0x91), + AccessWidth::Byte, + 0x33, + )) + .unwrap(); + assert_eq!(pio.get_last_write(), Some((0x91, 0x33))); + + let sysreg = Arc::new(MockSysRegDevice::new(0x400, 0x40f)); + let sysreg_adapter = LegacyDeviceAdapter::sysreg( + DeviceId::new(3), + "legacy-sysreg".into(), + vec![Resource::SysReg(sysreg.address_range())], + DeviceCapabilities::none(), + sysreg.clone(), + ); + assert_eq!( + sysreg_adapter + .access(BusAccess::sysreg_read( + SysRegAddr::new(0x400), + AccessWidth::Qword + )) + .unwrap(), + BusResponse::Read { value: 0x99 } + ); + sysreg_adapter + .access(BusAccess::sysreg_write( + SysRegAddr::new(0x401), + AccessWidth::Qword, + 0x77, + )) + .unwrap(); + assert_eq!(sysreg.get_last_write(), Some((0x401, 0x77))); + + let mismatch = sysreg_adapter + .access(BusAccess::pio_read(Port::new(0x90), AccessWidth::Byte)) + .unwrap_err(); + assert!(matches!(mismatch, DeviceError::BusAddressMismatch { .. })); +} + +#[test] +fn ax_vm_devices_dispatch_bus_access_uses_new_registry_without_breaking_old_path() { + let config = AxVmDeviceConfig::new(vec![]); + let mut devices = AxVmDevices::new(config); + let mmio = Arc::new(MockMmioDevice::new("dual-mmio", 0x9000_0000, 0x1000)); + devices.add_mmio_dev(mmio.clone()); + + assert_eq!( + devices + .handle_mmio_read(GuestPhysAddr::from(0x9000_0000), AccessWidth::Dword) + .unwrap(), + 0xDEAD_BEEF + ); + assert_eq!( + devices + .dispatch_bus_access(BusAccess::mmio_read( + GuestPhysAddr::from(0x9000_0000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0xDEAD_BEEF } + ); + + let pio = Arc::new(MockPortDevice::new(0xa0, 0xaf)); + devices.add_port_dev(pio.clone()); + devices + .dispatch_bus_access(BusAccess::pio_write( + Port::new(0xa1), + AccessWidth::Byte, + 0x55, + )) + .unwrap(); + assert_eq!(pio.get_last_write(), Some((0xa1, 0x55))); + + let sysreg = Arc::new(MockSysRegDevice::new(0x500, 0x50f)); + devices.add_sys_reg_dev(sysreg.clone()); + devices + .dispatch_bus_access(BusAccess::sysreg_write( + SysRegAddr::new(0x501), + AccessWidth::Qword, + 0x66, + )) + .unwrap(); + assert_eq!(sysreg.get_last_write(), Some((0x501, 0x66))); +} diff --git a/virtualization/axvm/src/vm.rs b/virtualization/axvm/src/vm.rs index 4d8786bd8a..b0eb09ed7e 100644 --- a/virtualization/axvm/src/vm.rs +++ b/virtualization/axvm/src/vm.rs @@ -20,7 +20,7 @@ use ax_errno::{AxError, AxResult, ax_err, ax_err_type}; use ax_kspin::SpinNoIrq as Mutex; use ax_memory_addr::{align_down_4k, align_up_4k}; use axaddrspace::{AddrSpace, MappingFlags}; -use axdevice::{AxVmDeviceConfig, AxVmDevices}; +use axdevice::{AxVmDeviceConfig, AxVmDevices, BusAccess, BusResponse, DeviceError}; use axdevice_base::AccessWidth; use axvcpu::{AxVCpu, AxVCpuExitReason}; #[cfg(target_arch = "x86_64")] @@ -68,6 +68,56 @@ fn sign_extend_value(value: usize, width: AccessWidth) -> usize { } } +fn device_error_to_ax_error(error: DeviceError) -> AxError { + match error { + DeviceError::Backend(error) => error, + DeviceError::DeviceNotFound { .. } => { + ax_err_type!(NotFound, format!("device dispatch failed: {error}")) + } + DeviceError::InvalidAccessWidth { .. } + | DeviceError::BusAddressMismatch { .. } + | DeviceError::AddressOutOfRange { .. } => { + ax_err_type!(InvalidInput, format!("device dispatch failed: {error}")) + } + DeviceError::UnsupportedOperation => { + ax_err_type!(Unsupported, format!("device dispatch failed: {error}")) + } + DeviceError::ReadOnly { .. } | DeviceError::WriteOnly { .. } => { + ax_err_type!(PermissionDenied, format!("device dispatch failed: {error}")) + } + DeviceError::DuplicateDeviceId { .. } | DeviceError::ResourceConflict { .. } => { + ax_err_type!(BadState, format!("device dispatch failed: {error}")) + } + } +} + +fn unexpected_device_response(access: BusAccess, response: BusResponse) -> AxError { + ax_err_type!( + BadState, + format!("unexpected device response {response:?} for access {access:?}") + ) +} + +fn dispatch_device_read(devices: &AxVmDevices, access: BusAccess) -> AxResult { + match devices + .dispatch_bus_access(access) + .map_err(device_error_to_ax_error)? + { + BusResponse::Read { value } => Ok(value), + response => Err(unexpected_device_response(access, response)), + } +} + +fn dispatch_device_write(devices: &AxVmDevices, access: BusAccess) -> AxResult { + match devices + .dispatch_bus_access(access) + .map_err(device_error_to_ax_error)? + { + BusResponse::Write => Ok(()), + response => Err(unexpected_device_response(access, response)), + } +} + struct AxVMInnerConst { phys_cpu_ls: PhysCpuList, vcpu_list: Box<[AxVCpuRef]>, @@ -586,7 +636,10 @@ impl AxVM { reg_width, signed_ext, } => { - let raw = self.get_devices().handle_mmio_read(addr, width)?; + let raw = dispatch_device_read( + self.get_devices(), + BusAccess::mmio_read(addr, width), + )?; let masked = raw & width_mask(width); let val = if signed_ext { sign_extend_value(masked, width) @@ -596,11 +649,16 @@ impl AxVM { vcpu.set_gpr(reg, val); } AxVCpuExitReason::MmioWrite { addr, width, data } => { - self.get_devices() - .handle_mmio_write(addr, width, data as usize)?; + dispatch_device_write( + self.get_devices(), + BusAccess::mmio_write(addr, width, data as usize), + )?; } AxVCpuExitReason::IoRead { port, width } => { - let val = self.get_devices().handle_port_read(port, width)?; + let val = dispatch_device_read( + self.get_devices(), + BusAccess::pio_read(port, width), + )?; #[cfg(not(target_arch = "riscv64"))] vcpu.set_gpr(0, val); // The target is always eax/ax/al, todo: handle access_width correctly @@ -608,23 +666,27 @@ impl AxVM { vcpu.set_gpr(riscv_vcpu::GprIndex::A0 as usize, val); } AxVCpuExitReason::IoWrite { port, width, data } => { - self.get_devices() - .handle_port_write(port, width, data as usize)?; + dispatch_device_write( + self.get_devices(), + BusAccess::pio_write(port, width, data as usize), + )?; } AxVCpuExitReason::SysRegRead { addr, reg } => { - let val = self.get_devices().handle_sys_reg_read( - addr, - // Generally speaking, the width of system register is fixed and needless to be specified. - // AccessWidth::Qword here is just a placeholder, may be changed in the future. - AccessWidth::Qword, + let val = dispatch_device_read( + self.get_devices(), + BusAccess::sysreg_read( + addr, + // Generally speaking, the width of system register is fixed and needless to be specified. + // AccessWidth::Qword here is just a placeholder, may be changed in the future. + AccessWidth::Qword, + ), )?; vcpu.set_gpr(reg, val); } AxVCpuExitReason::SysRegWrite { addr, value } => { - self.get_devices().handle_sys_reg_write( - addr, - AccessWidth::Qword, - value as usize, + dispatch_device_write( + self.get_devices(), + BusAccess::sysreg_write(addr, AccessWidth::Qword, value as usize), )?; } AxVCpuExitReason::NestedPageFault { addr, access_flags } => { From 8df2316a3ed7e00574fe61c52f95d8ba6a73c6ee Mon Sep 17 00:00:00 2001 From: baitwo02 Date: Sat, 20 Jun 2026 04:57:28 +0000 Subject: [PATCH 2/5] feat(axdevice): add native VGicR factory path --- .../vms/qemu/aarch64/linux-smp1-gppt-gic.toml | 71 ++++ .../build-aarch64-unknown-none-softfloat.toml | 10 + .../qemu/smoke/qemu-aarch64.toml | 28 ++ virtualization/arm_vgic/src/v3/vgicr.rs | 152 +++++++- virtualization/axdevice/src/device.rs | 239 +++++++------ virtualization/axdevice/src/factory.rs | 37 ++ virtualization/axdevice/src/legacy.rs | 10 +- virtualization/axdevice/src/lib.rs | 16 +- virtualization/axdevice/src/registry.rs | 11 +- virtualization/axdevice/tests/test.rs | 332 +++++++++++++++++- .../{axdevice => axdevice_base}/src/bus.rs | 3 +- .../{axdevice => axdevice_base}/src/irq.rs | 2 +- virtualization/axdevice_base/src/lib.rs | 10 + .../{axdevice => axdevice_base}/src/model.rs | 71 +++- .../src/resource.rs | 3 +- 15 files changed, 838 insertions(+), 157 deletions(-) create mode 100644 os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml create mode 100644 test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml create mode 100644 test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml create mode 100644 virtualization/axdevice/src/factory.rs rename virtualization/{axdevice => axdevice_base}/src/bus.rs (98%) rename virtualization/{axdevice => axdevice_base}/src/irq.rs (99%) rename virtualization/{axdevice => axdevice_base}/src/model.rs (75%) rename virtualization/{axdevice => axdevice_base}/src/resource.rs (97%) diff --git a/os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml b/os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml new file mode 100644 index 0000000000..570974bc03 --- /dev/null +++ b/os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml @@ -0,0 +1,71 @@ +# Vm base info configs +# +[base] +# Guest vm id. +id = 1 +# Guest vm name. +name = "linux-qemu-gppt-gic" +# Virtualization type. +vm_type = 1 +# The number of virtual CPUs. +cpu_num = 1 +# Guest vm physical cpu sets. +phys_cpu_ids = [0] + +# +# Vm kernel configs +# +[kernel] +# The entry point of the kernel image. +entry_point = 0x8020_0000 +# The location of image: "memory" | "fs". +# Load from file system. +image_location = "fs" +# The file path of the kernel image. +kernel_path = "/guest/linux/linux-qemu" +# The load address of the kernel image. +kernel_load_addr = 0x8020_0000 +# The file path of the device tree blob (DTB). +# dtb_path = "tmp/linux-aarch64-qemu-smp1.dtb" +# The load address of the device tree blob (DTB). +dtb_load_addr = 0x8000_0000 + +# Memory regions with format (`base_paddr`, `size`, `flags`, `map_type`). +# For `map_type`, 0 means `MAP_ALLOC`, 1 means `MAP_IDENTICAL`, 2 means `MAP_RESERVED`. +memory_regions = [ + [0x8000_0000, 0x1000_0000, 0x7, 1], # System RAM 1G MAP_IDENTICAL +] + +# +# Device specifications +# +[devices] +# The interrupt mode. +interrupt_mode = "passthrough" + +# Pass-through devices. +# Name Base-Ipa Base-Pa Length Alloc-Irq. +passthrough_devices = [ + ["/"], + #["/timer"], +] + +# Passthrough addresses. +# Base-GPA Length. +passthrough_addresses = [ + #[0x28041000, 0x100_0000] +] + +# Devices that are not desired to be passed through to the guest +excluded_devices = [ + # ["/gic-v3"], +] + +# Emu_devices. +# Name Base-Ipa Ipa_len Alloc-Irq Emu-Type EmuConfig. +emu_devices = [ + # Keep this case focused on the first native DeviceOps migration target. + # ["gppt-gicd", 0x0800_0000, 0x1_0000, 0, 0x21, []], + ["gppt-gicr", 0x080a_0000, 0x2_0000, 0, 0x20, [1, 0x2_0000, 0]], # 1 vcpu, stride 0x20000, starts with pcpu 0 + # ["gppt-gits", 0x0808_0000, 0x2_0000, 0, 0x22, [0x0808_0000]], # host_gits_base +] diff --git a/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml b/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..bdc97e966e --- /dev/null +++ b/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,10 @@ +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } +features = [ + "ept-level-4", + "ax-driver/virtio-blk", + "fs", +] +log = "Info" +plat_dyn = true +target = "aarch64-unknown-none-softfloat" +vm_configs = ["os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml"] diff --git a/test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml b/test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml new file mode 100644 index 0000000000..fcd6b3f48b --- /dev/null +++ b/test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml @@ -0,0 +1,28 @@ +args = [ + "-nographic", + "-cpu", + "cortex-a72", + "-machine", + "virt,virtualization=on,gic-version=3", + "-smp", + "4", + "-device", + "virtio-blk-device,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-append", + "root=/dev/vda rw init=/bin/sh", + "-m", + "8g", +] +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)kernel panic", + "(?i)login incorrect", + "(?i)permission denied", +] +success_regex = ["(?m)^guest test pass!\\s*$"] +shell_prefix = "~ #" +shell_init_cmd = "pwd && echo 'guest test pass!'" +to_bin = true +uefi = false diff --git a/virtualization/arm_vgic/src/v3/vgicr.rs b/virtualization/arm_vgic/src/v3/vgicr.rs index c09a19dd15..cd6ab7ae3c 100644 --- a/virtualization/arm_vgic/src/v3/vgicr.rs +++ b/virtualization/arm_vgic/src/v3/vgicr.rs @@ -12,13 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::{format, rc::Rc, vec, vec::Vec}; use core::{cell::UnsafeCell, ptr}; +use ax_errno::ax_err_type; use ax_kspin::SpinNoIrq as Mutex; use ax_memory_addr::PhysAddr; -use axdevice_base::{AccessWidth, BaseDeviceOps}; -use axvm_types::{GuestPhysAddr, GuestPhysAddrRange, HostPhysAddr}; -use log::{debug, trace}; +use axdevice_base::{ + AccessWidth, BusAddress, BusKind, BusOp, BusResponse, DeviceBuildContext, DeviceCapabilities, + DeviceError, DeviceFactory, DeviceMeta, DeviceOps, DeviceResult, EmuDeviceType, Resource, +}; +use axvm_types::{EmulatedDeviceConfig, GuestPhysAddr, GuestPhysAddrRange, HostPhysAddr}; +use log::{debug, info, trace}; use spin::Once; use super::{ @@ -30,6 +35,12 @@ use crate::host; /// Default size per GICR region. pub const DEFAULT_SIZE_PER_GICR: usize = 0x20000; // 128K: 64K for SGI/PPI, then 64K for LPI +fn mmio_resources(addr: GuestPhysAddr, size: usize) -> Vec { + vec![Resource::Mmio(GuestPhysAddrRange::from_start_size( + addr, size, + ))] +} + /// Virtual GICR registers. pub struct VGicRRegs { /// LPI configuration table base address. @@ -38,6 +49,8 @@ pub struct VGicRRegs { /// Virtual GICv3 Redistributor. pub struct VGicR { + meta: DeviceMeta, + /// The address of the VGicR in the guest physical address space. pub addr: GuestPhysAddr, /// The size of the VGicR in bytes. @@ -64,12 +77,13 @@ impl VGicR { unsafe { &mut *self.regs.get() } } - /// Creates a new VGicR instance. - pub fn new(addr: GuestPhysAddr, size: Option, cpu_id: usize) -> Self { + /// Creates a new VGicR instance with native device metadata. + pub fn new(meta: DeviceMeta, addr: GuestPhysAddr, size: Option, cpu_id: usize) -> Self { let size = size.unwrap_or(DEFAULT_SIZE_PER_GICR); let host_gicr_base_this_cpu = crate::api_reexp::get_host_gicr_base() + cpu_id * size; Self { + meta, addr, size, cpu_id, @@ -77,22 +91,12 @@ impl VGicR { regs: UnsafeCell::new(VGicRRegs { propbaser: 0 }), } } -} - -impl BaseDeviceOps for VGicR { - fn emu_type(&self) -> axdevice_base::EmuDeviceType { - axdevice_base::EmuDeviceType::GPPTRedistributor - } fn address_range(&self) -> GuestPhysAddrRange { GuestPhysAddrRange::from_start_size(self.addr, self.size) } - fn handle_read( - &self, - addr: ::Addr, - width: AccessWidth, - ) -> ax_errno::AxResult { + fn handle_read(&self, addr: GuestPhysAddr, width: AccessWidth) -> ax_errno::AxResult { let gicr_base = self.host_gicr_base_this_cpu; let reg = addr - self.addr; @@ -158,7 +162,7 @@ impl BaseDeviceOps for VGicR { fn handle_write( &self, - addr: ::Addr, + addr: GuestPhysAddr, width: AccessWidth, value: usize, ) -> ax_errno::AxResult<()> { @@ -221,6 +225,120 @@ impl BaseDeviceOps for VGicR { } } +impl DeviceOps for VGicR { + fn id(&self) -> axdevice_base::DeviceId { + self.meta.id() + } + + fn name(&self) -> &str { + self.meta.name() + } + + fn resources(&self) -> &[Resource] { + self.meta.resources() + } + + fn capabilities(&self) -> DeviceCapabilities { + self.meta.capabilities() + } + + fn access(&self, access: axdevice_base::BusAccess) -> DeviceResult { + if access.kind != BusKind::Mmio { + return Err(DeviceError::BusAddressMismatch { + kind: BusKind::Mmio, + address: access.addr, + }); + } + + let BusAddress::Mmio(addr) = access.addr else { + return Err(DeviceError::BusAddressMismatch { + kind: BusKind::Mmio, + address: access.addr, + }); + }; + + if !self.address_range().contains(addr) { + return Err(DeviceError::AddressOutOfRange { + kind: BusKind::Mmio, + address: access.addr, + }); + } + + match access.op { + BusOp::Read => self + .handle_read(addr, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + BusOp::Write { value } => self + .handle_write(addr, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from), + } + } +} + +/// Factory for ARM GIC partial passthrough redistributors. +pub struct GpptRedistributorFactory; + +impl DeviceFactory for GpptRedistributorFactory { + fn ty(&self) -> EmuDeviceType { + EmuDeviceType::GPPTRedistributor + } + + fn build( + &self, + ctx: &mut dyn DeviceBuildContext, + config: &EmulatedDeviceConfig, + ) -> DeviceResult>> { + const ARG_ERR_MSG: &str = "expect 3 args for gppt redistributor (cpu_num, stride, pcpu_id)"; + + let cpu_num = config + .cfg_list + .first() + .copied() + .ok_or_else(|| DeviceError::from(ax_err_type!(InvalidInput, ARG_ERR_MSG)))?; + let stride = config + .cfg_list + .get(1) + .copied() + .ok_or_else(|| DeviceError::from(ax_err_type!(InvalidInput, ARG_ERR_MSG)))?; + let pcpu_id = config + .cfg_list + .get(2) + .copied() + .ok_or_else(|| DeviceError::from(ax_err_type!(InvalidInput, ARG_ERR_MSG)))?; + + let mut devices: Vec> = Vec::new(); + for i in 0..cpu_num { + let addr = GuestPhysAddr::from(config.base_gpa + i * stride); + let size = config.length; + let name = if config.name.is_empty() { + format!("gppt-redistributor-{i}") + } else { + format!("{}-{i}", config.name) + }; + let meta = DeviceMeta::new( + ctx.alloc_device_id(), + name, + mmio_resources(addr, size), + DeviceCapabilities::none(), + ); + let device: Rc = + Rc::new(VGicR::new(meta, addr, Some(size), pcpu_id + i)); + devices.push(device); + + info!( + "GPPT Redistributor factory built native VGicR for vCPU {i} with base GPA {:#x} \ + and length {:#x}", + addr.as_usize(), + size + ); + } + + Ok(devices) + } +} + // todo: move the lpi prop table to arm-gic-driver, and find a good interface to use it. /// LPI property table for managing Locality-specific Peripheral Interrupts. pub struct LpiPropTable { diff --git a/virtualization/axdevice/src/device.rs b/virtualization/axdevice/src/device.rs index 4c8131aa4d..4c0376a489 100644 --- a/virtualization/axdevice/src/device.rs +++ b/virtualization/axdevice/src/device.rs @@ -17,7 +17,7 @@ use core::ops::Range; #[cfg(target_arch = "aarch64")] use arm_vgic::Vgic; -use ax_errno::{AxResult, ax_err}; +use ax_errno::{AxError, AxResult, ax_err, ax_err_type}; use ax_kspin::SpinNoIrq as Mutex; #[cfg(target_arch = "aarch64")] use ax_memory_addr::PhysAddr; @@ -33,8 +33,9 @@ use riscv_vplic::VPlicGlobal; use x86_vlapic::{EmulatedIoApic, EmulatedPit, EmulatedSerialPort, IoApicInterrupt}; use crate::{ - AxVmDeviceConfig, BusAccess, BusResponse, DeviceCapabilities, DeviceId, DeviceRegistry, - DeviceResult, LegacyDeviceAdapter, Resource, range_alloc::RangeAllocator, + AxVmDeviceConfig, BusAccess, BusResponse, DeviceBuildContext, DeviceCapabilities, DeviceError, + DeviceId, DeviceOps, DeviceRegistry, DeviceResult, LegacyDeviceAdapter, Resource, + range_alloc::RangeAllocator, }; /// A set of emulated device types that can be accessed by a specific address range type. @@ -108,30 +109,101 @@ pub struct AxVmDevices { ivc_channel: Option>, } -#[inline] -fn log_device_io( - addr_type: &'static str, - addr: impl core::fmt::LowerHex, - addr_range: impl core::fmt::LowerHex, - read: bool, - width: AccessWidth, -) { - let rw = if read { "read" } else { "write" }; - trace!("emu_device {rw}: {addr_type} {addr:#x} in range {addr_range:#x} with width {width:?}") +fn device_error_to_ax_error(error: DeviceError) -> AxError { + match error { + DeviceError::Backend(error) => error, + DeviceError::DeviceNotFound { .. } => { + ax_err_type!(NotFound, format!("device dispatch failed: {error}")) + } + DeviceError::InvalidAccessWidth { .. } + | DeviceError::BusAddressMismatch { .. } + | DeviceError::AddressOutOfRange { .. } => { + ax_err_type!(InvalidInput, format!("device dispatch failed: {error}")) + } + DeviceError::UnsupportedOperation => { + ax_err_type!(Unsupported, format!("device dispatch failed: {error}")) + } + DeviceError::ReadOnly { .. } | DeviceError::WriteOnly { .. } => { + ax_err_type!(PermissionDenied, format!("device dispatch failed: {error}")) + } + DeviceError::DuplicateDeviceId { .. } | DeviceError::ResourceConflict { .. } => { + ax_err_type!(BadState, format!("device dispatch failed: {error}")) + } + } +} + +fn unexpected_device_response(access: BusAccess, response: BusResponse) -> AxError { + ax_err_type!( + BadState, + format!("unexpected device response {response:?} for access {access:?}") + ) } -#[inline] -fn panic_device_not_found( - addr_type: &'static str, - addr: impl core::fmt::LowerHex, - read: bool, - width: AccessWidth, -) -> ! { - let rw = if read { "read" } else { "write" }; - error!( - "emu_device {rw} failed: device not found for {addr_type} {addr:#x} with width {width:?}" +fn dispatch_device_read(devices: &AxVmDevices, access: BusAccess) -> AxResult { + trace!("emu_device read: {access:?}"); + + match devices + .dispatch_bus_access(access) + .map_err(device_error_to_ax_error)? + { + BusResponse::Read { value } => Ok(value), + response => Err(unexpected_device_response(access, response)), + } +} + +fn dispatch_device_write(devices: &AxVmDevices, access: BusAccess) -> AxResult { + trace!("emu_device write: {access:?}"); + + match devices + .dispatch_bus_access(access) + .map_err(device_error_to_ax_error)? + { + BusResponse::Write => Ok(()), + response => Err(unexpected_device_response(access, response)), + } +} + +impl DeviceBuildContext for AxVmDevices { + fn alloc_device_id(&mut self) -> DeviceId { + Self::alloc_device_id(self) + } +} + +#[cfg(target_arch = "aarch64")] +fn init_from_aarch64_catalog( + devices: &mut AxVmDevices, + config: &EmulatedDeviceConfig, +) -> DeviceResult { + let gppt_redistributor_factory = arm_vgic::v3::vgicr::GpptRedistributorFactory; + let factories: [&dyn crate::DeviceFactory; 1] = [&gppt_redistributor_factory]; + let catalog = crate::DeviceFactoryCatalog::new(&factories); + + let Some(factory) = catalog.find(config.emu_type) else { + return Ok(false); + }; + + info!( + "aarch64 device factory matched: type={:?}, name={}, base_gpa={:#x}, length={:#x}", + config.emu_type, config.name, config.base_gpa, config.length ); - panic!("emu_device not found"); + + let built_devices = factory.build(devices, config)?; + let built_count = built_devices.len(); + for device in &built_devices { + info!( + "aarch64 device factory built native device: id={:?}, name={}, resources={:?}", + device.id(), + device.name(), + device.resources() + ); + } + + devices.register_factory_devices(built_devices)?; + info!( + "aarch64 device factory registered {built_count} native device(s) for type {:?}", + config.emu_type + ); + Ok(true) } /// The implemention for AxVmDevices @@ -160,6 +232,18 @@ impl AxVmDevices { /// According the emu_configs to init every specific device fn init(this: &mut Self, emu_configs: &Vec) { for config in emu_configs { + #[cfg(target_arch = "aarch64")] + match init_from_aarch64_catalog(this, config) { + Ok(true) => continue, + Ok(false) => {} + Err(err) => { + panic!( + "failed to initialize emulated device {} ({:?}): {err}", + config.name, config.emu_type + ); + } + } + match config.emu_type { EmulatedDeviceType::InterruptController => { #[cfg(target_arch = "aarch64")] @@ -175,50 +259,10 @@ impl AxVmDevices { } } EmulatedDeviceType::GPPTRedistributor => { - #[cfg(target_arch = "aarch64")] - { - const GPPT_GICR_ARG_ERR_MSG: &str = - "expect 3 args for gppt redistributor (cpu_num, stride, pcpu_id)"; - - let cpu_num = config - .cfg_list - .first() - .copied() - .expect(GPPT_GICR_ARG_ERR_MSG); - let stride = config - .cfg_list - .get(1) - .copied() - .expect(GPPT_GICR_ARG_ERR_MSG); - let pcpu_id = config - .cfg_list - .get(2) - .copied() - .expect(GPPT_GICR_ARG_ERR_MSG); - - for i in 0..cpu_num { - let addr = config.base_gpa + i * stride; - let size = config.length; - #[allow(clippy::arc_with_non_send_sync)] - this.add_mmio_dev(Arc::new(arm_vgic::v3::vgicr::VGicR::new( - addr.into(), - Some(size), - pcpu_id + i, - ))); - - info!( - "GPPT Redistributor initialized for vCPU {i} with base GPA \ - {addr:#x} and length {size:#x}" - ); - } - } - #[cfg(not(target_arch = "aarch64"))] - { - warn!( - "emu type: {} is not supported on this platform", - config.emu_type - ); - } + warn!( + "emu type: {} is not supported by the active device factory catalog", + config.emu_type + ); } EmulatedDeviceType::GPPTDistributor => { #[cfg(target_arch = "aarch64")] @@ -447,6 +491,23 @@ impl AxVmDevices { } } + /// Registers a native device directly in the device registry. + /// + /// Unlike the legacy `add_*_dev` helpers, this does not add the device to + /// the old MMIO/PIO/SysReg lists. The device must declare its own bus + /// resources through [`DeviceOps::resources`]. + pub fn register_device(&mut self, device: Rc) -> DeviceResult { + self.registry.register_device(device) + } + + #[cfg(target_arch = "aarch64")] + fn register_factory_devices(&mut self, devices: Vec>) -> DeviceResult { + for device in devices { + self.register_device(device)?; + } + Ok(()) + } + /// Add a MMIO device to the device list. pub fn add_mmio_dev(&mut self, dev: Arc) { let id = self.alloc_device_id(); @@ -589,12 +650,7 @@ impl AxVmDevices { /// Handle the MMIO read by GuestPhysAddr and data width, return the value of the guest want to read pub fn handle_mmio_read(&self, addr: GuestPhysAddr, width: AccessWidth) -> AxResult { - if let Some(emu_dev) = self.find_mmio_dev(addr) { - log_device_io("mmio", addr, emu_dev.address_range(), true, width); - - return emu_dev.handle_read(addr, width); - } - panic_device_not_found("mmio", addr, true, width); + dispatch_device_read(self, BusAccess::mmio_read(addr, width)) } /// Handle the MMIO write by GuestPhysAddr, data width and the value need to write, call specific device to write the value @@ -604,22 +660,12 @@ impl AxVmDevices { width: AccessWidth, val: usize, ) -> AxResult { - if let Some(emu_dev) = self.find_mmio_dev(addr) { - log_device_io("mmio", addr, emu_dev.address_range(), false, width); - - return emu_dev.handle_write(addr, width, val); - } - panic_device_not_found("mmio", addr, false, width); + dispatch_device_write(self, BusAccess::mmio_write(addr, width, val)) } /// Handle the system register read by SysRegAddr and data width, return the value of the guest want to read pub fn handle_sys_reg_read(&self, addr: SysRegAddr, width: AccessWidth) -> AxResult { - if let Some(emu_dev) = self.find_sys_reg_dev(addr) { - log_device_io("sys_reg", addr.0, emu_dev.address_range(), true, width); - - return emu_dev.handle_read(addr, width); - } - panic_device_not_found("sys_reg", addr, true, width); + dispatch_device_read(self, BusAccess::sysreg_read(addr, width)) } /// Handle the system register write by SysRegAddr, data width and the value need to write, call specific device to write the value @@ -629,31 +675,16 @@ impl AxVmDevices { width: AccessWidth, val: usize, ) -> AxResult { - if let Some(emu_dev) = self.find_sys_reg_dev(addr) { - log_device_io("sys_reg", addr.0, emu_dev.address_range(), false, width); - - return emu_dev.handle_write(addr, width, val); - } - panic_device_not_found("sys_reg", addr, false, width); + dispatch_device_write(self, BusAccess::sysreg_write(addr, width, val)) } /// Handle the port read by port number and data width, return the value of the guest want to read pub fn handle_port_read(&self, port: Port, width: AccessWidth) -> AxResult { - if let Some(emu_dev) = self.find_port_dev(port) { - log_device_io("port", port.0, emu_dev.address_range(), true, width); - - return emu_dev.handle_read(port, width); - } - panic_device_not_found("port", port, true, width); + dispatch_device_read(self, BusAccess::pio_read(port, width)) } /// Handle the port write by port number, data width and the value need to write, call specific device to write the value pub fn handle_port_write(&self, port: Port, width: AccessWidth, val: usize) -> AxResult { - if let Some(emu_dev) = self.find_port_dev(port) { - log_device_io("port", port.0, emu_dev.address_range(), false, width); - - return emu_dev.handle_write(port, width, val); - } - panic_device_not_found("port", port, false, width); + dispatch_device_write(self, BusAccess::pio_write(port, width, val)) } } diff --git a/virtualization/axdevice/src/factory.rs b/virtualization/axdevice/src/factory.rs new file mode 100644 index 0000000000..b45a093d1a --- /dev/null +++ b/virtualization/axdevice/src/factory.rs @@ -0,0 +1,37 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device factory catalog used during VM device initialization. + +use axdevice_base::{DeviceFactory, EmuDeviceType}; + +/// A platform-provided catalog of device factories. +pub struct DeviceFactoryCatalog<'a> { + factories: &'a [&'a dyn DeviceFactory], +} + +impl<'a> DeviceFactoryCatalog<'a> { + /// Creates a catalog from a platform-provided factory slice. + pub const fn new(factories: &'a [&'a dyn DeviceFactory]) -> Self { + Self { factories } + } + + /// Finds the factory handling the given emulated device type. + pub fn find(&self, ty: EmuDeviceType) -> Option<&'a dyn DeviceFactory> { + self.factories + .iter() + .copied() + .find(|factory| factory.ty() == ty) + } +} diff --git a/virtualization/axdevice/src/legacy.rs b/virtualization/axdevice/src/legacy.rs index e00ca66909..b72aea8b70 100644 --- a/virtualization/axdevice/src/legacy.rs +++ b/virtualization/axdevice/src/legacy.rs @@ -16,12 +16,10 @@ use alloc::{string::String, sync::Arc, vec::Vec}; -use axdevice_base::{BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps}; - -use crate::{ - bus::{BusAccess, BusAddress, BusKind, BusOp, BusResponse}, - model::{DeviceError, DeviceId, DeviceOps, DeviceResult}, - resource::{DeviceCapabilities, Resource}, +use axdevice_base::{ + BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps, BusAccess, BusAddress, BusKind, + BusOp, BusResponse, DeviceCapabilities, DeviceError, DeviceId, DeviceOps, DeviceResult, + Resource, }; /// The old single-bus device object carried by a legacy adapter. diff --git a/virtualization/axdevice/src/lib.rs b/virtualization/axdevice/src/lib.rs index 7d7165f3d2..b3d4a409a2 100644 --- a/virtualization/axdevice/src/lib.rs +++ b/virtualization/axdevice/src/lib.rs @@ -26,29 +26,25 @@ extern crate alloc; #[macro_use] extern crate log; -mod bus; mod config; mod device; -mod irq; +mod factory; mod legacy; -mod model; mod range_alloc; mod registry; -mod resource; pub use axdevice_base::{ - AccessWidth, BaseDeviceOps, BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps, Port, - SysRegAddr, + AccessWidth, BaseDeviceOps, BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps, + BusAccess, BusAddress, BusKind, BusOp, BusResponse, DeviceBuildContext, DeviceCapabilities, + DeviceError, DeviceFactory, DeviceId, DeviceMeta, DeviceOps, DeviceResult, IrqLine, IrqSink, + IrqTarget, MsiMessage, PciBarKind, Port, Resource, SysRegAddr, }; pub use axvm_types::GuestPhysAddr; -pub use bus::{BusAccess, BusAddress, BusKind, BusOp, BusResponse}; pub use config::AxVmDeviceConfig; pub use device::{AxEmuDevices, AxVmDevices}; -pub use irq::{IrqLine, IrqSink, IrqTarget, MsiMessage}; +pub use factory::DeviceFactoryCatalog; pub use legacy::{LegacyDeviceAdapter, LegacyDeviceInner}; -pub use model::{DeviceError, DeviceId, DeviceOps, DeviceResult}; pub use registry::DeviceRegistry; -pub use resource::{DeviceCapabilities, PciBarKind, Resource}; #[cfg(target_arch = "x86_64")] pub use x86_vlapic::IoApicInterrupt; // pub use virtio_dev::*; diff --git a/virtualization/axdevice/src/registry.rs b/virtualization/axdevice/src/registry.rs index b9ff0832fd..517c599b7e 100644 --- a/virtualization/axdevice/src/registry.rs +++ b/virtualization/axdevice/src/registry.rs @@ -16,14 +16,11 @@ use alloc::{rc::Rc, vec::Vec}; -use axdevice_base::{DeviceAddrRange, PortRange, SysRegAddrRange}; -use axvm_types::GuestPhysAddrRange; - -use crate::{ - bus::{BusAccess, BusAddress, BusResponse}, - model::{DeviceError, DeviceId, DeviceOps, DeviceResult}, - resource::Resource, +use axdevice_base::{ + BusAccess, BusAddress, BusResponse, DeviceAddrRange, DeviceError, DeviceId, DeviceOps, + DeviceResult, PortRange, Resource, SysRegAddrRange, }; +use axvm_types::GuestPhysAddrRange; /// A route entry for MMIO accesses. type MmioRoute = (GuestPhysAddrRange, DeviceId); diff --git a/virtualization/axdevice/tests/test.rs b/virtualization/axdevice/tests/test.rs index 5e858c58cf..945cf489fc 100644 --- a/virtualization/axdevice/tests/test.rs +++ b/virtualization/axdevice/tests/test.rs @@ -21,12 +21,14 @@ use ax_errno::AxResult; use ax_memory_addr::{PhysAddr, VirtAddr}; use axdevice::{ AxVmDeviceConfig, AxVmDevices, BusAccess, BusAddress, BusKind, BusOp, BusResponse, - DeviceCapabilities, DeviceError, DeviceId, DeviceOps, DeviceRegistry, IrqLine, IrqSink, - IrqTarget, LegacyDeviceAdapter, MsiMessage, PciBarKind, Resource, + DeviceBuildContext, DeviceCapabilities, DeviceError, DeviceFactory, DeviceFactoryCatalog, + DeviceId, DeviceOps, DeviceRegistry, DeviceResult, IrqLine, IrqSink, IrqTarget, + LegacyDeviceAdapter, MsiMessage, PciBarKind, Resource, }; use axdevice_base::{AccessWidth, BaseDeviceOps, Port, PortRange, SysRegAddr, SysRegAddrRange}; use axvm_types::{ - EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange, InterruptVector, VCpuId, VMId, + EmulatedDeviceConfig, EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange, InterruptVector, + VCpuId, VMId, }; use x86_vlapic::host::X86VlapicHostIf; @@ -113,15 +115,14 @@ fn test_mmio_dispatch_functionality() { } #[test] -#[should_panic(expected = "emu_device not found")] -fn test_mmio_panic_on_missing_device() { +fn test_mmio_missing_device_returns_error() { let config = AxVmDeviceConfig::new(vec![]); let devices = AxVmDevices::new(config); let invalid_addr = GuestPhysAddr::from(0x9999_9999); let width = AccessWidth::try_from(4).unwrap(); - let _ = devices.handle_mmio_read(invalid_addr, width); + assert!(devices.handle_mmio_read(invalid_addr, width).is_err()); } // Mock implementation for x86_vlapic host callbacks when running @@ -222,6 +223,124 @@ impl DeviceOps for AbstractMockDevice { } } +#[derive(Debug)] +struct NativeMmioCounter { + id: DeviceId, + name: &'static str, + range: GuestPhysAddrRange, + resources: Vec, + value: Mutex, +} + +impl NativeMmioCounter { + fn new(id: usize, base: usize, len: usize, initial: usize) -> Self { + let range = + GuestPhysAddrRange::new(GuestPhysAddr::from(base), GuestPhysAddr::from(base + len)); + + Self { + id: DeviceId::new(id), + name: "native-mmio-counter", + range, + resources: vec![Resource::Mmio(range)], + value: Mutex::new(initial), + } + } + + fn value(&self) -> usize { + *self.value.lock().unwrap() + } +} + +struct TestBuildContext { + next_id: usize, +} + +impl DeviceBuildContext for TestBuildContext { + fn alloc_device_id(&mut self) -> DeviceId { + let id = DeviceId::new(self.next_id); + self.next_id += 1; + id + } +} + +struct MultiCounterFactory; + +impl DeviceFactory for MultiCounterFactory { + fn ty(&self) -> EmulatedDeviceType { + EmulatedDeviceType::GPPTRedistributor + } + + fn build( + &self, + ctx: &mut dyn DeviceBuildContext, + config: &EmulatedDeviceConfig, + ) -> DeviceResult>> { + let count = config.cfg_list.first().copied().unwrap_or(1); + let stride = config.cfg_list.get(1).copied().unwrap_or(config.length); + let mut devices: Vec> = Vec::new(); + + for index in 0..count { + let id = ctx.alloc_device_id(); + let base = config.base_gpa + index * stride; + devices.push(Rc::new(NativeMmioCounter::new( + id.raw(), + base, + config.length, + index, + ))); + } + + Ok(devices) + } +} + +impl DeviceOps for NativeMmioCounter { + fn id(&self) -> DeviceId { + self.id + } + + fn name(&self) -> &str { + self.name + } + + fn resources(&self) -> &[Resource] { + &self.resources + } + + fn capabilities(&self) -> DeviceCapabilities { + DeviceCapabilities::none() + } + + fn access(&self, access: BusAccess) -> Result { + let addr = match access.addr { + BusAddress::Mmio(addr) if access.kind == BusKind::Mmio => addr, + _ => { + return Err(DeviceError::BusAddressMismatch { + kind: BusKind::Mmio, + address: access.addr, + }); + } + }; + + if !self.range.contains(addr) { + return Err(DeviceError::AddressOutOfRange { + kind: BusKind::Mmio, + address: access.addr, + }); + } + + match access.op { + BusOp::Read => Ok(BusResponse::Read { + value: self.value(), + }), + BusOp::Write { value } => { + *self.value.lock().unwrap() = value; + Ok(BusResponse::Write) + } + } + } +} + #[test] fn device_ops_exposes_identity_resources_and_capabilities() { let mmio_range = GuestPhysAddrRange::new( @@ -510,6 +629,63 @@ fn mock_device(id: usize, resources: Vec) -> Rc { }) } +#[test] +fn device_factory_catalog_builds_multiple_native_devices() { + let factory = MultiCounterFactory; + let factories: [&dyn DeviceFactory; 1] = [&factory]; + let catalog = DeviceFactoryCatalog::new(&factories); + + assert!( + catalog + .find(EmulatedDeviceType::GPPTRedistributor) + .is_some() + ); + assert!(catalog.find(EmulatedDeviceType::GPPTITS).is_none()); + + let config = EmulatedDeviceConfig { + name: "counter".into(), + base_gpa: 0xb000_0000, + length: 0x1000, + irq_id: 0, + emu_type: EmulatedDeviceType::GPPTRedistributor, + cfg_list: vec![2, 0x1000], + }; + let mut ctx = TestBuildContext { next_id: 200 }; + let devices = catalog + .find(config.emu_type) + .unwrap() + .build(&mut ctx, &config) + .unwrap(); + + assert_eq!(devices.len(), 2); + + let mut registry = DeviceRegistry::new(); + for device in devices { + registry.register_device(device).unwrap(); + } + assert_eq!(registry.device_count(), 2); + assert_eq!(registry.mmio_route_count(), 2); + + assert_eq!( + registry + .dispatch(BusAccess::mmio_read( + GuestPhysAddr::from(0xb000_0000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0 } + ); + assert_eq!( + registry + .dispatch(BusAccess::mmio_read( + GuestPhysAddr::from(0xb000_1000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 1 } + ); +} + #[test] fn registry_registers_routes_and_dispatches_mock_devices() { let mut registry = DeviceRegistry::new(); @@ -576,6 +752,83 @@ fn registry_registers_routes_and_dispatches_mock_devices() { assert_eq!(sysreg, BusResponse::Read { value: 0x55aa }); } +#[test] +fn registry_dispatches_native_mmio_counter_without_legacy_adapter() { + let mut registry = DeviceRegistry::new(); + let counter = Rc::new(NativeMmioCounter::new(50, 0x4100_0000, 0x1000, 0x11)); + + assert_eq!( + registry.register_device(counter.clone()).unwrap(), + DeviceId::new(50) + ); + assert_eq!(registry.device_count(), 1); + assert_eq!(registry.mmio_route_count(), 1); + assert_eq!(registry.pio_route_count(), 0); + assert_eq!(registry.sysreg_route_count(), 0); + + assert_eq!( + registry + .dispatch(BusAccess::mmio_read( + GuestPhysAddr::from(0x4100_0008), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0x11 } + ); + + assert_eq!( + registry + .dispatch(BusAccess::mmio_write( + GuestPhysAddr::from(0x4100_0008), + AccessWidth::Dword, + 0x22, + )) + .unwrap(), + BusResponse::Write + ); + assert_eq!(counter.value(), 0x22); + + assert_eq!( + registry + .dispatch(BusAccess::mmio_read( + GuestPhysAddr::from(0x4100_0008), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0x22 } + ); + + let out_of_range = counter + .access(BusAccess::mmio_read( + GuestPhysAddr::from(0x4100_2000), + AccessWidth::Dword, + )) + .unwrap_err(); + assert!(matches!( + out_of_range, + DeviceError::AddressOutOfRange { .. } + )); + + let mismatch = counter + .access(BusAccess::pio_read(Port::new(0x80), AccessWidth::Byte)) + .unwrap_err(); + assert!(matches!(mismatch, DeviceError::BusAddressMismatch { .. })); + + let duplicate = Rc::new(NativeMmioCounter::new(50, 0x4100_2000, 0x1000, 0)); + let err = registry.register_device(duplicate).unwrap_err(); + assert!(matches!(err, DeviceError::DuplicateDeviceId { id } if id == DeviceId::new(50))); + + let overlapping = Rc::new(NativeMmioCounter::new(51, 0x4100_0800, 0x1000, 0)); + let err = registry.register_device(overlapping).unwrap_err(); + assert!(matches!(err, DeviceError::ResourceConflict { .. })); + + let adjacent = Rc::new(NativeMmioCounter::new(52, 0x4100_1000, 0x1000, 0)); + assert_eq!( + registry.register_device(adjacent).unwrap(), + DeviceId::new(52) + ); +} + #[test] fn registry_rejects_duplicate_device_ids() { let mut registry = DeviceRegistry::new(); @@ -820,3 +1073,70 @@ fn ax_vm_devices_dispatch_bus_access_uses_new_registry_without_breaking_old_path .unwrap(); assert_eq!(sysreg.get_last_write(), Some((0x501, 0x66))); } + +#[test] +fn ax_vm_devices_registers_native_device_without_old_vec_membership() { + let config = AxVmDeviceConfig::new(vec![]); + let mut devices = AxVmDevices::new(config); + let counter = Rc::new(NativeMmioCounter::new(100, 0xa000_0000, 0x1000, 0x5)); + + assert_eq!(devices.iter_mmio_dev().count(), 0); + assert_eq!( + devices.register_device(counter.clone()).unwrap(), + DeviceId::new(100) + ); + assert_eq!(devices.iter_mmio_dev().count(), 0); + + assert_eq!( + devices + .dispatch_bus_access(BusAccess::mmio_read( + GuestPhysAddr::from(0xa000_0000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0x5 } + ); + + devices + .dispatch_bus_access(BusAccess::mmio_write( + GuestPhysAddr::from(0xa000_0004), + AccessWidth::Dword, + 0x6, + )) + .unwrap(); + assert_eq!(counter.value(), 0x6); + + let duplicate = Rc::new(NativeMmioCounter::new(100, 0xa000_2000, 0x1000, 0)); + let err = devices.register_device(duplicate).unwrap_err(); + assert!(matches!(err, DeviceError::DuplicateDeviceId { id } if id == DeviceId::new(100))); + + let overlapping = Rc::new(NativeMmioCounter::new(101, 0xa000_0800, 0x1000, 0)); + let err = devices.register_device(overlapping).unwrap_err(); + assert!(matches!(err, DeviceError::ResourceConflict { .. })); + + let legacy = Arc::new(MockMmioDevice::new( + "legacy-after-native", + 0xa000_2000, + 0x1000, + )); + devices.add_mmio_dev(legacy); + assert_eq!(devices.iter_mmio_dev().count(), 1); + assert_eq!( + devices + .dispatch_bus_access(BusAccess::mmio_read( + GuestPhysAddr::from(0xa000_2000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0xDEAD_BEEF } + ); + assert_eq!( + devices + .dispatch_bus_access(BusAccess::mmio_read( + GuestPhysAddr::from(0xa000_0000), + AccessWidth::Dword, + )) + .unwrap(), + BusResponse::Read { value: 0x6 } + ); +} diff --git a/virtualization/axdevice/src/bus.rs b/virtualization/axdevice_base/src/bus.rs similarity index 98% rename from virtualization/axdevice/src/bus.rs rename to virtualization/axdevice_base/src/bus.rs index f9e370e811..4c6b65836f 100644 --- a/virtualization/axdevice/src/bus.rs +++ b/virtualization/axdevice_base/src/bus.rs @@ -14,9 +14,10 @@ //! Unified bus transaction types for emulated device access. -use axdevice_base::{AccessWidth, Port, SysRegAddr}; use axvm_types::GuestPhysAddr; +use crate::{AccessWidth, Port, SysRegAddr}; + /// The kind of guest-visible bus or register namespace used for a device access. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum BusKind { diff --git a/virtualization/axdevice/src/irq.rs b/virtualization/axdevice_base/src/irq.rs similarity index 99% rename from virtualization/axdevice/src/irq.rs rename to virtualization/axdevice_base/src/irq.rs index 945bbca3de..1e60fb2cd4 100644 --- a/virtualization/axdevice/src/irq.rs +++ b/virtualization/axdevice_base/src/irq.rs @@ -14,7 +14,7 @@ //! Architecture-neutral interrupt signaling traits for emulated devices. -use crate::model::DeviceError; +use crate::DeviceError; /// A guest-visible interrupt line used by an emulated device. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/virtualization/axdevice_base/src/lib.rs b/virtualization/axdevice_base/src/lib.rs index b3fdcabda3..37ddfb6962 100644 --- a/virtualization/axdevice_base/src/lib.rs +++ b/virtualization/axdevice_base/src/lib.rs @@ -84,16 +84,26 @@ extern crate alloc; +mod bus; mod device; +mod irq; +mod model; +mod resource; use alloc::{string::String, sync::Arc, vec::Vec}; use core::any::Any; pub use ax_errno::AxResult; pub use axvm_types::{EmulatedDeviceType as EmuDeviceType, GuestPhysAddr, GuestPhysAddrRange}; +pub use bus::{BusAccess, BusAddress, BusKind, BusOp, BusResponse}; pub use device::{ AccessWidth, DeviceAddr, DeviceAddrRange, Port, PortRange, SysRegAddr, SysRegAddrRange, }; +pub use irq::{IrqLine, IrqSink, IrqTarget, MsiMessage}; +pub use model::{ + DeviceBuildContext, DeviceError, DeviceFactory, DeviceId, DeviceMeta, DeviceOps, DeviceResult, +}; +pub use resource::{DeviceCapabilities, PciBarKind, Resource}; /// Represents the configuration of an emulated device for a virtual machine. /// diff --git a/virtualization/axdevice/src/model.rs b/virtualization/axdevice_base/src/model.rs similarity index 75% rename from virtualization/axdevice/src/model.rs rename to virtualization/axdevice_base/src/model.rs index eb578d310c..f3f7b3f9e8 100644 --- a/virtualization/axdevice/src/model.rs +++ b/virtualization/axdevice_base/src/model.rs @@ -14,14 +14,14 @@ //! Core emulated device model traits and errors. +use alloc::{rc::Rc, string::String, vec::Vec}; use core::{any::Any, fmt}; use ax_errno::AxError; -use axdevice_base::AccessWidth; +use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType}; use crate::{ - bus::{BusAccess, BusAddress, BusKind, BusResponse}, - resource::{DeviceCapabilities, Resource}, + AccessWidth, BusAccess, BusAddress, BusKind, BusResponse, DeviceCapabilities, Resource, }; /// Unique identifier for a device instance inside one registry. @@ -40,6 +40,52 @@ impl DeviceId { } } +/// Common registry-facing metadata stored by native device implementations. +#[derive(Debug, Clone)] +pub struct DeviceMeta { + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, +} + +impl DeviceMeta { + /// Creates device metadata from raw parts. + pub fn new( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + ) -> Self { + Self { + id, + name, + resources, + capabilities, + } + } + + /// Returns the registry-local device identifier. + pub const fn id(&self) -> DeviceId { + self.id + } + + /// Returns the human-readable device instance name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns resources declared by this device. + pub fn resources(&self) -> &[Resource] { + &self.resources + } + + /// Returns device capability flags. + pub const fn capabilities(&self) -> DeviceCapabilities { + self.capabilities + } +} + /// Error type used by the new device abstraction layer. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeviceError { @@ -182,3 +228,22 @@ pub trait DeviceOps: Any { Ok(()) } } + +/// Build-time context provided to device factories. +pub trait DeviceBuildContext { + /// Allocates a registry-local device identifier. + fn alloc_device_id(&mut self) -> DeviceId; +} + +/// Factory that constructs native device model instances from VM configuration. +pub trait DeviceFactory: Sync { + /// Returns the emulated device type handled by this factory. + fn ty(&self) -> EmulatedDeviceType; + + /// Builds one or more native device instances from a VM device config. + fn build( + &self, + ctx: &mut dyn DeviceBuildContext, + config: &EmulatedDeviceConfig, + ) -> DeviceResult>>; +} diff --git a/virtualization/axdevice/src/resource.rs b/virtualization/axdevice_base/src/resource.rs similarity index 97% rename from virtualization/axdevice/src/resource.rs rename to virtualization/axdevice_base/src/resource.rs index 9c927ece67..39e4a8420b 100644 --- a/virtualization/axdevice/src/resource.rs +++ b/virtualization/axdevice_base/src/resource.rs @@ -14,10 +14,9 @@ //! Resource and capability declarations for emulated devices. -use axdevice_base::{PortRange, SysRegAddrRange}; use axvm_types::GuestPhysAddrRange; -use crate::irq::IrqLine; +use crate::{IrqLine, PortRange, SysRegAddrRange}; /// The kind of PCI BAR exposed by a device. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] From 60b18bb96fa32e326193197e9ccd3e0e8d80888c Mon Sep 17 00:00:00 2001 From: baitwo02 Date: Sun, 21 Jun 2026 11:19:13 +0800 Subject: [PATCH 3/5] feat(axdevice): add linker-section device factory registration mechanism Add a static device factory registration system using linker sections: - Introduce DeviceFactoryRegister struct and register_device_factory! macro in axdevice_base, placing entries in .axdevice.factory linker section - Extend DeviceFactoryCatalog with from_linker()/from_registers() constructors and find_unique() for duplicate detection - Update runtime.ld with __saxdevice_factory/__eaxdevice_factory symbols - Refactor aarch64 device initialization to use linker-collected factories - Register GpptRedistributorFactory via the new macro in arm_vgic - Add tests for factory registration lookup and duplicate rejection --- os/arceos/modules/axruntime/runtime.ld | 5 + virtualization/arm_vgic/src/v3/vgicr.rs | 4 + virtualization/axdevice/src/device.rs | 8 +- virtualization/axdevice/src/factory.rs | 140 ++++++++++++++++++++-- virtualization/axdevice/src/lib.rs | 4 +- virtualization/axdevice/tests/test.rs | 44 ++++++- virtualization/axdevice_base/src/lib.rs | 3 +- virtualization/axdevice_base/src/model.rs | 46 +++++++ 8 files changed, 236 insertions(+), 18 deletions(-) diff --git a/os/arceos/modules/axruntime/runtime.ld b/os/arceos/modules/axruntime/runtime.ld index 5db12a83d8..cd2b93cbc1 100644 --- a/os/arceos/modules/axruntime/runtime.ld +++ b/os/arceos/modules/axruntime/runtime.ld @@ -12,6 +12,11 @@ SECTIONS { KEEP(*(.driver.register*)) __edriver_register = .; + . = ALIGN(0x10); + __saxdevice_factory = .; + KEEP(*(.axdevice.factory*)) + __eaxdevice_factory = .; + . = ALIGN(0x10); _ex_table_start = .; KEEP(*(__ex_table)) diff --git a/virtualization/arm_vgic/src/v3/vgicr.rs b/virtualization/arm_vgic/src/v3/vgicr.rs index cd6ab7ae3c..374e9c6b0c 100644 --- a/virtualization/arm_vgic/src/v3/vgicr.rs +++ b/virtualization/arm_vgic/src/v3/vgicr.rs @@ -280,6 +280,10 @@ impl DeviceOps for VGicR { /// Factory for ARM GIC partial passthrough redistributors. pub struct GpptRedistributorFactory; +static GPPT_REDISTRIBUTOR_FACTORY: GpptRedistributorFactory = GpptRedistributorFactory; + +axdevice_base::register_device_factory!("gppt-redistributor", GPPT_REDISTRIBUTOR_FACTORY); + impl DeviceFactory for GpptRedistributorFactory { fn ty(&self) -> EmuDeviceType { EmuDeviceType::GPPTRedistributor diff --git a/virtualization/axdevice/src/device.rs b/virtualization/axdevice/src/device.rs index 4c0376a489..943eeda406 100644 --- a/virtualization/axdevice/src/device.rs +++ b/virtualization/axdevice/src/device.rs @@ -174,16 +174,14 @@ fn init_from_aarch64_catalog( devices: &mut AxVmDevices, config: &EmulatedDeviceConfig, ) -> DeviceResult { - let gppt_redistributor_factory = arm_vgic::v3::vgicr::GpptRedistributorFactory; - let factories: [&dyn crate::DeviceFactory; 1] = [&gppt_redistributor_factory]; - let catalog = crate::DeviceFactoryCatalog::new(&factories); + let catalog = crate::DeviceFactoryCatalog::from_linker()?; - let Some(factory) = catalog.find(config.emu_type) else { + let Some(factory) = catalog.find_unique(config.emu_type)? else { return Ok(false); }; info!( - "aarch64 device factory matched: type={:?}, name={}, base_gpa={:#x}, length={:#x}", + "aarch64 linker device factory matched: type={:?}, name={}, base_gpa={:#x}, length={:#x}", config.emu_type, config.name, config.base_gpa, config.length ); diff --git a/virtualization/axdevice/src/factory.rs b/virtualization/axdevice/src/factory.rs index b45a093d1a..b713eea27a 100644 --- a/virtualization/axdevice/src/factory.rs +++ b/virtualization/axdevice/src/factory.rs @@ -14,24 +14,148 @@ //! Device factory catalog used during VM device initialization. -use axdevice_base::{DeviceFactory, EmuDeviceType}; +use alloc::format; + +use ax_errno::ax_err_type; +use axdevice_base::{ + DeviceError, DeviceFactory, DeviceFactoryRegister, DeviceResult, EmuDeviceType, +}; + +/// Returns device factory registration entries collected from linker sections. +#[cfg(target_os = "none")] +pub fn linker_device_factory_registers() -> DeviceResult<&'static [DeviceFactoryRegister]> { + unsafe extern "C" { + fn __saxdevice_factory(); + fn __eaxdevice_factory(); + } + + let start = __saxdevice_factory as *const () as usize; + let end = __eaxdevice_factory as *const () as usize; + if start > end { + return Err(DeviceError::from(ax_err_type!( + BadState, + format!("invalid axdevice factory section range: start={start:#x}, end={end:#x}") + ))); + } + + let len = end - start; + if len == 0 { + return Ok(&[]); + } + + let align = core::mem::align_of::(); + if !start.is_multiple_of(align) { + return Err(DeviceError::from(ax_err_type!( + BadState, + format!("misaligned axdevice factory section: start={start:#x}, align={align}") + ))); + } + + let entry_size = core::mem::size_of::(); + if !len.is_multiple_of(entry_size) { + return Err(DeviceError::from(ax_err_type!( + BadState, + format!( + "invalid axdevice factory section length: len={len:#x}, entry_size={entry_size}" + ) + ))); + } + + let count = len / entry_size; + unsafe { Ok(core::slice::from_raw_parts(start as *const _, count)) } +} + +/// Returns an empty linker factory list for non-bare-metal host builds. +#[cfg(not(target_os = "none"))] +pub fn linker_device_factory_registers() -> DeviceResult<&'static [DeviceFactoryRegister]> { + Ok(&[]) +} + +enum DeviceFactorySource<'a> { + Factories(&'a [&'a dyn DeviceFactory]), + Registers(&'a [DeviceFactoryRegister]), +} /// A platform-provided catalog of device factories. pub struct DeviceFactoryCatalog<'a> { - factories: &'a [&'a dyn DeviceFactory], + source: DeviceFactorySource<'a>, } impl<'a> DeviceFactoryCatalog<'a> { /// Creates a catalog from a platform-provided factory slice. pub const fn new(factories: &'a [&'a dyn DeviceFactory]) -> Self { - Self { factories } + Self { + source: DeviceFactorySource::Factories(factories), + } + } + + /// Creates a catalog from linker-collected factory registration entries. + pub const fn from_registers(registers: &'a [DeviceFactoryRegister]) -> Self { + Self { + source: DeviceFactorySource::Registers(registers), + } + } + + /// Creates a catalog from the final image's linker-collected entries. + pub fn from_linker() -> DeviceResult> { + let registers = linker_device_factory_registers()?; + Ok(DeviceFactoryCatalog::from_registers(registers)) } - /// Finds the factory handling the given emulated device type. + /// Finds the first factory handling the given emulated device type. pub fn find(&self, ty: EmuDeviceType) -> Option<&'a dyn DeviceFactory> { - self.factories - .iter() - .copied() - .find(|factory| factory.ty() == ty) + match self.source { + DeviceFactorySource::Factories(factories) => { + factories.iter().copied().find(|factory| factory.ty() == ty) + } + DeviceFactorySource::Registers(registers) => registers + .iter() + .find(|register| register.factory().ty() == ty) + .map(DeviceFactoryRegister::factory), + } } + + /// Finds a factory handling the type and reports duplicate registrations. + pub fn find_unique(&self, ty: EmuDeviceType) -> DeviceResult> { + let mut matched: Option<(&'a dyn DeviceFactory, &'static str)> = None; + + match self.source { + DeviceFactorySource::Factories(factories) => { + for factory in factories.iter().copied() { + if factory.ty() != ty { + continue; + } + if let Some((_, existing_name)) = matched { + return Err(duplicate_factory_error(ty, existing_name, "")); + } + matched = Some((factory, "")); + } + } + DeviceFactorySource::Registers(registers) => { + for register in registers { + let factory = register.factory(); + if factory.ty() != ty { + continue; + } + if let Some((_, existing_name)) = matched { + return Err(duplicate_factory_error(ty, existing_name, register.name())); + } + matched = Some((factory, register.name())); + } + } + } + + Ok(matched.map(|(factory, _)| factory)) + } +} + +fn duplicate_factory_error( + ty: EmuDeviceType, + existing_name: &'static str, + duplicate_name: &'static str, +) -> DeviceError { + DeviceError::from(ax_err_type!( + BadState, + format!("duplicate device factories for {ty:?}: {existing_name} and {duplicate_name}") + )) } diff --git a/virtualization/axdevice/src/lib.rs b/virtualization/axdevice/src/lib.rs index b3d4a409a2..0ddf51663c 100644 --- a/virtualization/axdevice/src/lib.rs +++ b/virtualization/axdevice/src/lib.rs @@ -36,8 +36,8 @@ mod registry; pub use axdevice_base::{ AccessWidth, BaseDeviceOps, BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps, BusAccess, BusAddress, BusKind, BusOp, BusResponse, DeviceBuildContext, DeviceCapabilities, - DeviceError, DeviceFactory, DeviceId, DeviceMeta, DeviceOps, DeviceResult, IrqLine, IrqSink, - IrqTarget, MsiMessage, PciBarKind, Port, Resource, SysRegAddr, + DeviceError, DeviceFactory, DeviceFactoryRegister, DeviceId, DeviceMeta, DeviceOps, + DeviceResult, IrqLine, IrqSink, IrqTarget, MsiMessage, PciBarKind, Port, Resource, SysRegAddr, }; pub use axvm_types::GuestPhysAddr; pub use config::AxVmDeviceConfig; diff --git a/virtualization/axdevice/tests/test.rs b/virtualization/axdevice/tests/test.rs index 945cf489fc..ec5fee65e4 100644 --- a/virtualization/axdevice/tests/test.rs +++ b/virtualization/axdevice/tests/test.rs @@ -22,8 +22,8 @@ use ax_memory_addr::{PhysAddr, VirtAddr}; use axdevice::{ AxVmDeviceConfig, AxVmDevices, BusAccess, BusAddress, BusKind, BusOp, BusResponse, DeviceBuildContext, DeviceCapabilities, DeviceError, DeviceFactory, DeviceFactoryCatalog, - DeviceId, DeviceOps, DeviceRegistry, DeviceResult, IrqLine, IrqSink, IrqTarget, - LegacyDeviceAdapter, MsiMessage, PciBarKind, Resource, + DeviceFactoryRegister, DeviceId, DeviceOps, DeviceRegistry, DeviceResult, IrqLine, IrqSink, + IrqTarget, LegacyDeviceAdapter, MsiMessage, PciBarKind, Resource, }; use axdevice_base::{AccessWidth, BaseDeviceOps, Port, PortRange, SysRegAddr, SysRegAddrRange}; use axvm_types::{ @@ -265,6 +265,9 @@ impl DeviceBuildContext for TestBuildContext { struct MultiCounterFactory; +static MULTI_COUNTER_FACTORY: MultiCounterFactory = MultiCounterFactory; +static DUPLICATE_MULTI_COUNTER_FACTORY: MultiCounterFactory = MultiCounterFactory; + impl DeviceFactory for MultiCounterFactory { fn ty(&self) -> EmulatedDeviceType { EmulatedDeviceType::GPPTRedistributor @@ -686,6 +689,43 @@ fn device_factory_catalog_builds_multiple_native_devices() { ); } +#[test] +fn device_factory_catalog_finds_registered_factories() { + let registers = [DeviceFactoryRegister::new( + "multi-counter", + &MULTI_COUNTER_FACTORY, + )]; + let catalog = DeviceFactoryCatalog::from_registers(®isters); + + assert!( + catalog + .find(EmulatedDeviceType::GPPTRedistributor) + .is_some() + ); + assert!( + catalog + .find_unique(EmulatedDeviceType::GPPTRedistributor) + .unwrap() + .is_some() + ); + assert!(catalog.find(EmulatedDeviceType::GPPTITS).is_none()); +} + +#[test] +fn device_factory_catalog_rejects_duplicate_registered_factories() { + let registers = [ + DeviceFactoryRegister::new("multi-counter", &MULTI_COUNTER_FACTORY), + DeviceFactoryRegister::new("duplicate-counter", &DUPLICATE_MULTI_COUNTER_FACTORY), + ]; + let catalog = DeviceFactoryCatalog::from_registers(®isters); + + let error = match catalog.find_unique(EmulatedDeviceType::GPPTRedistributor) { + Ok(_) => panic!("duplicate factories should be rejected"), + Err(error) => error, + }; + assert!(matches!(error, DeviceError::Backend(_))); +} + #[test] fn registry_registers_routes_and_dispatches_mock_devices() { let mut registry = DeviceRegistry::new(); diff --git a/virtualization/axdevice_base/src/lib.rs b/virtualization/axdevice_base/src/lib.rs index 37ddfb6962..3e6373fac9 100644 --- a/virtualization/axdevice_base/src/lib.rs +++ b/virtualization/axdevice_base/src/lib.rs @@ -101,7 +101,8 @@ pub use device::{ }; pub use irq::{IrqLine, IrqSink, IrqTarget, MsiMessage}; pub use model::{ - DeviceBuildContext, DeviceError, DeviceFactory, DeviceId, DeviceMeta, DeviceOps, DeviceResult, + DeviceBuildContext, DeviceError, DeviceFactory, DeviceFactoryRegister, DeviceId, DeviceMeta, + DeviceOps, DeviceResult, }; pub use resource::{DeviceCapabilities, PciBarKind, Resource}; diff --git a/virtualization/axdevice_base/src/model.rs b/virtualization/axdevice_base/src/model.rs index f3f7b3f9e8..e286def874 100644 --- a/virtualization/axdevice_base/src/model.rs +++ b/virtualization/axdevice_base/src/model.rs @@ -247,3 +247,49 @@ pub trait DeviceFactory: Sync { config: &EmulatedDeviceConfig, ) -> DeviceResult>>; } + +/// Static factory registration entry collected from the final image. +/// +/// Entries of this type may be placed in the `.axdevice.factory` linker +/// section by concrete device crates. `axdevice` consumes the collected slice +/// to build a runtime factory catalog without knowing concrete device types. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DeviceFactoryRegister { + name: &'static str, + factory: &'static dyn DeviceFactory, +} + +impl DeviceFactoryRegister { + /// Creates a static factory registration entry. + pub const fn new(name: &'static str, factory: &'static dyn DeviceFactory) -> Self { + Self { name, factory } + } + + /// Returns the human-readable factory name used for diagnostics. + pub const fn name(&self) -> &'static str { + self.name + } + + /// Returns the factory referenced by this registration entry. + pub const fn factory(&self) -> &'static dyn DeviceFactory { + self.factory + } +} + +/// Registers a native device factory in the `.axdevice.factory` linker section. +/// +/// The concrete device crate remains responsible for ensuring that the crate is +/// linked into the final image. The final linker script must keep +/// `.axdevice.factory*` and export the start/end symbols consumed by `axdevice`. +#[macro_export] +macro_rules! register_device_factory { + ($name:expr, $factory:expr $(,)?) => { + const _: () = { + #[unsafe(link_section = ".axdevice.factory")] + #[used] + static FACTORY_REGISTER: $crate::DeviceFactoryRegister = + $crate::DeviceFactoryRegister::new($name, &$factory); + }; + }; +} From 704891c82a7c4c9c18badb2e2bc57ba415b8bf5e Mon Sep 17 00:00:00 2001 From: baitwo02 Date: Sun, 21 Jun 2026 12:35:21 +0800 Subject: [PATCH 4/5] docs: add device migration study records with linker-section factory design --- study_records.md | 387 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 study_records.md diff --git a/study_records.md b/study_records.md new file mode 100644 index 0000000000..a8feac337a --- /dev/null +++ b/study_records.md @@ -0,0 +1,387 @@ +# 学习记录 + +## 设备抽象重构当前判断 + +AxVisor 当前设备框架已经进入从 legacy 设备模型迁移到统一 `DeviceOps` 模型的阶段。核心目标不是简单把 `AxVmDevices::init` 里的 `match` 移到另一个文件,而是把设备模型拆成几层职责: + +```text +设备本体:声明资源、能力、访问语义和生命周期 +factory:根据 VM 配置创建设备实例 +catalog:描述某个架构/平台可创建哪些设备 +registry:保存已创建设备并负责总线路由和资源冲突检查 +VM exit:只生成 BusAccess,不关心具体设备类型 +``` + +当前已经形成的基础设施: + +- `axdevice_base` 已承载基础模型:`DeviceOps`、`DeviceId`、`DeviceError`、`Resource`、`DeviceCapabilities`、`BusAccess`、`BusResponse`、`IrqSink` 等。 +- `axdevice` 已有 `DeviceRegistry`,可以根据 `Resource::Mmio/Pio/SysReg` 建立路由,并做重复 `DeviceId` 和 bus resource 冲突检查。 +- `LegacyDeviceAdapter` 已能把旧 `BaseMmioDeviceOps`、`BasePortDeviceOps`、`BaseSysRegDeviceOps` 适配成 `DeviceOps`。 +- `AxVmDevices::add_*_dev` 当前仍保留旧 `Vec`,同时把 legacy 设备注册进 `DeviceRegistry`。 +- 普通 MMIO/PIO/SysReg 访问入口已经可以收敛到 `DeviceRegistry::dispatch()`。 + +因此,后续新增 factory 时,不应该再把 factory API 设计成返回 `BaseDeviceOps`。`BaseDeviceOps` 是迁移期兼容层,不应该反向塑造新接口。 + +## 直接实现 DeviceOps 的原则 + +新的设备迁移原则是:**进入 factory 迁移范围的设备,默认直接由具体设备类型实现 `DeviceOps`**。 + +也就是说,目标形态应当是: + +```text +EmulatedDeviceConfig + -> DeviceFactory::build(ctx, config) + -> Rc + -> AxVmDevices::register_device() + -> DeviceRegistry +``` + +不推荐的形态是: + +```text +EmulatedDeviceConfig + -> DeviceFactory::build(...) + -> BaseDeviceOps + -> AxVmDevices 再包 LegacyDeviceAdapter +``` + +直接实现 `DeviceOps` 的好处: + +- 设备自己的资源和能力声明留在设备代码里,而不是散落在 factory 或 wrapper 中。 +- factory 只负责解析配置并调用设备构造函数,逻辑足够薄。 +- 新增设备时主要修改设备 crate 和对应平台 catalog,不需要修改 `AxVmDevices::init`、`DeviceRegistry` 或总线分发核心。 +- 可以逐步减少 `LegacyDeviceAdapter` 的使用范围,而不是让 legacy 过渡状态固化成新架构。 + +直接实现 `DeviceOps` 时,设备结构需要持有 registry metadata,例如: + +```text +DeviceId +name +Vec +DeviceCapabilities +``` + +为了减少重复样板,后续可以在 `axdevice_base` 中增加一个轻量 `DeviceMeta` helper。具体设备可以包含 `meta: DeviceMeta`,并在 `DeviceOps` 的 `id/name/resources/capabilities` 中直接转发。这个 helper 只是减少样板,不改变“设备本体直接实现 DeviceOps”的方向。 + +只有在少数场景下才考虑临时 wrapper: + +- 设备结构来自外部 crate,短期不方便改字段。 +- 设备内部状态生命周期和 registry metadata 确实不适合放在同一个结构里。 +- 迁移过程中需要极短期桥接,且计划明确后续删除。 + +wrapper 不是默认路线。 + +## legacy 路径的定位 + +迁移过程中仍然需要保留 legacy 路径,但它的定位要清楚: + +```text +LegacyDeviceAdapter:未迁移设备接入 DeviceRegistry 的兼容层 +旧 MMIO/PIO/SysReg Vec:typed control path 和迁移期兼容层 +``` + +它们不再是普通 bus 访问的目标路径。 + +旧 `Vec` 当前仍有实际用途: + +- AArch64 可能通过遍历 MMIO 设备找到 `VGicD` 后执行 `assign_irq()`。 +- RISC-V 可能通过查找 vPLIC 设备设置 pending。 +- x86 当前还有 IOAPIC/PIT/serial 的专用句柄和控制路径。 + +这些行为不是普通 MMIO/PIO/SysReg 访问,而是架构控制路径。后续应逐步迁到更明确的 `InterruptRouter`、`IrqSink` 或 platform service,而不是强行用 bus dispatch 表达。 + +## catalog 的位置 + +仍然需要维护一个“可构造设备集合”,也就是 factory catalog。原因很简单:配置里只有设备类型和参数,系统必须知道哪个构造函数能处理它。 + +但 catalog 不应该放进 `axdevice` 核心并重新形成中心化大表。建议边界是: + +```text +axdevice_base: + 定义 DeviceOps、Resource、BusAccess、DeviceFactory 等具体设备 crate 需要依赖的基础协议。 + +axdevice: + 定义 AxVmDevices、DeviceRegistry、注册流程、catalog 消费逻辑。 + 不直接知道 VGicD、Gits、vPLIC、IOAPIC 等具体类型。 + +架构/平台 glue: + 组合该平台支持的 factory catalog,例如 AArch64 catalog 包含 VGIC/GPPT 相关 factory。 + +具体设备 crate: + 直接实现 DeviceOps,并导出自己的 factory 或 factory entry。 +``` + +这样新增设备时,需要修改的是设备 crate 和平台 catalog,而不是修改核心 registry/router。 + +长期如果确实需要完全插件式自动发现,可以评估 linker-section 自动注册。但在 no_std/hypervisor 场景中,显式 catalog 更稳,链接脚本、LTO、多架构兼容和测试成本都更可控。 + +## 下一阶段路线 + +下一阶段建议围绕 AArch64 主线做 factory 和原生 `DeviceOps` 迁移: + +```text +1. 定义 DeviceFactory / DeviceFactoryCatalog / DeviceBuildContext。 +2. 让 factory 返回 Vec>,不暴露 BaseDeviceOps。 +3. 具体设备默认直接实现 DeviceOps;必要时补 DeviceMeta helper 减少样板。 +4. 由 AArch64 平台 glue 组合 VGIC/GPPT factory catalog。 +5. AxVmDevices::init 从“match 具体设备类型并构造”逐步变成“config -> catalog -> factory -> register_device”。 +6. 保留 legacy fallback 服务未迁移设备,直到对应设备完成原生化。 +``` + +AArch64 侧优先迁移这些当前 `AxVmDevices::init` 中的设备: + +- `EmulatedDeviceType::GPPTDistributor` -> `arm_vgic::v3::vgicd::VGicD` +- `EmulatedDeviceType::GPPTRedistributor` -> 多个 `arm_vgic::v3::vgicr::VGicR` +- `EmulatedDeviceType::GPPTITS` -> `arm_vgic::v3::gits::Gits` +- `EmulatedDeviceType::InterruptController` -> `arm_vgic::Vgic` + +其中 `GPPTRedistributor` 需要注意:一个 config 会根据 `cpu_num/stride/pcpu_id` 展开成多个设备实例,所以 factory 返回值必须支持多个 `Rc`。 + +`IVCChannel` 暂时不要塞进普通 `DeviceOps` factory。它不是 bus device,而是 VM 级 meta resource/allocator。可以后续单独设计 VM resource initializer,避免污染设备 factory 模型。 + +## AArch64 VGicR 迁移验证记录 + +本阶段已经以 AArch64 主线的 `GPPTRedistributor -> VGicR` 作为第一条设备迁移样例,验证了 factory/native `DeviceOps` 路径是可行的。 + +实际落地后的路径是: + +```text +EmulatedDeviceConfig(GPPTRedistributor) + -> AArch64 DeviceFactoryCatalog + -> GpptRedistributorFactory::build() + -> VGicR::new(DeviceMeta, ...) + -> Rc + -> AxVmDevices::register_device() + -> DeviceRegistry +``` + +这次迁移中确认了几个重要原则: + +- factory 直接返回 `Vec>`,不再返回 `BaseDeviceOps`。 +- `VGicR` 本体直接实现 `DeviceOps`,不再通过 wrapper 进入新模型。 +- `VGicR` 删除了 `legacy_meta`、`new_with_meta` 和 `impl BaseDeviceOps`。 +- 原 `BaseDeviceOps` 中的 `address_range/read/write` 逻辑改为 `VGicR` 自己的私有方法,再由 `DeviceOps::access()` 调用。 +- 设备 metadata 和资源声明由设备/factory 自己构造,核心容器只消费 `DeviceOps` 和 `Resource`。 +- `AxVmDevices::init` 对已迁移设备只负责 catalog 查找、factory build、registry register,不再关心具体设备构造细节。 + +为了验证这条迁移链路,新增了一个专用测试组: + +```text +test-suit/axvisor/aarch64-device-migration/qemu +``` + +对应命令: + +```bash +cargo xtask axvisor test qemu --arch aarch64 --test-group aarch64-device-migration --test-case smoke > tmp.log +``` + +当前验证配置只打开 `gppt-gicr`,暂时注释掉 `gppt-gicd` 和 `gppt-gits`,目的是避免 `VGicD` typed-control 路径和 `Gits` host ITS MMIO 初始化路径干扰 `VGicR` factory 迁移判断。 + +成功日志包括: + +```text +aarch64 device factory matched: type=GPPTRedistributor +GPPT Redistributor factory built native VGicR for vCPU 0 +aarch64 device factory built native device: id=DeviceId(0), name=gppt-gicr-0 +aarch64 device factory registered 1 native device(s) for type GPPTRedistributor +PASS smoke +``` + +因此可以认为:**设备创建、资源声明、factory/catalog 接入、registry 注册这条迁移路径已经跑通**。 + +但这次验证不等于所有设备问题都解决了。日志中仍然可能出现: + +```text +Failed to assign SPIs: No VGicD found in device list +``` + +这是 `VGicD` 仍被旧 typed-control 路径查找的结果,不属于 `VGicR` factory 迁移失败。它说明后续迁移 `VGicD` 时需要同时处理“设备控制面”问题,而不是只迁移普通 MMIO access。 + +## linker-section factory 自注册方案记录 + +在继续讨论“新增设备是否仍然要改 `device.rs`”时,确认了当前 `DeviceFactoryCatalog` 只解决了“构建设备逻辑不写在 `AxVmDevices::init` 里”的问题,还没有解决“factory 列表仍由核心代码维护”的问题。 + +当前 AArch64 路径中仍然存在类似下面的中心化注册点: + +```text +device.rs + -> 手写 factories: [&dyn DeviceFactory; N] + -> DeviceFactoryCatalog::new(&factories) + -> find(config.emu_type) +``` + +因此,新增一个 factory 仍然需要改 `device.rs` 或某个显式 platform catalog。这个状态比直接在 `match config.emu_type` 中构造设备更好,但还不是设备 crate 自注册。 + +项目里已经有一个可参考的 no_std linker-section 注册模式:`rdrive`/`ax-driver` 的 `.driver.register`。 + +现有模式大致是: + +```text +driver crate + -> #[link_section = ".driver.register"] static DriverRegister + +runtime.ld + -> __sdriver_register = . + -> KEEP(*(.driver.register*)) + -> __edriver_register = . + +axruntime/registers.rs + -> start/end symbol + -> &[DriverRegister] + -> rdrive::register_append(...) +``` + +这个机制的关键不是 `inventory` 或 `linkme` 本身,而是更基础的 linker-section 思路: + +```text +多个 crate 分散提交静态注册项 + -> 链接器把同名 section 合并 + -> 最终镜像通过 start/end 符号扫描注册项 + -> runtime/catalog 消费这些注册项 +``` + +如果把它复用到 axdevice factory,建议做成一套平行机制,例如 `.axdevice.factory`。 + +建议的职责边界: + +```text +axdevice_base: + 定义 DeviceFactoryRegister 或等价注册项类型。 + 提供 register_device_factory! 宏。 + 这样具体设备 crate 只依赖 axdevice_base,不依赖 axdevice,避免循环依赖。 + +具体设备 crate: + 定义自己的 factory。 + 通过 register_device_factory! 把 factory entry 放进 .axdevice.factory section。 + +axdevice: + 读取 __saxdevice_factory/__eaxdevice_factory。 + 将 linker section 中的注册项转换成 factory catalog。 + AxVmDevices::init 只消费 catalog,不再手写具体 factory 列表。 + +runtime/linker script: + 保留 .axdevice.factory* section。 + 导出 start/end symbol。 +``` + +一个可能的初始化路径: + +```text +arm_vgic::v3::vgicr + -> static GPPT_REDISTRIBUTOR_FACTORY + -> register_device_factory!("gppt-redistributor", GPPT_REDISTRIBUTOR_FACTORY) + +final ELF + -> .axdevice.factory + +axdevice + -> linker_device_factories() + -> DeviceFactoryCatalog + -> factory.build(ctx, config) + -> register_device() +``` + +需要特别注意依赖方向。注册宏如果放在 `axdevice`,`arm_vgic` 为了注册 factory 就要依赖 `axdevice`;但当前 `axdevice` 在 AArch64 下又依赖 `arm_vgic`,这会形成不合适的循环。因此注册项类型和宏更适合放在 `axdevice_base`。 + +no_std 下需要确认的正确性点: + +- `runtime.ld` 或最终链接脚本必须 `KEEP(*(.axdevice.factory*))`,否则 LTO/`--gc-sections` 可能裁掉注册项。 +- 需要导出 `__saxdevice_factory` 和 `__eaxdevice_factory`,并保证空 section 时 start/end 仍然可用。 +- section 应放在已加载、可读、地址映射正确的区域,通常跟 `.runtime` 或 `.rodata` 附近更合理。 +- section 起始地址要满足注册项类型的对齐要求,读取时要检查 section 字节长度是否是 `size_of::()` 的整数倍。 +- 注册项类型需要保持简单稳定,适合静态放入 section,例如名称、设备类型、factory 指针或构造函数指针。 +- 如果使用 `&'static dyn DeviceFactory`,它只应作为同一个 Rust 镜像内的布局约定使用,不应把它当跨语言 ABI。 +- 如果宏使用 `#[used(linker)]`,展开所在 crate 可能需要 `#![feature(used_with_arg)]`;如果为了降低接入成本,可以先使用稳定 `#[used]` 加 linker script `KEEP`。 +- 设备 crate 必须被最终镜像实际链接进来。只写一个注册 static 不代表 Cargo 一定会把 crate 拉进最终 ELF。 +- 不应依赖注册项顺序。若同一 `EmulatedDeviceType` 出现多个 factory,应定义清楚是报错、按 priority 选择,还是 first match。 +- 构建后需要用 `readelf -S/-s` 或启动日志验证 section、start/end symbol 和 factory 数量。 + +和 `inventory`/`linkme` 相比,复用 `.driver.register` 思路更贴合当前项目: + +- 不需要先引入新的分布式注册 crate。 +- 可以沿用现有 linker script/start-end symbol/slice 扫描经验。 +- 更容易在 Axvisor 的 no_std、多架构、定制链接脚本环境中审计。 + +这条路线能解决的是“新增 factory 不再改 `device.rs` 的 factory 数组”。它不能自动解决所有中心化修改: + +- 新增全新的 `EmulatedDeviceType` 仍然需要改 `axvm-types` 和配置解析。 +- 新设备 crate 仍然需要通过 feature/dependency 被最终镜像链接。 +- 有 typed-control path 的设备仍然需要后续 IRQ/control-plane 抽象配合迁移。 + +因此,这个方案适合作为显式 catalog 之后的下一阶段增强:先把 factory 构建路径稳定下来,再把 factory 列表维护从核心代码迁移到 linker-section 注册表。 + +## 当前结论 + +设备迁移可以分成两层: + +```text +设备创建和普通 bus access: + 当前方案已验证,可以继续按 VGicR 模板迁移。 + +设备控制面和中断注入: + 仍然散落在 VM/架构代码中,需要进入下一阶段设计。 +``` + +因此后续不需要继续把主要精力放在“证明 factory 是否可行”上。后续普通设备迁移可以按需推进,但方向二的主线应该转向 IRQ 路由和中断后端抽象。 + +## 后续计划:转向 IRQ 路由迁移 + +下一阶段建议把重心放在 VM 级中断模型,而不是继续机械迁移所有设备。 + +目标形态: + +```text +设备后端 + -> IrqSink / InterruptRouter + -> arch-specific interrupt backend + -> vLAPIC/vIOAPIC | VGIC | vPLIC/AIA | LoongArch virtual interrupt controller +``` + +核心原则: + +- 设备只表达中断语义,例如 `raise/lower/pulse/msi/eoi`。 +- 设备不直接调用 VGIC/vPLIC/vLAPIC/CSR/GCSR 等架构接口。 +- 设备不通过“写另一个设备 MMIO”间接注入中断。 +- VM 层提供统一 `InterruptRouter`,根据架构和 IRQ route 分发到实际后端。 +- route 表负责描述 `IrqLine -> IrqTarget`、SPI/PPI/SGI、MSI/MSI-X、vCPU target 等关系。 + +建议拆成几个小步: + +```text +1. 梳理现有中断注入路径 + AArch64: VGIC/GICH/ICH/LR 注入、VGicD::assign_irq、timer 中断 + RISC-V: vPLIC pending/claim/complete + x86_64: vLAPIC/vIOAPIC/MSI/pending event + LoongArch: CSR/GCSR 虚拟中断注入 + +2. 定义 VM 级 IRQ 概念层 + IrqLine + IrqTarget + IrqRoute + IrqEvent + IrqSink + InterruptRouter + InterruptControllerOps 或 ArchIrqBackend + +3. 先做最小 AArch64 backend + 从设备视角只需要 pulse/raise/lower 一个 SPI/PPI。 + router 内部再调用现有 VGIC 注入逻辑。 + 不急着一次性重写 VGICD/GICR/GITS。 + +4. 选择一个真实调用点迁移 + 优先选择 timer tick、简单虚拟设备 IRQ、或当前最清晰的一条 SPI 注入路径。 + 避免一开始就迁移完整 ITS/MSI。 + +5. 再回头处理 typed-control path + VGicD::assign_irq 这类路径应从“遍历 MMIO 设备 downcast”迁到明确的控制接口或 interrupt backend service。 +``` + +已有 `components/irq-framework` 更偏 host IRQ action/registry,可以借鉴它的 request/enable/dispatch 思想,但 VM 级 IRQ router 关注的是 guest interrupt injection,不应直接把两者混成一个对象。 + +短期里可以把后续工作拆成两条并行线: + +- **设备线**:继续按 `VGicR` 模板迁移简单设备;遇到有控制面依赖的设备先记录 blocker。 +- **IRQ 线**:优先设计和落地 VM 级 `InterruptRouter/IrqSink`,把架构特判从设备后端中抽出来。 + +我当前更倾向先推进 IRQ 线,因为设备创建路径已经被 `VGicR` 验证过,真正影响方向二收益的是“中断注入语义是否能统一”。 From 9146414536805ca355b5a76d4982664d8cdb5885 Mon Sep 17 00:00:00 2001 From: baitwo02 Date: Tue, 21 Jul 2026 00:43:19 +0800 Subject: [PATCH 5/5] feat(axdevice): discover linked device factories --- .../build-aarch64-unknown-none-softfloat.toml | 2 - virtualization/arm_vgic/src/v3/vgicr.rs | 87 +++++- virtualization/axdevice/src/device.rs | 8 +- virtualization/axdevice/src/error.rs | 16 +- virtualization/axdevice/src/factory.rs | 194 ++++++++++-- virtualization/axdevice/src/lib.rs | 5 +- virtualization/axdevice/src/registration.rs | 75 +---- .../axdevice_base/src/factory_contract.rs | 190 +++++++++++ virtualization/axdevice_base/src/lib.rs | 5 + virtualization/axdevice_base/src/model.rs | 295 ------------------ virtualization/axvm/src/arch/riscv64/irq.rs | 10 +- virtualization/axvm/src/irq/mod.rs | 8 +- virtualization/axvm/src/vm/prepare.rs | 3 +- .../virtualization-tests/tests/axdevice.rs | 29 +- .../virtualization-tests/tests/axvm_irq.rs | 10 +- 15 files changed, 490 insertions(+), 447 deletions(-) create mode 100644 virtualization/axdevice_base/src/factory_contract.rs delete mode 100644 virtualization/axdevice_base/src/model.rs diff --git a/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml b/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml index bdc97e966e..24a2fdb10d 100644 --- a/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml +++ b/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml @@ -1,10 +1,8 @@ env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } features = [ - "ept-level-4", "ax-driver/virtio-blk", "fs", ] log = "Info" -plat_dyn = true target = "aarch64-unknown-none-softfloat" vm_configs = ["os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml"] diff --git a/virtualization/arm_vgic/src/v3/vgicr.rs b/virtualization/arm_vgic/src/v3/vgicr.rs index bef2bef13a..d5bfaa3d1f 100644 --- a/virtualization/arm_vgic/src/v3/vgicr.rs +++ b/virtualization/arm_vgic/src/v3/vgicr.rs @@ -12,13 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::sync::Arc; use core::ptr; use ax_kspin::SpinNoIrq; use ax_memory_addr::PhysAddr; -use axdevice_base::{AccessWidth, BaseDeviceOps, DeviceResult}; -use axvm_types::{GuestPhysAddr, GuestPhysAddrRange, HostPhysAddr}; -use log::{debug, trace}; +use axdevice_base::{ + AccessWidth, BaseDeviceOps, DeviceBundle, DeviceFactory, DeviceFactoryContext, + DeviceFactoryError, DeviceFactoryResult, DeviceRegistration, DeviceResult, MmioDeviceAdapter, +}; +use axvm_types::{ + EmulatedDeviceConfig, EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange, HostPhysAddr, +}; +use log::{debug, info, trace}; use spin::Once; use super::{ @@ -222,6 +228,81 @@ impl BaseDeviceOps for VGicR { } } +/// Builds partial-passthrough redistributors declared by a VM configuration. +pub struct GpptRedistributorFactory; + +static GPPT_REDISTRIBUTOR_FACTORY: GpptRedistributorFactory = GpptRedistributorFactory; + +axdevice_base::register_device_factory!("gppt-redistributor", GPPT_REDISTRIBUTOR_FACTORY); + +impl DeviceFactory for GpptRedistributorFactory { + fn device_type(&self) -> EmulatedDeviceType { + EmulatedDeviceType::GPPTRedistributor + } + + fn build( + &self, + config: &EmulatedDeviceConfig, + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { + const EXPECTED_ARGS: &str = + "three arguments (cpu count, redistributor stride, physical CPU base)"; + + let cpu_count = config_argument(config, 0, EXPECTED_ARGS)?; + let stride = config_argument(config, 1, EXPECTED_ARGS)?; + let physical_cpu_base = config_argument(config, 2, EXPECTED_ARGS)?; + let mut bundle = DeviceBundle::new(); + + for vcpu_id in 0..cpu_count { + let offset = vcpu_id + .checked_mul(stride) + .ok_or_else(|| invalid_factory_config(config, "redistributor offset overflows"))?; + let base_gpa = config + .base_gpa + .checked_add(offset) + .ok_or_else(|| invalid_factory_config(config, "redistributor address overflows"))?; + let physical_cpu_id = physical_cpu_base + .checked_add(vcpu_id) + .ok_or_else(|| invalid_factory_config(config, "physical CPU ID overflows"))?; + let redistributor = Arc::new(VGicR::new( + base_gpa.into(), + Some(config.length), + physical_cpu_id, + )); + bundle.push(DeviceRegistration::Device(MmioDeviceAdapter::from_arc( + redistributor, + ))); + + info!( + "GPPT redistributor factory built vCPU {vcpu_id} device at {base_gpa:#x} with \ + length {:#x}", + config.length + ); + } + + Ok(bundle) + } +} + +fn config_argument( + config: &EmulatedDeviceConfig, + index: usize, + expected: &'static str, +) -> DeviceFactoryResult { + config + .cfg_list + .get(index) + .copied() + .ok_or_else(|| invalid_factory_config(config, expected)) +} + +fn invalid_factory_config(config: &EmulatedDeviceConfig, detail: &str) -> DeviceFactoryError { + DeviceFactoryError::InvalidConfig { + operation: "build GPPT redistributor", + detail: alloc::format!("device '{}': {detail}", config.name), + } +} + // todo: move the lpi prop table to arm-gic-driver, and find a good interface to use it. /// LPI property table for managing Locality-specific Peripheral Interrupts. pub struct LpiPropTable { diff --git a/virtualization/axdevice/src/device.rs b/virtualization/axdevice/src/device.rs index 12e9176aa9..d3bb122eb9 100644 --- a/virtualization/axdevice/src/device.rs +++ b/virtualization/axdevice/src/device.rs @@ -169,7 +169,6 @@ impl AxVmDevices { EmulatedDeviceType::InterruptController | EmulatedDeviceType::Console | EmulatedDeviceType::IVCChannel - | EmulatedDeviceType::GPPTRedistributor | EmulatedDeviceType::GPPTDistributor | EmulatedDeviceType::GPPTITS | EmulatedDeviceType::FwCfg @@ -510,11 +509,12 @@ impl AxVmDevices { /// already-registered devices in this bundle are rolled back via /// `pop()` + index-key removal. pub fn register_bundle(&mut self, bundle: DeviceBundle) -> DeviceManagerResult { - for (index, pollable) in bundle.pollable.iter().enumerate() { + let bundle = bundle.into_parts(); + for (index, pollable) in bundle.pollable_devices.iter().enumerate() { if self .pollable_devices .iter() - .chain(bundle.pollable[..index].iter()) + .chain(bundle.pollable_devices[..index].iter()) .any(|existing| Arc::ptr_eq(existing, pollable)) { return Err(DeviceManagerError::ResourceConflict { @@ -531,7 +531,7 @@ impl AxVmDevices { return Err(error.into()); } } - self.pollable_devices.extend(bundle.pollable); + self.pollable_devices.extend(bundle.pollable_devices); Ok(()) } diff --git a/virtualization/axdevice/src/error.rs b/virtualization/axdevice/src/error.rs index 9ff54b0cb0..d78a438562 100644 --- a/virtualization/axdevice/src/error.rs +++ b/virtualization/axdevice/src/error.rs @@ -2,7 +2,9 @@ use alloc::string::String; -use axdevice_base::{AccessWidth, BusKind, DeviceError, IrqError, RegistryError}; +use axdevice_base::{ + AccessWidth, BusKind, DeviceError, DeviceFactoryError, IrqError, RegistryError, +}; /// Result type returned by device manager operations. pub type DeviceManagerResult = Result; @@ -90,6 +92,18 @@ pub enum DeviceManagerError { Irq(#[from] IrqError), } +impl From for DeviceManagerError { + fn from(error: DeviceFactoryError) -> Self { + match error { + DeviceFactoryError::InvalidConfig { operation, detail } => { + Self::InvalidConfig { operation, detail } + } + DeviceFactoryError::Device(error) => Self::Device(error), + DeviceFactoryError::Irq(error) => Self::Irq(error), + } + } +} + impl From for DeviceError { fn from(error: DeviceManagerError) -> Self { match error { diff --git a/virtualization/axdevice/src/factory.rs b/virtualization/axdevice/src/factory.rs index 42a7bc2b31..e86f9e73b2 100644 --- a/virtualization/axdevice/src/factory.rs +++ b/virtualization/axdevice/src/factory.rs @@ -12,23 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Extensible construction of emulated devices from VM configuration. +//! Factory discovery and VM-owned construction context. use alloc::{sync::Arc, vec::Vec}; -use axdevice_base::{InterruptTriggerMode, IrqLine}; +use axdevice_base::{DeviceBundle, InterruptTriggerMode, IrqLine, IrqResult}; +pub use axdevice_base::{ + DeviceFactory, DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegister, + DeviceFactoryResult, +}; use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType}; -use crate::{DeviceBundle, DeviceManagerError, DeviceManagerResult}; +use crate::{DeviceManagerError, DeviceManagerResult}; /// Resolves a VM-local interrupt line for a device under construction. pub trait IrqResolver: Send + Sync { /// Resolves `line` with the requested trigger mode. - fn resolve_irq( - &self, - line: usize, - trigger: InterruptTriggerMode, - ) -> DeviceManagerResult; + fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> IrqResult; } /// VM-owned services available while a device factory is building a device. @@ -43,43 +43,56 @@ impl<'a> DeviceBuildContext<'a> { } /// Resolves a VM-local interrupt line. - pub fn resolve_irq( + pub fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> IrqResult { + self.irq_resolver.resolve_irq(line, trigger) + } +} + +impl DeviceFactoryContext for DeviceBuildContext<'_> { + fn resolve_irq( &self, line: usize, trigger: InterruptTriggerMode, - ) -> DeviceManagerResult { - self.irq_resolver.resolve_irq(line, trigger) + ) -> DeviceFactoryResult { + self.irq_resolver + .resolve_irq(line, trigger) + .map_err(DeviceFactoryError::from) } } -/// Builds all capabilities contributed by one emulated device type. -pub trait DeviceFactory: Send + Sync { - /// Returns the configuration type handled by this factory. - fn device_type(&self) -> EmulatedDeviceType; +enum RegisteredFactory { + Static(&'static dyn DeviceFactory), + Owned(Arc), +} - /// Builds a device without modifying the destination device registry. - fn build( - &self, - config: &EmulatedDeviceConfig, - context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult; +impl RegisteredFactory { + fn factory(&self) -> &dyn DeviceFactory { + match self { + Self::Static(factory) => *factory, + Self::Owned(factory) => factory.as_ref(), + } + } } -/// A registry containing at most one factory for each emulated device type. +/// Runtime catalog of factories discovered from the final image. +/// +/// The default VM initialization path populates this catalog from linker +/// registrations. [`Self::register`] remains available for tests and temporary +/// architecture adapters while their factories migrate to static registration. #[derive(Default)] pub struct DeviceFactoryRegistry { - factories: Vec<(EmulatedDeviceType, Arc)>, + factories: Vec<(EmulatedDeviceType, RegisteredFactory)>, } impl DeviceFactoryRegistry { - /// Creates an empty factory registry. + /// Creates an empty factory catalog. pub const fn new() -> Self { Self { factories: Vec::new(), } } - /// Registers a factory, rejecting a duplicate device type. + /// Registers an explicitly provided factory, rejecting a duplicate type. pub fn register(&mut self, factory: Arc) -> DeviceManagerResult { let device_type = factory.device_type(); if self.get(device_type).is_some() { @@ -90,7 +103,51 @@ impl DeviceFactoryRegistry { ), }); } - self.factories.push((device_type, factory)); + self.factories + .push((device_type, RegisteredFactory::Owned(factory))); + Ok(()) + } + + /// Adds statically linked factory registrations to this catalog. + /// + /// The full set is validated before insertion, so duplicate types leave the + /// catalog unchanged. + pub fn register_static_factories( + &mut self, + registers: &[DeviceFactoryRegister], + ) -> DeviceManagerResult { + for (index, register) in registers.iter().enumerate() { + let device_type = register.factory().device_type(); + if self.get(device_type).is_some() { + return Err(DeviceManagerError::ResourceConflict { + operation: "register static device factory", + detail: alloc::format!( + "factory '{}' duplicates an existing factory for device type {device_type}", + register.name() + ), + }); + } + if let Some(existing) = registers[..index] + .iter() + .find(|existing| existing.factory().device_type() == device_type) + { + return Err(DeviceManagerError::ResourceConflict { + operation: "register static device factory", + detail: alloc::format!( + "factories '{}' and '{}' both handle device type {device_type}", + existing.name(), + register.name() + ), + }); + } + } + + for register in registers { + self.factories.push(( + register.factory().device_type(), + RegisteredFactory::Static(register.factory()), + )); + } Ok(()) } @@ -99,10 +156,10 @@ impl DeviceFactoryRegistry { self.factories .iter() .find(|(registered_type, _)| *registered_type == device_type) - .map(|(_, factory)| factory.as_ref()) + .map(|(_, factory)| factory.factory()) } - /// Builds a bundle for `config`. + /// Builds a device bundle for one VM configuration entry. pub fn build( &self, config: &EmulatedDeviceConfig, @@ -118,12 +175,74 @@ impl DeviceFactoryRegistry { ), }); }; - factory.build(config, context) + factory.build(config, context).map_err(Into::into) + } +} + +/// Returns factory registrations collected from the final linker image. +#[cfg(any(target_os = "none", target_env = "musl"))] +pub fn linker_device_factory_registers() -> DeviceManagerResult<&'static [DeviceFactoryRegister]> { + unsafe extern "C" { + fn __saxdevice_factory(); + fn __eaxdevice_factory(); + } + + let start = __saxdevice_factory as *const () as usize; + let end = __eaxdevice_factory as *const () as usize; + if start > end { + return Err(DeviceManagerError::InvalidConfig { + operation: "read device factory linker section", + detail: alloc::format!("section start {start:#x} is after section end {end:#x}"), + }); + } + + let byte_len = end - start; + if byte_len == 0 { + return Ok(&[]); + } + + let alignment = core::mem::align_of::(); + if !start.is_multiple_of(alignment) { + return Err(DeviceManagerError::InvalidConfig { + operation: "read device factory linker section", + detail: alloc::format!("section start {start:#x} is not aligned to {alignment} bytes"), + }); + } + + let entry_size = core::mem::size_of::(); + if !byte_len.is_multiple_of(entry_size) { + return Err(DeviceManagerError::InvalidConfig { + operation: "read device factory linker section", + detail: alloc::format!( + "section length {byte_len:#x} is not a multiple of entry size {entry_size}" + ), + }); } + + let entry_count = byte_len / entry_size; + // SAFETY: `runtime.ld` places only `DeviceFactoryRegister` values between + // these symbols, keeps the input sections, and aligns their start. The + // checks above validate the remaining range and layout preconditions. + Ok(unsafe { core::slice::from_raw_parts(start as *const DeviceFactoryRegister, entry_count) }) +} + +/// Returns no linker registrations on targets without the runtime linker script. +#[cfg(not(any(target_os = "none", target_env = "musl")))] +pub fn linker_device_factory_registers() -> DeviceManagerResult<&'static [DeviceFactoryRegister]> { + Ok(&[]) +} + +/// Registers every factory collected from the final linker image. +pub fn register_linker_factories(registry: &mut DeviceFactoryRegistry) -> DeviceManagerResult { + registry.register_static_factories(linker_device_factory_registers()?) } struct MetaDeviceFactory; +static META_DEVICE_FACTORY: MetaDeviceFactory = MetaDeviceFactory; + +axdevice_base::register_device_factory!("meta", META_DEVICE_FACTORY); + impl DeviceFactory for MetaDeviceFactory { fn device_type(&self) -> EmulatedDeviceType { EmulatedDeviceType::Dummy @@ -132,13 +251,22 @@ impl DeviceFactory for MetaDeviceFactory { fn build( &self, _config: &EmulatedDeviceConfig, - _context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { Ok(DeviceBundle::new()) } } -/// Registers device factories that do not depend on an architecture backend. +/// Registers built-in factories on targets without the runtime linker script. +#[cfg(not(any(target_os = "none", target_env = "musl")))] pub fn register_builtin_factories(registry: &mut DeviceFactoryRegistry) -> DeviceManagerResult { - registry.register(Arc::new(MetaDeviceFactory)) + static META_REGISTER: DeviceFactoryRegister = + DeviceFactoryRegister::new("meta", &META_DEVICE_FACTORY); + registry.register_static_factories(core::slice::from_ref(&META_REGISTER)) +} + +/// ArceOS image built-ins are discovered from `.axdevice.factory`. +#[cfg(any(target_os = "none", target_env = "musl"))] +pub fn register_builtin_factories(_registry: &mut DeviceFactoryRegistry) -> DeviceManagerResult { + Ok(()) } diff --git a/virtualization/axdevice/src/lib.rs b/virtualization/axdevice/src/lib.rs index be23fcf454..649c786af7 100644 --- a/virtualization/axdevice/src/lib.rs +++ b/virtualization/axdevice/src/lib.rs @@ -50,8 +50,9 @@ pub use config::AxVmDeviceConfig; pub use device::AxVmDevices; pub use error::{DeviceManagerError, DeviceManagerResult}; pub use factory::{ - DeviceBuildContext, DeviceFactory, DeviceFactoryRegistry, IrqResolver, - register_builtin_factories, + DeviceBuildContext, DeviceFactory, DeviceFactoryContext, DeviceFactoryError, + DeviceFactoryRegister, DeviceFactoryRegistry, DeviceFactoryResult, IrqResolver, + linker_device_factory_registers, register_builtin_factories, register_linker_factories, }; pub use fw_cfg::{ FwCfg, FwCfgInterruptConfig, FwCfgPciConfig, FwCfgPlatformConfig, FwCfgRamRegion, diff --git a/virtualization/axdevice/src/registration.rs b/virtualization/axdevice/src/registration.rs index ef1ea0e65c..f4fc8ed82d 100644 --- a/virtualization/axdevice/src/registration.rs +++ b/virtualization/axdevice/src/registration.rs @@ -12,77 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Transactional device registration types. +//! Compatibility exports for device factory registration capabilities. -use alloc::{sync::Arc, vec::Vec}; - -use axdevice_base::Device; - -use crate::DeviceManagerResult; - -/// A device capability that can be polled by the VM runtime. -pub trait PollableDeviceOps: Send + Sync { - /// Advances the device using the current monotonic time in nanoseconds. - fn poll(&self, now_ns: u64) -> DeviceManagerResult; -} - -/// One strongly typed capability contributed by a device. -#[non_exhaustive] -pub enum DeviceRegistration { - /// A device implementing the unified [`Device`] trait. - Device(Arc), - /// A capability that requires periodic polling. - Pollable(Arc), -} - -/// A set of device capabilities that must be registered atomically. -/// -/// The contained registration lists are private so callers cannot bypass -/// [`DeviceRegistration`] when adding future capability kinds. -#[derive(Default)] -pub struct DeviceBundle { - pub(crate) devices: Vec>, - pub(crate) pollable: Vec>, -} - -impl DeviceBundle { - /// Creates an empty bundle. - pub const fn new() -> Self { - Self { - devices: Vec::new(), - pollable: Vec::new(), - } - } - - /// Creates a bundle containing one registration. - pub fn from_registration(registration: DeviceRegistration) -> Self { - let mut bundle = Self::new(); - bundle.push(registration); - bundle - } - - /// Adds one capability to this bundle. - pub fn push(&mut self, registration: DeviceRegistration) { - match registration { - DeviceRegistration::Device(device) => self.devices.push(device), - DeviceRegistration::Pollable(device) => self.pollable.push(device), - } - } - - /// Adds one capability and returns the bundle for builder-style use. - pub fn with_registration(mut self, registration: DeviceRegistration) -> Self { - self.push(registration); - self - } - - /// Returns whether this bundle contains no capabilities. - pub fn is_empty(&self) -> bool { - self.devices.is_empty() && self.pollable.is_empty() - } -} - -impl From for DeviceBundle { - fn from(registration: DeviceRegistration) -> Self { - Self::from_registration(registration) - } -} +pub use axdevice_base::{DeviceBundle, DeviceRegistration, PollableDeviceOps}; diff --git a/virtualization/axdevice_base/src/factory_contract.rs b/virtualization/axdevice_base/src/factory_contract.rs new file mode 100644 index 0000000000..f3bf6a4284 --- /dev/null +++ b/virtualization/axdevice_base/src/factory_contract.rs @@ -0,0 +1,190 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device factory capabilities shared by device implementations and VM containers. + +use alloc::{string::String, sync::Arc, vec::Vec}; + +use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, InterruptTriggerMode}; + +use crate::{Device, DeviceError, DeviceResult, IrqError, IrqLine}; + +/// Errors reported while a factory validates configuration or builds devices. +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +pub enum DeviceFactoryError { + /// The VM device configuration is malformed or inconsistent. + #[error("invalid device factory configuration for {operation}: {detail}")] + InvalidConfig { + /// The factory operation that rejected the configuration. + operation: &'static str, + /// Diagnostic detail describing the invalid configuration. + detail: String, + }, + /// A constructed device or device adapter reported an error. + #[error(transparent)] + Device(#[from] DeviceError), + /// Resolving a VM-local interrupt line failed. + #[error(transparent)] + Irq(#[from] IrqError), +} + +/// Result type returned by device factory operations. +pub type DeviceFactoryResult = Result; + +/// VM-owned services available while a device factory builds a device. +pub trait DeviceFactoryContext: Send + Sync { + /// Resolves a VM-local interrupt line with the requested trigger mode. + fn resolve_irq( + &self, + line: usize, + trigger: InterruptTriggerMode, + ) -> DeviceFactoryResult; +} + +/// A device capability that can be polled by the VM runtime. +pub trait PollableDeviceOps: Send + Sync { + /// Advances the device using the current monotonic time in nanoseconds. + fn poll(&self, now_ns: u64) -> DeviceResult; +} + +/// One strongly typed capability contributed by a device factory. +#[non_exhaustive] +pub enum DeviceRegistration { + /// A device implementing the unified [`Device`] trait. + Device(Arc), + /// A capability that requires periodic polling. + Pollable(Arc), +} + +/// Device capability lists consumed by a VM device container. +pub struct DeviceBundleParts { + /// Devices that participate in bus routing and resource registration. + pub devices: Vec>, + /// Device capabilities that require periodic polling. + pub pollable_devices: Vec>, +} + +/// A set of device capabilities built from one VM device configuration. +#[derive(Default)] +pub struct DeviceBundle { + devices: Vec>, + pollable: Vec>, +} + +impl DeviceBundle { + /// Creates an empty bundle. + pub const fn new() -> Self { + Self { + devices: Vec::new(), + pollable: Vec::new(), + } + } + + /// Creates a bundle containing one registration. + pub fn from_registration(registration: DeviceRegistration) -> Self { + let mut bundle = Self::new(); + bundle.push(registration); + bundle + } + + /// Adds one capability to this bundle. + pub fn push(&mut self, registration: DeviceRegistration) { + match registration { + DeviceRegistration::Device(device) => self.devices.push(device), + DeviceRegistration::Pollable(device) => self.pollable.push(device), + } + } + + /// Adds one capability and returns the bundle for builder-style use. + pub fn with_registration(mut self, registration: DeviceRegistration) -> Self { + self.push(registration); + self + } + + /// Returns whether this bundle contains no capabilities. + pub fn is_empty(&self) -> bool { + self.devices.is_empty() && self.pollable.is_empty() + } + + /// Splits the bundle into the capabilities consumed by the VM device container. + pub fn into_parts(self) -> DeviceBundleParts { + DeviceBundleParts { + devices: self.devices, + pollable_devices: self.pollable, + } + } +} + +impl From for DeviceBundle { + fn from(registration: DeviceRegistration) -> Self { + Self::from_registration(registration) + } +} + +/// Builds all capabilities contributed by one emulated device type. +pub trait DeviceFactory: Send + Sync { + /// Returns the VM configuration type handled by this factory. + fn device_type(&self) -> EmulatedDeviceType; + + /// Builds device instances from one VM device configuration. + fn build( + &self, + config: &EmulatedDeviceConfig, + context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult; +} + +/// Static factory registration entry collected from the final image. +/// +/// This is an internal Rust-image layout contract, not a cross-language ABI. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DeviceFactoryRegister { + name: &'static str, + factory: &'static dyn DeviceFactory, +} + +impl DeviceFactoryRegister { + /// Creates a static factory registration entry. + pub const fn new(name: &'static str, factory: &'static dyn DeviceFactory) -> Self { + Self { name, factory } + } + + /// Returns the human-readable factory name used for diagnostics. + pub const fn name(&self) -> &'static str { + self.name + } + + /// Returns the registered factory. + pub const fn factory(&self) -> &'static dyn DeviceFactory { + self.factory + } +} + +/// Registers a device factory in the `.axdevice.factory` linker section. +/// +/// The concrete device crate must be linked into the final image. The final +/// linker script must retain `.axdevice.factory*` and export the section's +/// start and end symbols. +#[macro_export] +macro_rules! register_device_factory { + ($name:expr, $factory:expr $(,)?) => { + const _: () = { + #[unsafe(link_section = ".axdevice.factory")] + #[used] + static FACTORY_REGISTER: $crate::DeviceFactoryRegister = + $crate::DeviceFactoryRegister::new($name, &$factory); + }; + }; +} diff --git a/virtualization/axdevice_base/src/lib.rs b/virtualization/axdevice_base/src/lib.rs index eb5d4e852b..408e7f9d78 100644 --- a/virtualization/axdevice_base/src/lib.rs +++ b/virtualization/axdevice_base/src/lib.rs @@ -596,7 +596,12 @@ pub trait BusRouter { // --------------------------------------------------------------------------- mod adapter; +mod factory_contract; mod irq; pub use adapter::{MmioDeviceAdapter, PortDeviceAdapter, SysRegDeviceAdapter}; +pub use factory_contract::{ + DeviceBundle, DeviceBundleParts, DeviceFactory, DeviceFactoryContext, DeviceFactoryError, + DeviceFactoryRegister, DeviceFactoryResult, DeviceRegistration, PollableDeviceOps, +}; pub use irq::{IrqError, IrqLine, IrqResult, IrqSink}; diff --git a/virtualization/axdevice_base/src/model.rs b/virtualization/axdevice_base/src/model.rs deleted file mode 100644 index e286def874..0000000000 --- a/virtualization/axdevice_base/src/model.rs +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2025 The Axvisor Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Core emulated device model traits and errors. - -use alloc::{rc::Rc, string::String, vec::Vec}; -use core::{any::Any, fmt}; - -use ax_errno::AxError; -use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType}; - -use crate::{ - AccessWidth, BusAccess, BusAddress, BusKind, BusResponse, DeviceCapabilities, Resource, -}; - -/// Unique identifier for a device instance inside one registry. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId(usize); - -impl DeviceId { - /// Creates a device identifier from a raw numeric value. - pub const fn new(id: usize) -> Self { - Self(id) - } - - /// Returns the raw numeric identifier. - pub const fn raw(self) -> usize { - self.0 - } -} - -/// Common registry-facing metadata stored by native device implementations. -#[derive(Debug, Clone)] -pub struct DeviceMeta { - id: DeviceId, - name: String, - resources: Vec, - capabilities: DeviceCapabilities, -} - -impl DeviceMeta { - /// Creates device metadata from raw parts. - pub fn new( - id: DeviceId, - name: String, - resources: Vec, - capabilities: DeviceCapabilities, - ) -> Self { - Self { - id, - name, - resources, - capabilities, - } - } - - /// Returns the registry-local device identifier. - pub const fn id(&self) -> DeviceId { - self.id - } - - /// Returns the human-readable device instance name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns resources declared by this device. - pub fn resources(&self) -> &[Resource] { - &self.resources - } - - /// Returns device capability flags. - pub const fn capabilities(&self) -> DeviceCapabilities { - self.capabilities - } -} - -/// Error type used by the new device abstraction layer. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeviceError { - /// No device was registered for the requested bus access. - DeviceNotFound { - /// Bus namespace searched by the router. - kind: BusKind, - /// Address that missed all registered device resources. - address: BusAddress, - }, - /// A device with the same registry-local identifier already exists. - DuplicateDeviceId { - /// Duplicated identifier. - id: DeviceId, - }, - /// The bus kind and concrete bus address do not describe the same namespace. - BusAddressMismatch { - /// Expected or requested bus namespace. - kind: BusKind, - /// Address carrying a different bus namespace. - address: BusAddress, - }, - /// A resource conflicts with an already registered resource. - ResourceConflict { - /// Existing registered resource. - existing: Resource, - /// Newly requested resource. - requested: Resource, - }, - /// The access width is not accepted by the device or bus. - InvalidAccessWidth { - /// Rejected access width. - width: AccessWidth, - }, - /// The address does not belong to the resource handled by the selected device. - AddressOutOfRange { - /// Bus namespace used by the access. - kind: BusKind, - /// Rejected address. - address: BusAddress, - }, - /// A write was attempted on a read-only register. - ReadOnly { - /// Bus namespace used by the access. - kind: BusKind, - /// Rejected address. - address: BusAddress, - }, - /// A read was attempted on a write-only register. - WriteOnly { - /// Bus namespace used by the access. - kind: BusKind, - /// Rejected address. - address: BusAddress, - }, - /// The operation is validly formed but unsupported by this device/backend. - UnsupportedOperation, - /// Error returned by an existing backend while it is adapted into the new model. - Backend(AxError), -} - -impl From for DeviceError { - fn from(error: AxError) -> Self { - Self::Backend(error) - } -} - -impl fmt::Display for DeviceError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DeviceNotFound { kind, address } => { - write!(f, "device not found for {kind:?} address {address:?}") - } - Self::DuplicateDeviceId { id } => { - write!(f, "duplicate device id {:?}", id) - } - Self::BusAddressMismatch { kind, address } => { - write!(f, "bus kind {kind:?} does not match address {address:?}") - } - Self::ResourceConflict { - existing, - requested, - } => write!( - f, - "device resource conflict: existing {existing:?}, requested {requested:?}" - ), - Self::InvalidAccessWidth { width } => { - write!(f, "invalid device access width {width:?}") - } - Self::AddressOutOfRange { kind, address } => { - write!( - f, - "{kind:?} address {address:?} is outside the selected device resource" - ) - } - Self::ReadOnly { kind, address } => { - write!(f, "write to read-only {kind:?} address {address:?}") - } - Self::WriteOnly { kind, address } => { - write!(f, "read from write-only {kind:?} address {address:?}") - } - Self::UnsupportedOperation => write!(f, "unsupported device operation"), - Self::Backend(error) => write!(f, "device backend error: {error}"), - } - } -} - -/// Result type for the new device abstraction layer. -pub type DeviceResult = Result; - -/// Unified interface for an emulated device instance. -pub trait DeviceOps: Any { - /// Returns the registry-local device identifier. - fn id(&self) -> DeviceId; - - /// Returns a human-readable device instance name. - fn name(&self) -> &str; - - /// Returns all resources occupied or requested by this device. - fn resources(&self) -> &[Resource]; - - /// Returns optional capabilities supported by this device. - fn capabilities(&self) -> DeviceCapabilities; - - /// Handles a normalized bus access. - fn access(&self, access: BusAccess) -> DeviceResult; - - /// Resets the device state. - fn reset(&self) -> DeviceResult { - Ok(()) - } - - /// Suspends the device state. - fn suspend(&self) -> DeviceResult { - Ok(()) - } - - /// Resumes the device state. - fn resume(&self) -> DeviceResult { - Ok(()) - } -} - -/// Build-time context provided to device factories. -pub trait DeviceBuildContext { - /// Allocates a registry-local device identifier. - fn alloc_device_id(&mut self) -> DeviceId; -} - -/// Factory that constructs native device model instances from VM configuration. -pub trait DeviceFactory: Sync { - /// Returns the emulated device type handled by this factory. - fn ty(&self) -> EmulatedDeviceType; - - /// Builds one or more native device instances from a VM device config. - fn build( - &self, - ctx: &mut dyn DeviceBuildContext, - config: &EmulatedDeviceConfig, - ) -> DeviceResult>>; -} - -/// Static factory registration entry collected from the final image. -/// -/// Entries of this type may be placed in the `.axdevice.factory` linker -/// section by concrete device crates. `axdevice` consumes the collected slice -/// to build a runtime factory catalog without knowing concrete device types. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct DeviceFactoryRegister { - name: &'static str, - factory: &'static dyn DeviceFactory, -} - -impl DeviceFactoryRegister { - /// Creates a static factory registration entry. - pub const fn new(name: &'static str, factory: &'static dyn DeviceFactory) -> Self { - Self { name, factory } - } - - /// Returns the human-readable factory name used for diagnostics. - pub const fn name(&self) -> &'static str { - self.name - } - - /// Returns the factory referenced by this registration entry. - pub const fn factory(&self) -> &'static dyn DeviceFactory { - self.factory - } -} - -/// Registers a native device factory in the `.axdevice.factory` linker section. -/// -/// The concrete device crate remains responsible for ensuring that the crate is -/// linked into the final image. The final linker script must keep -/// `.axdevice.factory*` and export the start/end symbols consumed by `axdevice`. -#[macro_export] -macro_rules! register_device_factory { - ($name:expr, $factory:expr $(,)?) => { - const _: () = { - #[unsafe(link_section = ".axdevice.factory")] - #[used] - static FACTORY_REGISTER: $crate::DeviceFactoryRegister = - $crate::DeviceFactoryRegister::new($name, &$factory); - }; - }; -} diff --git a/virtualization/axvm/src/arch/riscv64/irq.rs b/virtualization/axvm/src/arch/riscv64/irq.rs index fc4b540753..362c22f995 100644 --- a/virtualization/axvm/src/arch/riscv64/irq.rs +++ b/virtualization/axvm/src/arch/riscv64/irq.rs @@ -17,8 +17,8 @@ use alloc::sync::Arc; use axdevice::{ - DeviceBuildContext, DeviceBundle, DeviceFactory, DeviceFactoryRegistry, DeviceManagerError, - DeviceManagerResult, DeviceRegistration, MmioDeviceAdapter, + DeviceBundle, DeviceFactory, DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegistry, + DeviceFactoryResult, DeviceRegistration, MmioDeviceAdapter, }; use axdevice_base::{IrqError, IrqLineId, IrqResult, IrqSink}; use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, VMInterruptMode}; @@ -72,13 +72,13 @@ impl DeviceFactory for RiscvPlicFactory { fn build( &self, config: &EmulatedDeviceConfig, - _context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { if config.base_gpa != self.base_gpa || config.length != self.length || config.cfg_list.as_slice() != [self.contexts_num] { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build virtual PLIC", detail: alloc::format!( "factory configuration does not match device '{}'", diff --git a/virtualization/axvm/src/irq/mod.rs b/virtualization/axvm/src/irq/mod.rs index 5ae6c5af25..e388c42dda 100644 --- a/virtualization/axvm/src/irq/mod.rs +++ b/virtualization/axvm/src/irq/mod.rs @@ -16,7 +16,7 @@ use alloc::sync::Arc; -use axdevice::{DeviceManagerResult, IrqResolver}; +use axdevice::IrqResolver; use axdevice_base::{InterruptTriggerMode, IrqError, IrqLine, IrqLineId, IrqResult, IrqSink}; use axvm_types::VMInterruptMode; @@ -145,11 +145,7 @@ impl Default for InterruptFabric { } impl IrqResolver for InterruptFabric { - fn resolve_irq( - &self, - line: usize, - trigger: InterruptTriggerMode, - ) -> DeviceManagerResult { + fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> IrqResult { Ok(IrqLine::new( IrqLineId(line), trigger, diff --git a/virtualization/axvm/src/vm/prepare.rs b/virtualization/axvm/src/vm/prepare.rs index c2fa477eb5..74e5927ac0 100644 --- a/virtualization/axvm/src/vm/prepare.rs +++ b/virtualization/axvm/src/vm/prepare.rs @@ -6,7 +6,7 @@ pub(crate) mod vcpus; use alloc::{format, sync::Arc}; -use axdevice::{DeviceFactoryRegistry, register_builtin_factories}; +use axdevice::{DeviceFactoryRegistry, register_builtin_factories, register_linker_factories}; use self::{devices::PreparedDevices, vcpus::PreparedVcpus}; use super::{AxVM, AxVMResources}; @@ -56,6 +56,7 @@ impl AxVM { pub(crate) fn default_device_factories() -> AxVmResult { let mut factories = DeviceFactoryRegistry::new(); register_builtin_factories(&mut factories)?; + register_linker_factories(&mut factories)?; Ok(factories) } diff --git a/virtualization/test_crates/virtualization-tests/tests/axdevice.rs b/virtualization/test_crates/virtualization-tests/tests/axdevice.rs index 2587b7090b..875faff5bd 100644 --- a/virtualization/test_crates/virtualization-tests/tests/axdevice.rs +++ b/virtualization/test_crates/virtualization-tests/tests/axdevice.rs @@ -16,14 +16,14 @@ use std::sync::{Arc, Mutex}; use axdevice::{ AxVmDeviceConfig, AxVmDevices, DeviceBuildContext, DeviceBundle, DeviceFactory, - DeviceFactoryRegistry, DeviceManagerError, DeviceManagerResult, DeviceRegistration, - IrqResolver, MmioDeviceAdapter, PollableDeviceOps, PortDeviceAdapter, SysRegDeviceAdapter, - register_builtin_factories, + DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegistry, DeviceFactoryResult, + DeviceManagerError, DeviceRegistration, IrqResolver, MmioDeviceAdapter, PollableDeviceOps, + PortDeviceAdapter, SysRegDeviceAdapter, register_builtin_factories, }; use axdevice_base::{ AccessWidth, BaseDeviceOps, Device, DeviceError, DeviceRegistry as _, DeviceResult, - InterruptTriggerMode, InvalidResourceReason, IrqError, IrqLine, IrqLineId, Port, PortRange, - RegistryError, Resource, SysRegAddr, SysRegAddrRange, + InterruptTriggerMode, InvalidResourceReason, IrqError, IrqLine, IrqLineId, IrqResult, Port, + PortRange, RegistryError, Resource, SysRegAddr, SysRegAddrRange, }; use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange}; @@ -212,7 +212,7 @@ impl BaseDeviceOps for MockMmioPollableDevice { } impl PollableDeviceOps for MockMmioPollableDevice { - fn poll(&self, now_ns: u64) -> DeviceManagerResult { + fn poll(&self, now_ns: u64) -> DeviceResult { self.polled_at.lock().unwrap().push(now_ns); Ok(()) } @@ -282,17 +282,12 @@ fn device_config( struct RejectingIrqResolver; impl IrqResolver for RejectingIrqResolver { - fn resolve_irq( - &self, - line: usize, - _trigger: InterruptTriggerMode, - ) -> DeviceManagerResult { + fn resolve_irq(&self, line: usize, _trigger: InterruptTriggerMode) -> IrqResult { Err(IrqError::Unsupported { line: IrqLineId(line), operation: "resolve test IRQ", detail: "test resolver rejects every line".into(), - } - .into()) + }) } } @@ -306,16 +301,16 @@ impl DeviceFactory for MockMmioFactory { fn build( &self, config: &EmulatedDeviceConfig, - _context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { let Some(end) = config.base_gpa.checked_add(config.length) else { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build mock MMIO device", detail: "device address range overflows".into(), }); }; if config.length == 0 { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build mock MMIO device", detail: "device range is empty".into(), }); diff --git a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs index 2965a5fd27..b9009e4e61 100644 --- a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs +++ b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs @@ -17,8 +17,8 @@ use std::sync::{Arc, Mutex, Weak}; use ax_plat::{console::ConsoleIf, time::TimeIf}; use axdevice::{ AxVmDeviceConfig, AxVmDevices, DeviceBuildContext, DeviceBundle, DeviceFactory, - DeviceFactoryRegistry, DeviceManagerError, DeviceManagerResult, DeviceRegistration, - IrqResolver, MmioDeviceAdapter, + DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegistry, DeviceFactoryResult, + DeviceManagerError, DeviceRegistration, IrqResolver, MmioDeviceAdapter, }; use axdevice_base::{ AccessWidth, BaseDeviceOps, DeviceResult, InterruptTriggerMode, IrqError, IrqLine, IrqLineId, @@ -153,10 +153,10 @@ impl DeviceFactory for IrqMmioFactory { fn build( &self, config: &EmulatedDeviceConfig, - context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { let Some(end) = config.base_gpa.checked_add(config.length) else { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build IRQ MMIO test device", detail: "device address range overflows".into(), });