Using a Trait to Share Functionality on Structs (my first time)

I have at least four structures that are all the same, but with different names. Each struct holds a single usize field.

#[derive(Clone, Copy, Debug)]
pub struct Pmpcfg0 {
    bits: usize,
}

These usize fields, 32 bits, are densely packed meaning that each byte of the register controls separate functionality. I want to return another structure from this one that takes a single byte from the bits field that describes the configuration via a couple of enums and a bool.

/// Pmpconfig struct to hold pmp register's current settings of a certain byte
#[derive(Clone, Copy, Debug)]
pub struct Pmpconfig {
    pub permission: Permission,
    pub range_type: Range,
    pub locked: bool,
}

I have never used traits before, and I'm not sure how to structure it, my idea is to define a trait that can be implemented on all of the four Pmpconfig<x> structs.

pub trait Pmp {
    fn pmp_byte(&self, index: usize) -> PmpByte {}
    fn permission(&self) -> Option<Permission> {}
    fn locked(&self) -> bool {}
    fn pmp_cfg(&self, index: usize) -> Pmpconfig {} 
}

My question is on the pmp_cfg method as it needs to return a struct with fields populated by the other methods. I feel like this isn't quite the setup I need. Any insight or recommendations?

More of the code I am playing with
https://github.com/dkhayes117/riscv/blob/master/src/register/pmpcfgx.rs

If I understand correctly, I'd probably just have an into_config() method on PmbByte and use that to impl From<PmbByte> for Pmbconfig, and probably something similar to go the other direction as well. Then you could have something like

pub trait Pmp {
    fn pmp_byte(&self, index: usize) -> PmpByte;
    fn pmp_cfg(&self, index: usize) -> Pmpconfig {
        self.pmp_byte(index).into()
    }
}

Your other two methods in your example trait don't take an index, was that intentional? If so I'm not sure what they are about. If not, they could similarly just delegate to methods on PmpByte.

Other alternatives or considerations:

  • You could likewise put methods on a PmpUsize or whatever and delegate everything
    • This is a sort-of substitute for the non-existent "fields in traits" feature
  • You could implement Index<Output=PmpByte> on your structs (or PmbUsize) too...
    • And then require that as a supertrait of Pmp and drop the index parameters
    • Arguably you don't even need the extra trait at that point

If all the methods of the Pmp trait take an index and delegate to what are actually methods on PmpByte, I think I would prefer the Index approach.

Other side note:

Your Permission enum has NONE = 0 but elsewhere you have an Option<Permission> where the returned value is either Option::None, or Option::Some(x) where x is never Permission::NONE. I would choose one approach or the other. You're also unwrapping such an option in one place -- if this is intentional (i.e. it should never be None/NONE), I would put some sort of explanation in the code, either using .expect("...explanation...") or a comment.

Thanks for the reply, I've decided to work my way up since it seems I've got too much going on at once to handle. To start I want a trait with a method to get a single byte of the struct field bits.

No matter what I do, I can't get the trait method to recognize the bits field on &self.

   Compiling playground v0.0.1 (/playground)
error[E0609]: no field `bits` on type `&Self`
  --> src/main.rs:8:14
   |
6  | / pub trait Pmp{
7  | |     fn pmp_byte(&self,index: usize) -> u8{
8  | |         self.bits.to_be_bytes().iter()[0] as u8
   | |              ^^^^
9  | |     }
10 | | }
   | |_- type parameter 'Self' declared here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0609`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

You need to move your implementation code from trait Pmp into impl Pmp for Pmpcfg0, since it is Pmpcfg0 which has the bits field.

What you were trying to do is a default implementation, which doesn't work when you need to know about the datatype.

I just figured that out. It makes me wonder why I would even want to use a trait at all. If I have to define the method in the impl block for each of the 4 structs that are all the same but are different "types", why would I not just define them as methods on the structs themselves. The trait is worthless for this, it doesn't make the code less verbose in the slightest.

A trait allows for functions which take any object which implements that trait, rather than a specific concrete type, e.g. fn takes_impl(config: impl Pmp) vs fn takes_concrete(config: Pmpcfg0). And there is also Box<dyn Trait> for ownership of dynamic dispatch objects.

If you never need to obfuscate a concrete type as 'an object which can do methods x, y, and z', then traits may not be of any benefit. A trait just gives an addressable name to a pre-defined set of implementations. impl Pmpcfg0 { /* methods */ } -> impl Pmp for Pmpcfg0 { /* the same methods */ }.

It also looks like you may be implementing something similar to bitflags, not sure if that might help simplify what you're trying to accomplish.

2 Likes

I'll be honest, I don't fully understand your answer. The actual code I am using uses bit_field for accessing individual bits. What I am after however is taking a byte from a 32 bit register then taking the bits in that byte and decoding them to mean something obvious (options from an enum).

However, I have four structs that are in four different modules (for name spacing) and all need to implement the methods, but I don't want to repeat them four times.

I do get that the trait can't know that my struct has a field named bits because it doesn't know what it will be implemented on, bummer.

EDIT:
I think that if I implement getting the byte for each struct, I can make the methods that get the fields of the byte just once as they all would take a pmp_byte type. :slight_smile: Now how to do that?....

A concrete type knows all of the (public) methods implemented for it, as well as all of its (public) fields. Any time you pass a concrete type (argument or return) all of that info comes with it for free. If you have types A, B, C, and D, but never need to give 'some object that can do a set of methods common to A, B, C, and D' then traits can't help. Traits are basically just a uniform interface guarantee. The alternative where traits are useful is a concrete type which hides its base concrete type. A Box<dyn MyTrait> will make a heap allocated pointer to whichever type you built it around (A, B, C, or D), but on the surface it only knows that it holds a pointer to an object than can perform the methods specified in the trait.

Traits are not something you write once and then get to implement for free with a single line anywhere you want. When traits are defined their 'layout' is set, and then that exact form must be implemented, in detail, separately for every struct you want to implement the trait on.

What you really seem to be after is a macro to reduce the repetition of the implementation details:

pub trait Pmp {
    fn pmp_byte(&self, index: usize) -> PmpByte;
    fn pmp_cfg(&self, index: usize) -> Pmpconfig;
}

macro_rules! impl_pmp { $( $my_type:ident ), * ) => { $(
impl Pmp for $my_type {
    fn pmp_byte(&self, index: usize) -> PmpByte {
        /* code for this method */
    }
    
    fn pmp_cfg(&self, index: usize) -> Pmpconfig {
        /* note that if calling a field name it must be identical for each type */
        self.pmp_byte(index).into()
    }
}
)* } }

impl_pmp!(Pmpcfg0, Pmpcfg1, Pmpcfg2, Pmpcfg3)

And a couple things I've run into that might be worth noting. First, you can make a module that is just for config parsing, define the trait, import all your structs you want to implement it on, and then setup and call the macro. You don't need to implement the trait separately in each module where each struct is defined. Though, alternatively, you can use macro_use flag to allow the macro to be used throughout your crate and do it that way. Also, either the trait or the struct (or both) need to be defined in your crate. You can't implement an external trait on an external or built-in type.

2 Likes

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.