Collections of trait objects

I am trying to create a model for a network device management. Different device drivers register themselves with the manager for later use. Each driver can then create an instance of a Device trait object, specific to that driver. Creating the Device instance requires Driver-impl -specific data to instantiate it.

What I have below seems like it will work, but the Any bits are unpleasant. Is there a way to do this in the type system without requiring casting? I tried using associated types on Driver to include what each Driver's device_params type would be, but that didn't seem to solve my problem.

use std::any::Any;

pub struct Manager {
    drivers: Vec<Box<dyn Driver>>,
    devices: Vec<Box<dyn Device>>,
}

impl Manager {
    pub fn register_driver(&mut self, driver: Box<dyn Driver>) { ... }
    pub fn create_device_by_driver_name(&mut self, driver_name: &str, device_params: &dyn Any) { ... }
}

pub struct PlaceholderError;

pub trait Driver {
    fn new_device(&mut self, device_params: &dyn Any) -> Result<Box<dyn Device>, PlaceholderError>;
    fn del_device(&mut self, device: Box<dyn Device>);
    fn name(&self) -> &'static str;
}

pub struct DummyDriver;
pub struct DummyDriverDeviceParams;

impl Driver for DummyDriver {
    fn new_device(&mut self, params: &dyn Any) -> Result<Box<dyn Device>, ()> {
        let params: &DummyDriverDeviceParams = params.downcast_ref().ok_or(PlaceholderError)?;
        // use params to create an instance of DummyDriverDevice
        Ok(Box::new(DummyDriverDevice{}))
    }

    fn del_device(&mut self, device: Box<dyn Device>) {
        todo!()
    }

    fn name(&self) -> &'static str {
        "dummy"
    }
}

pub struct DummyDriverDevice {}

impl Device for DummyDriverDevice {}

pub trait Device {/* TODO */}

Not that I know of (at least in the way you want). The fundamental issue is that Manager is a heterogeneous collection of Drivers with different interfaces. If the interface for each driver (e.g., how devices are created and destroyed) is different, a Manager cannot possibly do anything useful besides merely storing drivers.

It could not (safely) offer a method to find a Driver by name, for example, because the interface of the driver cannot be determined at compile time.

For these reasons, if possible, I would recommend taking Manager out of the equation, or at least making it optional. That said, if you really want to keep Manager, here's how I would do it with some unsafe:

(Disclaimer: I have not tested this code or validated it with Miri. Proceed with caution!.)

use std::collections::HashMap;

pub struct Manager {
    drivers: HashMap<&'static str, usize>,
}

impl Manager {
    pub fn register_driver<D: Driver + 'static>(&mut self, driver: D, name: &'static str) {
        let driver = Box::into_raw(Box::new(driver)) as usize;
        self.drivers.insert(name, driver);
    }
    
    pub unsafe fn find_driver<'a, D: Driver + 'static>(
        &'a mut self,
        name: &str,
    ) -> Option<&'a D> {
        self.drivers.get(name).copied().map(|driver| {
            unsafe { &*(driver as *const D) }
        })
    }
    
    pub unsafe fn find_driver_mut<'a, D: Driver + 'static>(
        &'a mut self,
        name: &str,
    ) -> Option<&'a mut D> {
        self.drivers.get_mut(name).copied().map(|driver| {
            unsafe { &mut *(driver as *mut D) }
        })
    }
}

In essence, the responsibility of ensuring a driver conforms to an interface is pushed onto the caller. Note that the above implementation does not drop the drivers. That's where things get a little more complicated; it may be necessary for callers to manually drop drivers with a Manager::destroy_driver method or something.

Edit: I should reiterate that I think this approach is not a good idea for most applications. At the end of the day, we have added a lot of complexity to a Manager type that does nothing except storing Drivers. This only makes sense if drivers are implemented by external code and are loaded dynamically at runtime.

It would be helpful to have more context for the problem.

As for Driver, we can remove usage of dyn Any like this:

pub trait Driver {
    type Device: Device;
    type DeviceParams;
    
    type DeviceHandle: Copy;
    type NewDeviceError: std::error::Error;

    fn new_device(
        &mut self,
        params: Self::DeviceParams,
    ) -> Result<Self::DeviceHandle, Self::NewDeviceError>;
        
    fn del_device(&mut self, handle: Self::DeviceHandle);
    
    fn find_device(&self, handle: Self::DeviceHandle) -> Option<&Self::Device>;
    
    fn find_device_mut(
        &mut self,
        handle: Self::DeviceHandle,
    ) -> Option<&mut Self::Device>;
}

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.