Struct Trait Implementation

#1

I’ve been working to learn Rust and ran into an issue I’m not quite sure about. I figured I’d come here to see if it’s possible to do in Rust, or if there’s a better Rust way of doing something. Is it possible to have a trait inherit another trait and then have a struct implement both of those traits without declaring a new struct? That wording might sound confusing, so here’s a quick example of what I’m talking about. Say I have this struct and these traits.

struct ExampleStruct {
    example_variable: u8,
}

trait Read {
    fn read(&self) -> u8;
}

trait ReadWrite: Read {
    fn write(&mut self, value: u8);
}

impl Read for ExampleStruct {}
impl Write for ExampleStruct {}

let example_struct_a = // The readonly implementation
let example_struct_b = // The read/write implementation

I don’t really know Rust well enough yet to know if this is possible with Traits, or if that’s even what they’re supposed to be used for. I’ve messed around with serde a bit and it seems similar to how they have a Serialize trait and a Deserialize trait, but I could be wrong. Is anyone here able to offer any advice/point me in the right direction? Any help would be greatly appreciated.

0 Likes

#2

You can have one trait be a supertrait of another, and you can have a single struct implement both of those traits. You can see this in action with a more filled out version of your example:

struct ExampleStruct {
    example_variable: u8,
}

trait Read {
    fn read(&self) -> u8;
}

trait ReadWrite: Read {
    fn write(&mut self, value: u8);
}

impl Read for ExampleStruct {
    fn read(&self) -> u8 {
        self.example_variable
    }
}

impl ReadWrite for ExampleStruct {
    fn write(&mut self, value: u8) {
        self.example_variable = value;
    }
}

fn main() {
    let mut a = ExampleStruct { example_variable: 0 };

    // prints `0`
    println!("{}", a.read());
    
    a.write(42);
    
    // prints `42`
    println!("{}", a.read());
}

Note that you don’t need to create two separate “instances” of your ExampleStruct to use the two traits. They both work fine on a single struct instance. Also note that ReadWrite: Read doesn’t necessarily mean that ReadWrite “inherits” from Read, but instead it means "Anything implementing ReadWrite also has to implement Read"

0 Likes

#3

Also, take note that ReadWrite::write() takes &mut self therefore only a &mut ExampleStruct or mut ExampleStruct can use write(). IE:

let a = ExampleStruct { example_variable: 0 };
a.read(); 
// a.write(32); <-- Error, cannot mutate immutable variable
//--------
let mut b = ExampleStruct { example_variable: 0};
b.read();
b.write(32); //Possible because `b` is now mutable
//--------
fn takes_ref(data: &ExampleStruct) {
    data.read();
    //data.write(32); <-- Error, cannot mutate value behind immutable reference
}
//--------
fn takes_mut_ref(data: &mut ExampleStruct) {
    data.read();
    data.write(32); // `data` is now behind a mutable reference
}
0 Likes

#4

Thanks for the code example. I don’t think I explained it correctly. I was able to get my ExampleStruct to call read and write, but is there a trait/struct version of extending like you would a class in another language like this?

class Read
{
    int _x;

    public int Read() 
    {
        Console.WriteLine(_x); // Has access to x
    }
}

class ReadWrite
{
    public void Write(int x) 
    {
        _x = x;
    }
}

class ReadExample : Read {}

class ReadWriteExample: ReadExample {}

Example a = new ReadExample();
Example b = new ReadWriteExample();

(a as ReadExample).Read();       // This works
(a as ReadWriteExample).Write(10); // This fails since Read can't Write
(b as ReadWriteExample).Write(11); // This works since it has the Write method
(b as ReadWriteExample).Read();  // This works since it has the Read method

This is a pretty bad example, but I guess I’m getting at if it’s possible to do a similar base struct like you would with a base class and then impl different traits so that you could share the same struct member variables, but one could only Read, while the other could Read and Write.

0 Likes

#5

Hmm, are you talking about dynamic dispatch?
For example:

trait Read {
    fn read(&self) -> u8;
}
impl Read for () {
    fn read(&self) -> u8 {
        0
    }
}
impl Read for usize {
    fn read(&self) -> u8 {
        self as u8
    }
}
fn main() {
    let x: Box<dyn Read> = Box::new(());
    println!("{}", x.read()); //Prints "0"
    x = Box::new(32usize);
    println!("{}", x.read()); //Prints "32"
}


Or a better example (Using the previous impls and definitions):

trait ReadWrite: Read {
    fn write(&mut self, data: u8); 
}
fn foo(data: &dyn Read) {
    println!("{:?}", data.read());
}
fn bar(data: &mut dyn ReadWrite) {
    println!("{:?}", data.read());
    data.write(64u8);
    println!("{:?}", data.read()); //Prints "64"
}
0 Likes

#6

That’s pretty close to what I was looking for. Is it possible to use dyn as a struct field, sort of like:

trait Read {
    fn read(&self, data: u8);
}

trait ReadWrite: Read {
    fn write(&mut self, data: u8);
}

struct Test {
    readonly_field: dyn Read,
    read_write_field: dyn ReadWrite),
}

fn test() {
    let mut test = Test {
        readonly_field:   // Some sort of dyn Read struct initialization?
        read_write_field: // Some sort of dyn ReadWrite struct initialization?
    };

    let readonly = test.readonly_field.read();
    test.read_write_field.write(10);
}
0 Likes

#7

