refactor: extract ports/drivers/bins layers — clean dependency inversion

Separate abstractions from implementations following DIP:

  servers/              drivers/
      │                     │
      ▼                     ▼
    ports/  (trait-only crates, zero deps)

New crates:

ports/ (abstractions):
  - port-framebuffer: Framebuffer trait
  - port-block-device: BlockDevice trait + SectorId
  - port-input-device: InputDevice trait + InputEvent types

drivers/ (implementations depend on ports/):
  - driver-framebuffer-virtio: VirtIO GPU framebuffer
  - driver-framebuffer-mock: test double
  - driver-block-virtio: VirtIO block device

bins/ (composition roots wire server + driver):
  - display-qemu: DisplayServer<VirtioFramebuffer>
  - storage-qemu: storage server with VirtIO block

Servers no longer own their hardware traits or driver code.
Neither server nor driver depends on the other — both point
inward to the port abstraction.
This commit is contained in:
2026-10-18 13:13:53 +02:00
parent 38f20cc3c4
commit 5766c6f7f7
39 changed files with 272 additions and 217 deletions

View File

@@ -3,17 +3,9 @@ name = "storage-server"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "storage-server"
test = false
bench = false
[features]
default = ["qemu-virt"]
qemu-virt = []
[dependencies]
types = { workspace = true }
userspace = { workspace = true }
userspace-rt = { workspace = true }
server-framework = { workspace = true }
port-block-device = { workspace = true }
driver-block-virtio = { workspace = true }

View File

@@ -1,4 +1,4 @@
use crate::block_device::{BlockDevice, SectorId};
use port_block_device::{BlockDevice, SectorId};
const SECTOR_SIZE: usize = 512;
const MAX_SECTORS: usize = 64;

View File

@@ -1,3 +1,2 @@
pub mod mock_block;
pub mod mock_fs;
pub mod virtio_blk;

View File

