Architecture recommendation for generic Address type

Hi, I'm writing an OS in rust and need to deal with the different address types, Physical, Virtual and Ports. Generally the physical and virtual addresses are fairly easy to handle with a generic read<T> / write<T> function that cast the raw pointer to the correct type. IO ports on the other hand generally only accept i8, i16, i32 and i64 and you need to use specific instructions in, out instructions, and load up the respective CPU registers. An example of what I tried (and that shows this) is:

pub trait PortIO<T: RegisterType> {
    fn read(&self) -> Result<T, ErrorCode>;
    fn write(&self, value: T) -> Result<(), ErrorCode>;
}

#[cfg(target_arch = "x86_64")]
impl PortIO<i8> for Address {
    fn read(&self) -> Result<i8, ErrorCode> {
        let addr = self.value()?;
        let mut value;
        unsafe {
            asm!(
                "in al, dx",
                in("rdx") addr,
                out("al") value
            )
        }
        Ok(value)
    }

    fn write(&self, value: i8) -> Result<(), ErrorCode> {
        let addr = self.value()?;
        Ok(unsafe {
            asm!(
                "out dx, al",
                in("rdx") addr,
                in("al") value,
            )
        })
    }
}

Given the exact combinations of instructions for different data sizes, I have to implement specific i8, i16, i32 and i64 versions for Port-Mapped devices. This is where I am comming unstuck, I can't work out how to limit the available read() and write() functions to the address subject to the address type. I.e. my starting point was:

pub struct Address<T: RegisterType> {
    pub fn read<T>(&self) -> Result<T, ErrorCode> {...}
    pub fn write<T>(&self) -> Result<T, ErrorCode> {...}
}

Obviously I can't concurrently implement read<T> and say read<i8> etc. I tried a few things using the Factory design pattern, but that does expect a fixed interface, and even with a variable interface, still not sure it can be done with rust.

Does anyone have suggestions how to achieve this generic / transparent address design, or should I park this in the not-possible spot?

I don't quite understand the description of your problem. besides, you code snippets are not valid.

in the first code snippet, Address is a non-generic type, as in

the second part, it is declared as a struct with one type argument, as in

yet it is invalid rust code, as rust doesn't allow functions to be defined inside a struct definition. besides the functions themselves are generic, with T shadowing the type argument of the struct itself.

whatever you might be trying to achieve, I feel like you are probably over engineering
things and make it unnecessarily complicated. if you just doing IO, simple functions like read_i8(), read_i16() etc should be good enough.

1 Like

Is this what you are looking for?

struct Address<T> {
    phantom: PhantomData<fn() -> T>,
}

trait PortIO<T> {
    fn read(&self) -> io::Result<T>;
    fn write(&self, value: T) -> io::Result<()>;
}

impl PortIO<u8> for Address<u8> {
    fn read(&self) -> io::Result<u8> {
        todo!()
    }

    fn write(&self, value: u8) -> io::Result<()> {
        todo!()
    }
}

impl PortIO<u16> for Address<u16> {
    fn read(&self) -> io::Result<u16> {
        todo!()
    }

    fn write(&self, value: u16) -> io::Result<()> {
        todo!()
    }
}

I'm honestly not sure, not familiar with PhantomData does, need more reading.

It doesn't do anything except make the (otherwise unused) type parameter T unused. It's not the point of the implementation. The point of the implementation is that both the trait and the type are generic, and Trait<T> is implemented for Type<U> (for some specific set of types).

1 Like

Sorry, those were quick examples to try and communicate the idea. I did not test them. Here is a complete example that compiles, though not sure if it actually runs (need to run it in qemu through the kernel). I did not include the code to properly format the different address types, but added "constructors" for them as stubs. It seems a bit "hacky" to me, hoping for a more elegant solution.

use core::any::TypeId;
use core::arch::asm;

#[derive(Debug)]
enum ErrorCode {
    AddressOutOfRange,
    FunctionNotSupported,
}