Ah, there are some drawbacks to using dyn:
Mainly being that it is !Sized, therefore the size of it is unknown, and can therefore only be the last field of a struct declaration (Also making said struct: !Sized too) or the more common route, of keeping a reference or Box to it:

struct Foo {
    var: Box<dyn std::fmt::Debug> //Debug is a trait
    data: &dyn Read
}

Also, a reference to T where T: Trait can automatically be coerced to &dyn Trait:

fn foo(data: &dyn std::fmt::Debug) {
    println!("{:?}", data);
}
fn main() {
    let x = 0;
    foo(&x);
    //Also works for boxes
    let y: Box<dyn std::fmt::Debug> = Box::new(0usize);
}
0 Likes

#8

Ahh okay, so if I needed to have a large mix of Read and ReadWrite structs, I’d have to box each one of them, unless it’s the final field in the struct. I guess my last question would be, from a Rust approach, is one way preferred over the other? From my understanding it looks like I could either do:

trait Read {} // Implementation left out
trait ReadWrite: Read {} // Implementation left out

struct TraitStruct {
    var_a: Box<dyn Read>,
    var_b: Box<dyn Read>,
    var_c: Box<dyn ReadWrite>,
    var_d: &dyn ReadWrite,
}

or I could do it as two structs:

struct ReadStruct {
    value: u8,
}

impl ReadStruct {
    fn read(&self) {
        do_read_things()
    }
}

struct ReadWriteStruct {
    value: u8,
}

impl ReadWriteStruct {
    fn read(&self) -> u8 {
        do_read_stuff_that_returns_u8
    }

    fn write(&mut self, value: u8) {
        self.do_write_stuff(value)
    }
}

struct Test {
    var_a: ReadStruct,
    var_b: ReadStruct,
    var_c: ReadWriteStruct,
    var_d: ReadWriteStruct,
}

I’d like to try and do things the Rust way from the beginning if possible while I learn the language. Is one of these preferred over the other?

0 Likes

#9

structs and traits are different in rust, so the second one wouldn’t be an equal approach to the first one.

As for the idiomatic approach, definitely Boxing it up or receiving a reference from elsewhere. Unsized types are a pain to work with in other cases.

0 Likes

#10

Okay thanks for the help and the quick replies. I’ll read up more on traits, Boxing and dyn and see if I can get something working.

1 Like

#11

I was able to come up with a working version of what I was asking about, so I figured I’d post it in case you, or anyone else who read this was interested. I wasn’t able to use Box since I’m using no_std, so I mixed your examples with the original information I was basing this off of that had everything contained in a single ReadWrite trait. It basically just boils down to this.

pub trait PortRead {
    unsafe fn read_from_port(port: u16) -> Self;
}

pub trait PortWrite {
    unsafe fn write_to_port(port: u16, value: Self);
}

pub trait PortReadWrite: PortRead + PortWrite {}

impl PortRead for u8 {
    unsafe fn read_from_port(port: u16) -> u8 {}
}

impl PortRead for u16 {
    unsafe fn read_from_port(port: u16) -> u16 {}
}

impl PortRead for u32 {
    unsafe fn read_from_port(port: u16) -> u32 {}
}

impl PortWrite for u8 {
    unsafe fn write_to_port(port: u16, value u8) {}
}

impl PortWrite for u16 {
    unsafe fn write_to_port(port: u16, value u16) {}
}

impl PortWrite for u32 {
    unsafe fn write_to_port(port: u16, value u32) {}
}

impl PortReadWrite for u8 {}   // Placeholder so I don't have to rewrite read/write
impl PortReadWrite for u16 {}  // Placeholder so I don't have to rewrite read/write
impl PortReadWrite for u32 {}  // Placeholder so I don't have to rewrite read/write

pub struct ReadonlyPort<T: PortRead> {
    port: u16,
    phantom: PhantomData<T>,
}

pub struct WriteOnlyPort<T: PortWrite> {
    port: u16,
    phantom: PhantomData<T>,
}

pub struct Port<T: PortReadWrite> {
    port: u16,
    phantom: PhantomData<T>,
}

impl<T: PortRead> ReadonlyPort<T> {
    #[inline]
    pub unsafe fn read(&self) -> T {
        T::read_from_port(self.port)
    }
}

impl<T:PortWrite> WriteOnlyPort<T> {
    #[inline]
    pub unsafe fn write(&mut self, value: T) {
        T::write_to_port(self.port, value)
    }
}

impl<T:PortReadWrite> Port<T> {
    #[inline]
    pub unsafe fn read(&self) -> T {
        T::read_from_port(self.port)
    }

    #[inline]
    pub unsafe fn write(&mut self, value: T) {
        T::write_to_port(self.port, value)
    }
}

I left out the impl code for u8 -> u32 because it’s just different versions of inb/outb assembly depending on the size of the data. I’m not sure if this is the cleanest/best way to implement what I was talking about, but doing it this way allows me to be able to create structs like so:

struct GeneralRegisters {
    input_status_0: ReadonlyPort<u8>,
    input_status_1_mono: ReadonlyPort<u8>,
    input_status_1_color: ReadonlyPort<u8>,
    feature_control_read: ReadonlyPort<u8>,
    feature_control_mono_write: WriteOnlyPort<u8>,
    feature_control_color_write: WriteOnlyPort<u8>,
    miscellaneous_output_read: ReadonlyPort<u8>,
    miscellaneous_output_write: WriteOnlyPort<u8>,
}

and make it so it’s impossible to call write on readonly ports, as well as support different data sizes. Thanks for taking the time to help me out.

0 Likes