Idiomatic pattern for struct initialization

Hi everyone!

In our current project, we have "plugins", which need some initialization and offer some methods later on.

Currently we're doing this as follows:
All plugins implement two traits, InstanceInit and InstanceFeatures.

InstanceInit offers two methods

  • get_default_parameters() -> Vec<Parameter>
  • initialize(parameters: Vec<Parameter>) -> Result<Box<dyn InstanceFeatures>, Box<dyn Error>>

InstanceFeatures then offers a few methods that all contain &self.

In Rust, I can easily make a trait object out of InstanceFeatures, but not out of InstanceInit.

In our code, we need to keep track of an assortment of different plugins and then instantiate them at runtime, depending on user input.
Currently we're doing that by requiring that all plugins implement both traits and manually unpacking the init-trait into a struct of

PluginInit{
   name: String,
   get_default_parameters: Box<fn() -> Vec<Parameters>>,
   initialize: Box<fn(Vec<Parameter>)-> Result<Box<dyn InstanceFeatures>, Box<dyn Error>>
}

Is there a more idiomatic way to do this?
I understand that I can't create a trait object by just doing Box::new(Plugin) for a struct Plugin that implements InstanceInit and InstanceFeatures, but the current way we're doing this seems a bit convoluted.

Thanks in advance!

If you want dynamic dispatch for a trait that's not object-safe, there is a trick you can use: Define a second, object-safe, trait with a blanket implementation for everything that implements the non-object-safe one.

A first cut of what your API might look like with that technique is below. Plugin authors would implement Plugin and Builder and your dynamic code uses Box<dyn DynPlugin> and Box<dyn DynBuilder>. The Plugin objects will probably be unit structs and the Builders correspond to your InstanceInit.

struct Parameters;

trait Plugin: Sized {
    type Builder: Builder;
    fn builder(&self)->Self::Builder;
}

trait DynPlugin {
    fn dyn_builder(&self)->Box<dyn DynBuilder>;
}

impl<T:Plugin> DynPlugin for T {
    fn dyn_builder(&self)->Box<dyn DynBuilder> {
        Box::new(self.builder())
    }
}

trait Builder: Sized + Extend<Parameters> + 'static {
    type ParamIter: IntoIterator<Item = Parameters>;
    fn current_params(&self)->Self::ParamIter;
    
    type Instance: InstanceFeatures;
    fn build(self)->Self::Instance;
}

trait DynBuilder {
    fn dyn_current_params(&self)->Box<dyn Iterator<Item=Parameters>>;
    fn dyn_build(self: Box<Self>)->Box<dyn InstanceFeatures>;
    fn push_param(&mut self, param: Parameters);
}

impl<T:Builder> DynBuilder for T {
    fn dyn_current_params(&self)->Box<dyn Iterator<Item=Parameters>> {
        Box::new(self.current_params().into_iter())
    }
    
    fn dyn_build(self: Box<Self>)->Box<dyn InstanceFeatures> {
        Box::new(self.build())
    }
    
    fn push_param(&mut self, param: Parameters) {
        self.extend(std::iter::once(param));
    }
}

impl Extend<Parameters> for Box<dyn DynBuilder> {
    fn extend<I:IntoIterator<Item=Parameters>>(&mut self, iter:I) {
        iter.into_iter().for_each(|p| self.push_param(p));
    }
}

trait InstanceFeatures {}

(Playground, no tests)

1 Like

Thanks! That looks readable. We will go for a builder then.

You can get pretty close to “just doing Box::new(Plugin) for a struct Plugin that implements InstanceInit”. A struct like

struct PluginInit {
    get_default_parameters: fn() -> Vec<Parameter>,
    initialize: fn(parameters: Vec<Parameter>) -> Result<Box<dyn InstanceFeatures>, Box<dyn Error>>,
}

is pretty similar to a vtable of a trait object (except that there’s no self parameters). It could have a method so that you can build it with PluginInit::new::<MyPlugin>(). E.g.

fn main() -> Result<(), Box<dyn Error>> {
    let initializer = PluginInit::new::<MyPlugin>();
    // ...
    let params = initializer.get_default_parameters();
    let _plugin = initializer.initialize(params)?;
    Ok(())
}

(playground)

Edit: You could even add a name field in this setting.

1 Like

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.