#[derive(PartialEq)]
enum AddressType {
    Virtual,
    Physical,
    Port,
}

struct Address {
    address_type: AddressType,
    raw_pointer: *mut u8,
}

impl Address {
    fn new_virtual_address(address: usize) -> Result<Self, ErrorCode> {
        // Correctly setup virtual address here.
        Ok(Address {
            address_type: AddressType::Virtual,
            raw_pointer: unsafe { core::mem::transmute(address) },
        })
    }

    fn new_physical_address(address: usize) -> Result<Self, ErrorCode> {
        // Correctly setup physical address here.
        Ok(Address {
            address_type: AddressType::Physical,
            raw_pointer: unsafe { core::mem::transmute(address) },
        })
    }

    fn new_port_address(address: usize) -> Result<Self, ErrorCode> {
        // Correctly setup port address here
        Ok(Address {
            address_type: AddressType::Port,
            raw_pointer: unsafe { core::mem::transmute(address) },
        })
    }

    pub fn new(address: usize, address_type: AddressType) -> Result<Self, ErrorCode> {
        match address_type {
            AddressType::Physical => Address::new_physical_address(address),
            AddressType::Virtual => Address::new_virtual_address(address),
            AddressType::Port => Address::new_port_address(address),
        }
    }

    pub fn value(&self) -> Result<usize, ErrorCode> {
        Ok(unsafe { core::mem::transmute(self.raw_pointer) })
    }

    fn port_read_i8<T: Copy>(&self) -> Result<T, ErrorCode> {
        let addr = self.value()?;
        let mut value: i8;
        unsafe {
            asm!(
                "in al, dx",
                in("rdx") addr,
                out("al") value
            )
        }

        let result: *const T = unsafe { core::mem::transmute(core::ptr::addr_of!(value)) };
        Ok(unsafe { *result })
    }

    fn port_write_i8<T>(&self, value: T) -> Result<(), ErrorCode> {
        let value: i8 =
            unsafe { *core::mem::transmute::<*const T, *const i8>(core::ptr::addr_of!(value)) };
        let addr = self.value()?;
        Ok(unsafe {
            asm!(
                "out dx, al",
                in("rdx") addr,
                in("al") value,
            )
        })
    }

    pub fn read<T: Copy + 'static>(&self) -> Result<T, ErrorCode> {
        if self.address_type == AddressType::Port {
            if TypeId::of::<i8>() == TypeId::of::<T>() {
                self.port_read_i8()
            } else {
                Err(ErrorCode::FunctionNotSupported)
            }
        } else {
            let raw_ptr: *const T = unsafe { core::mem::transmute(self.raw_pointer) };
            Ok(unsafe { *raw_ptr })
        }
    }

    pub fn write<T: Copy + 'static>(&self, value: T) -> Result<(), ErrorCode> {
        if self.address_type == AddressType::Port {
            if TypeId::of::<i8>() == TypeId::of::<T>() {
                self.port_write_i8(value)
            } else {
                Err(ErrorCode::FunctionNotSupported)
            }
        } else {
            let raw_ptr: *mut T = unsafe { core::mem::transmute(self.raw_pointer) };
            Ok(unsafe { *raw_ptr = value })
        }
    }
}

fn serial_tty_driver_write(serial_port: Address, text: &str) {
    for cur_byte in text.chars() {
        serial_port.write(cur_byte as i8).unwrap();
    }
}

fn main() {
    // Create a port-mapped serial port and write to it.
    let port_mapped_serial_port = Address::new(0x3F8, AddressType::Port).unwrap();

    // Create a memory mapped serial port and write to it.
    let memory_mapped_serial_port = Address::new(0x3F8, AddressType::Virtual).unwrap();

    // Now a standard function that accept Address can use it transparently without needing to know
    // if the address refer to a port mapped address or a memory mapped address.
    serial_tty_driver_write(port_mapped_serial_port, "Hello World!");
    serial_tty_driver_write(memory_mapped_serial_port, "Hello World!");
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.