@@ -1,183 +0,0 @@
use crate::block_device::{BlockDevice, SectorId};
const QUEUE_SIZE: usize = 8;
const SECTOR_SIZE: usize = 512;
const MMIO_BASE: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000;
const REQ_HDR_VA: usize = 0x23000;
// Physical addresses are written to a known location by the adapter at boot
// Layout at 0x24000: [req_hdr_pa: u64, status_pa: u64, sector_buf_pa: u64]
const PHYS_INFO_VA: usize = 0x24000;
#[repr(C)]
#[derive(Clone, Copy)]
struct VirtqDesc {
addr: u64,
len: u32,
flags: u16,
next: u16,
}
#[repr(C)]
struct VirtqAvail {
flags: u16,
idx: u16,
ring: [u16; QUEUE_SIZE],
used_event: u16,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct VirtqUsedElem {
id: u32,
len: u32,
}
#[repr(C)]
struct VirtqUsed {
flags: u16,
idx: u16,
ring: [VirtqUsedElem; QUEUE_SIZE],
}
#[repr(C)]
struct VirtioBlkReqHeader {
req_type: u32,
reserved: u32,
sector: u64,
}
fn mmio_read(offset: usize) -> u32 {
unsafe { core::ptr::read_volatile((MMIO_BASE + offset) as *const u32) }
}
fn mmio_write(offset: usize, val: u32) {
unsafe { core::ptr::write_volatile((MMIO_BASE + offset) as *mut u32, val) }
}
fn descs() -> *mut VirtqDesc {
VQUEUE_VA as *mut VirtqDesc
}
fn avail() -> *mut VirtqAvail {
(VQUEUE_VA + QUEUE_SIZE * 16) as *mut VirtqAvail
}
fn used() -> *mut VirtqUsed {
(VQUEUE_VA + 0x1000) as *mut VirtqUsed
}
fn req_header() -> *mut VirtioBlkReqHeader {
REQ_HDR_VA as *mut VirtioBlkReqHeader
}
fn req_status() -> *mut u8 {
(REQ_HDR_VA + 16) as *mut u8
}
struct PhysAddrs {
req_hdr: u64,
status: u64,
sector_buf: u64,
}
fn read_phys_addrs() -> PhysAddrs {
unsafe {
let base = PHYS_INFO_VA as *const u64;
PhysAddrs {
req_hdr: core::ptr::read_volatile(base),
status: core::ptr::read_volatile(base.add(1)),
sector_buf: core::ptr::read_volatile(base.add(2)),
}
}
}
pub struct VirtioBlockDevice {
phys: PhysAddrs,
}
impl VirtioBlockDevice {
pub fn new() -> Self {
Self { phys: read_phys_addrs() }
}
}
impl BlockDevice for VirtioBlockDevice {
fn sector_size(&self) -> usize {
SECTOR_SIZE
}
fn sector_count(&self) -> usize {
0xFFFF
}
fn read_sector(&mut self, sector: SectorId, buf: &mut [u8]) -> bool {
if buf.len() < SECTOR_SIZE { return false; }
// Write to the local VA — device reads via physical address
unsafe {
let hdr = req_header();
(*hdr).req_type = 0;
(*hdr).reserved = 0;
(*hdr).sector = sector.raw() as u64;
core::ptr::write_volatile(req_status(), 0xFF);
// Sector buffer: use a fixed area at REQ_HDR_VA + 32
let sector_buf_va = (REQ_HDR_VA + 32) as *mut u8;
let d = descs();
(*d.add(0)) = VirtqDesc {
addr: self.phys.req_hdr,
len: 16,
flags: 1,
next: 1,
};
(*d.add(1)) = VirtqDesc {
addr: self.phys.sector_buf,
len: SECTOR_SIZE as u32,
flags: 2 | 1,
next: 2,
};
(*d.add(2)) = VirtqDesc {
addr: self.phys.status,
len: 1,
flags: 2,
next: 0,
};
let a = avail();
let avail_idx = core::ptr::read_volatile(&(*a).idx);
(*a).ring[(avail_idx as usize) % QUEUE_SIZE] = 0;
core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
core::ptr::write_volatile(&mut (*a).idx, avail_idx.wrapping_add(1));
core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
mmio_write(0x050, 0);
let expected = avail_idx.wrapping_add(1);
let u = used();
let mut timeout = 10_000_000u32;
while core::ptr::read_volatile(&(*u).idx) != expected {
core::hint::spin_loop();
timeout -= 1;
if timeout == 0 { return false; }
}
if core::ptr::read_volatile(req_status()) != 0 {
return false;
}
// Copy from sector buffer VA to caller's buffer
core::ptr::copy_nonoverlapping(sector_buf_va, buf.as_mut_ptr(), SECTOR_SIZE);
}
true
}
fn write_sector(&mut self, _sector: SectorId, _buf: &[u8]) -> bool {
false
}
}

View File

@@ -1,19 +0,0 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SectorId(u32);
impl SectorId {
pub const fn new(id: u32) -> Self {
Self(id)
}
pub const fn raw(&self) -> u32 {
self.0
}
}
pub trait BlockDevice {
fn sector_size(&self) -> usize;
fn sector_count(&self) -> usize;
fn read_sector(&mut self, sector: SectorId, buf: &mut [u8]) -> bool;
fn write_sector(&mut self, sector: SectorId, buf: &[u8]) -> bool;
}

View File

@@ -1,4 +1,4 @@
use crate::block_device::{BlockDevice, SectorId};
use port_block_device::{BlockDevice, SectorId};
use crate::filesystem::{DirEntry, FileHandle, FileName, FileSystem};
const SECTOR_SIZE: usize = 512;

View File

@@ -1,4 +1,4 @@
use crate::block_device::{BlockDevice, SectorId};
use port_block_device::{BlockDevice, SectorId};
use crate::filesystem::{DirEntry, FileHandle, FileName, FileSystem};
const SECTOR_SIZE: usize = 512;

View File

@@ -1,7 +1,6 @@
#![no_std]
pub mod adapters;
pub mod block_device;
pub mod filesystem;
pub mod fs;
pub mod protocol;
@@ -30,7 +29,7 @@ fn debug_print(s: &[u8]) {
pub fn run() -> ! {
debug_print(b"storage: started\n");
let device = adapters::virtio_blk::VirtioBlockDevice::new();
let device = driver_block_virtio::VirtioBlockDevice::new();
let fat16 = fs::fat16::Fat16::mount(device);
if fat16.is_none() {
debug_print(b"storage: FAT16 mount failed\n");
@@ -71,7 +70,7 @@ pub fn run() -> ! {
}
}
fn read_entire_file<B: block_device::BlockDevice>(
fn read_entire_file<B: port_block_device::BlockDevice>(
fat: &mut fs::fat16::Fat16<B>,
filename: &[u8],
out: &mut [u8],

View File

@@ -1,9 +0,0 @@
#![no_std]
#![no_main]
extern crate userspace_rt;
#[unsafe(no_mangle)]
extern "C" fn main() {
storage_server::run();
}

View File

@@ -1,5 +1,5 @@
use crate::adapters::mock_block::MockBlockDevice;
use crate::block_device::{BlockDevice, SectorId};
use port_block_device::{BlockDevice, SectorId};
use crate::filesystem::{DirEntry, FileSystem};
use crate::fs::ext4::Ext4;

View File

@@ -1,5 +1,5 @@
use crate::adapters::mock_block::MockBlockDevice;
use crate::block_device::{BlockDevice, SectorId};
use port_block_device::{BlockDevice, SectorId};
use crate::filesystem::{DirEntry, FileSystem};
use crate::fs::fat16::Fat16;