Design decisions for storing trait objects


#1

Moin,

I’m creating a plugin system. The desired interface may look like this:

fn main() {
    let mars = Mars{};
    // Desired interface
    let plugin = Plugin::new("Mars")
        .provider(ProviderType::Air(mars)) // mars.clone()?
        .provider(ProviderType::Food(mars));
}

Now I’m stuck with how to store trait objects. I commented the code where I need help with design decision. Please excuse the fact that it’s a bit contrary at a few points.

// Containers for example data
struct Food {
    glycemic_index: u32
}
struct Air {
    oxygen_level: u32
}

// Marker trait for storing itself in a container as trait objects.
// Or shall I use an enum?
trait Provider {}

// In the Plugin implementation I want to be able to call the traits' methods via matching.
// What's the best way here? Shall I store trait objects of type Provider or match over this enum?
enum ProviderType {
    Air(AirProvider), // Won't work like this
    Food(FoodProvider),
}

// Example Provider trait
trait FoodProvider: Provider {
    fn food(&self) -> Food;
}

// Another example Provider trait
trait AirProvider: Provider {
    fn air(&self) -> Air;
}

// Plugin = multiple Providers composed
struct Plugin {
    name: String,
    // What's the best way to store trait objects?
    //         Vec<Box<Provider>>       ?
    //         Vec<&'a (Provider + 'a)> ?
    //         Vec<ProviderType>        ?
    //         ...                      ?
    providers: Vec<ProviderType>,
}


// Apply builder pattern. See main() for desired interface
impl Plugin {
    fn new(name: &str) -> Self {
        Self {
            name: name.into(),
            providers: Vec::new(),
        }
    }

    // What's the best way to implement this function? Regarding function signature and body
    fn provider(mut self, provider: &ProviderType) -> Self {
        self.providers.push(provider);
        self
    }
}

// Specific Provider
struct Mars {}
impl Provider for Mars {}
impl AirProvider for Mars {
    fn air(&self) -> Air {
        Air {
            oxygen_level: 55
        }
    }
}
impl FoodProvider for Mars {
    fn food(&self) -> Food {
        Food {
            glycemic_index: 7
        }
    }
}

Thanks in advance :slight_smile:


#2
trait Provider {}

is useless as it does not provide any method to get the actual interface from that. Just delete it.

enum ProviderType {
    Air(AirProvider), // Won't work like this
    Food(FoodProvider),
}

Of course it won’t. The compiler does not know how the instances will look, so it can’t reserve space for them. So you need to hold them by reference. You have a choice of:

  • A borrow, Air(&'a AirProvider). This requires a lift-time to restrict the life of the enum value to life-time of the plugin that provided it. Not sure how practical.
  • A unique owned instance, Air(Box<AirProvider>). You can borrow from this, but not extend the life-time.
  • A shared owned instance, Air(Rc<AirProvider>). You can hand out Rc<AirProvider> references that extend the life of the object.
  • A shared owned instance, thread-safe variant, Air(Arc<AirProvider>). Unlike the previous, can be shared between threads.

Which you use is up to you, depending on what you need.

Note that since the *Provider traits need to be object-safe, they can’t be generic (except over lifetimes), so if you need to return derived objects from them, you’ll need boxes (or rcs) there too.


#3

Ah thanks @jan_hudec. Thanks for listing my choices.
I also asked some questions on the IRC. I definitely have to specify the type right away.
I’d like to do:

enum ProviderType<T: Clone> {
    Air(T<AirProvider>), 
    Food(T<FoodProvider>),
}

but it turns out Rust doesn’t have higher-kinded types yet.
So I tried to create a naive workaround:

type Container = Rc;
enum ProviderType {
    Air(Container<AirProvider>), 
    Food(Container<FoodProvider>),
}

but Rc requires a <type>. This would have had the advantage that I can just change Container to whatever I like when I change my mind.
I really dislike the idea that I have to hardcode the type already.


#4

Box<Trait> is a specific type. Try:

type Container<T> = Rc<Box<T>>;

#5

Thanks, kornel, that worked :slight_smile: