8000 Improving `Interface` data structure by using generic data type · Issue #2727 · nmstate/nmstate · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Improving Interface data structure by using generic data type #2727

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cathay4t opened this issue Jul 4, 2024 · 4 comments
Open

Improving Interface data structure by using generic data type #2727

cathay4t opened this issue Jul 4, 2024 · 4 comments

Comments

@cathay4t
Copy link
Member
cathay4t commented Jul 4, 2024

Demo code:

// SPDX-License-Identifier: Apache-2.0

#[derive(Debug, Default)]
enum InterfaceType {
    #[default]
    Unknown,
    Bond,
    LinuxBridge,
}

#[derive(Debug, Default)]
struct BaseInterface {
    name: String,
    iface_type: InterfaceType,
}

#[derive(Debug)]
struct BondInterface {
    mode: u8,
}

#[derive(Debug)]
struct LinuxBridgeInterface {
    stp: bool,
}

#[derive(Debug)]
struct Interface<T> {
    base: BaseInterface,
    specific: T,
}

impl<T> Interface<T> {
    fn new_without_type(base: BaseInterface, specific: T) -> Self {
        Self { base, specific }
    }

    fn name(&self) -> &str {
        self.base.name.as_str()
    }
}

impl Interface<LinuxBridgeInterface> {
    fn new(base: BaseInterface, bridge: LinuxBridgeInterface) -> Self {
        let mut rt = Self::new_without_type(base, bridge);
        rt.base.iface_type = InterfaceType::LinuxBridge;
        rt
    }

    fn stp(&self) -> bool {
        self.specific.stp
    }
}

impl Interface<BondInterface> {
    fn new(base: BaseInterface, bond: BondInterface) -> Self {
        let mut rt = Self::new_without_type(base, bond);
        rt.base.iface_type = InterfaceType::Bond;
        rt
    }

    fn mode(&self) -> u8 {
        self.specific.mode
    }
}

fn main() {
    let mut bond = Interface::<BondInterface>::new(
        BaseInterface {
            name: "bond0".into(),
            ..Default::default()
        },
        BondInterface { mode: 0 },
    );
    bond.specific.mode = 1;
    println!("Bond {} mode {}", bond.name(), bond.mode());

    let mut bridge = Interface::<LinuxBridgeInterface>::new(
        BaseInterface {
            name: "bond0".into(),
            ..Default::default()
        },
        LinuxBridgeInterface { stp: false },
    );
    bridge.specific.stp = true;
    println!("Bridge {} stp {}", bridge.name(), bridge.stp());
}
@cathay4t
Copy link
Member Author
cathay4t commented Jul 4, 2024

After this, we can introduce Controller trait, so we can have function like:

8000
impl Interface<T> where T: Controller {
  fn validate_port_over_book(&self) -> Result<(), NmstateError> {
    for port_name in self.specifc.ports().unwrap_or_default() {
      validate_over_book(port_name)?;
    }
    Ok(())
  }

This would remove a lot duplicate code and provide better code layout.

@cathay4t
Copy link
Member Author
cathay4t commented Jul 4, 2024

With struct Interface<T>, we cannot place different interface type into a Vec as they are different type now.

Still searching for better approaches.

@ihuguet
Copy link
Member
ihuguet commented Aug 1, 2024

I have been thinking about this recently too. The problem that I see with the current approach is that Interface being an enum forces us to do match all the time, everywhere, becoming too verbose.

Something more similar to using Interfaces and/or polymorphism would be desirable. It would allow to comply with the "Open-Closed principle" much more, too.

For example, all interface types implementing an InterfaceTrait with common behaviours. Then, you can place them into a Vec as Box<&dyn InterfaceTrait>. If you want to put them together into a single Vec/container you need dynamic dispatch, static dispatch is not an option.

The problem with that is: how to convert to the specific interface type from the InterfaceTrait? For example, if you need to call to methods that are specific of EthernetInterface but what you have is a &InterfaceTrait.

I've seen that std::any::Any could be an option: https://medium.com/fnexec/casting-dynamic-trait-to-a-concrete-type-in-rust-4116c2addad8. However, I'm not fully convinced about this for this use case, probably it won't be very ergonomic.

For the moment, the 2 things that I think that would be nice are (both can be used at the same time):

  • Implement AsRef<Interface> for InterfaceTrait (and AsRefMut), allowing to get a reference to the current enum Interface from the &InterfaceTrait. Then, you can do the match as we do now.
  • Add a method fn as_iface_type<T>() -> Result<&T,...> to InterfaceTrait, with a default implementation that always returns Err, and overwriting the implementation for the right type on each interface type (for example, EthernetInterface will implement as_iface_type<EthernetInterface> and nothing else). I haven't tested it, but should work I guess?

@ihuguet
Copy link
Member
ihuguet commented Aug 2, 2024

Today I have been experimenting about this. Those 2 things that I initially planned to do don't work, actually, for an obvious reason: they are still static dispatch, so obviously you cannot use them from a dynamically dispatched type.

Using std::any::Any works, and helps to implement some methods in the trait to convert from the trait to the concrete type. However, what works the best for me is using the enum_dispatch crate.

Here I have 3 small test projects summarizing the different ways that I've found that can work: https://github.com/ihuguet/rust-poly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
0