Register structs implementing specific trait

Hi everybody,

I have currently a working code that register struct init function inside a singleton, to allow building when required.

Here the current code
pub trait ComponentBuilder: Send + Sync where Self: 'static {
    fn new() -> Box<dyn ComponentBuilder> where Self: Default {
        Box::new(Self::default())
    }
    // Some other functions...
    fn build(&self) -> Result<Box<dyn Component>, CustomError>;
}

static REGISTERED_COMPONENT_BUILDERS: Mutex<Vec<fn() -> Box<dyn ComponentBuilder>>> = Mutex::new(Vec::new());

pub fn register_new_component_builder(builder: fn() -> Box<dyn ComponentBuilder>) -> Result<(), CustomError> {
    match REGISTERED_COMPONENT_BUILDERS.lock() {
        Ok(mut builders) => {
            if is_builder_registered(builder().get_id(), builders.as_ref()) {
                Err(CustomError::from(ErrorType::BuilderAlreadyRegistered))
            } else {
                builders.push(builder);
                Ok(())
            }
        }
        Err(error) => {
            Err(CustomError::from(error))
        }
    }
}

fn is_builder_registered(id: String, builders: &Vec<fn() -> Box<dyn ComponentBuilder>>) -> bool {
    builders.iter().any(|b| b().get_id() == id)
}

pub fn get_builder_for_id(id: String) -> Result<Option<Box<dyn ComponentBuilder>>, CustomError> {
    match REGISTERED_COMPONENT_BUILDERS.lock() {
        Ok(builders) => Ok(builders.iter().map(|b| b()).find(|b| b.get_id() == id)),
        Err(error) => Err(CustomError::from(error)),
    }
}

// In main.rs :
fn register_components() {
    register_component(FirstExampleBuilder::new);
    register_component(SecondExampleBuilder::new);
    register_component(ThirdExampleBuilder::new);
}

fn register_component(builder: fn() -> Box<dyn ComponentBuilder>) {
    let builder_id = builder().get_id();
    if let Err(error) = component_builder::register_new_component_builder(builder) {
        warn!("Could not register component '{builder_id}' : {error}");
    }
}

I want to make a more concise code. A simple way to register each struct implementing the ComponentBuilder trait.
I was thinkg about macro (attribute-like maybe ?), but I have never write it, and I'm not sure this can resolve my problem.
I've understand that macro will be replaced with code, so the code must be called at a moment.

So do you have any ideo how to simplify this ? Or is too complex and I must let the code as is ?

Thanks by advance ! :slight_smile:

Could you share a little more context on what you are trying to achieve? As far as I can see, the only way a new builder type could come about is by adding it to the code. So I'll assume what you want is a top-level immutable registry of component builders. I don´t think it is currently possible to do this through an attribute macro. I'd suggest initializing the registry using lazy_static instead:

use std::{any::type_name, collections::BTreeMap, error::Error};

pub trait ComponentBuilder: Send + Sync + 'static {
    fn registry_entry() -> (&'static str, Box<dyn ComponentBuilder>)
    where
        Self: Sized + Default,
    {
        (type_name::<Self>(), Box::new(Self::default()))
    }

    fn build(&self) -> Result<Box<dyn Component>, Box<dyn Error>>;
}

lazy_static::lazy_static! {
    static ref REGISTERED_COMPONENT_BUILDERS: BTreeMap<&'static str, Box<dyn ComponentBuilder>> =
    BTreeMap::from_iter([ABuilder::registry_entry(), BBuilder::registry_entry()]);
}

Since the builders are registered only once in a central location, there isn't really a need to handle doubly registered builders. And since we have implemented the registry as a map, we can now find a builder without iterating over it (complete example):

fn main() {
    let builder = REGISTERED_COMPONENT_BUILDERS
        .get("playground::ABuilder")
        .expect("missing component builder");
    let component = builder
        .build()
        .expect("failed to build component");
    println!("built: {}", component.name());
}
1 Like

Hi, and thank you for the time you spend to help me.

I am trying to develop a CMS to improve my rust skills.

I have a singleton that store all available component builders (article, slider, header, ...) and thanks to the id, I can retrieve the right id to build the component associated.

I have refactorized the code a bit today, so here :

traits definition
pub trait Component: Debug {
    fn render(&self, cdn_base_url: String) -> String;
}

pub trait ComponentBuilder: Send + Sync
where
    Self: 'static,
{
    fn new() -> Box<dyn ComponentBuilder>
    where
        Self: Default,
    {
        Box::new(Self::default())
    }
    fn get_id(&self) -> String;
    fn with_content(&mut self, _content: Value) {}
    fn with_pages(&mut self, _pages: Vec<String>) {}
    fn build(&self) -> Result<Box<dyn Component>, CustomError>;
}
singleton storing builders (with functions to allow storing and retrieving)
static REGISTERED_COMPONENT_BUILDERS: Mutex<Vec<fn() -> Box<dyn ComponentBuilder>>> = Mutex::new(Vec::new());

pub fn register_new_component_builder(builder: fn() -> Box<dyn ComponentBuilder>) -> Result<(), CustomError> {
    match REGISTERED_COMPONENT_BUILDERS.lock() {
        Ok(mut builders) => {
            let builder_already_registered = builders.iter().any(|b| b().get_id() == builder().get_id());

            if builder_already_registered {
                Err(CustomError::from(ErrorType::BuilderAlreadyRegistered))
            } else {
                builders.push(builder);
                Ok(())
            }
        }
        Err(error) => {
            Err(CustomError::from(error))
        }
    }
}

pub fn get_builder_for_id(id: String) -> Result<Option<Box<dyn ComponentBuilder>>, CustomError> {
    match REGISTERED_COMPONENT_BUILDERS.lock() {
        Ok(builders) => Ok(builders.iter().map(|b| b()).find(|b| b.get_id() == id)),
        Err(error) => Err(CustomError::from(error)),
    }
}
Code in `main.rs` to register all implemented builders

fn register_components() {
register_component(ArticleBuilder::new);
register_component(CarouselBuilder::new);
register_component(HeaderBuilder::new);
}

fn register_component(builder: fn() -> Box) {
let builder_id = builder().get_id();
if let Err(error) = component_use_case::register_new_component_builder(builder) {
warn!("Could not register component '{builder_id}' : {error}");
}
}

I have next a function that use maud to create DOM and call render on a components list that I build thanks to the singleton :

rendering
fn build_html_content(current_site: Site, current_page: Page) -> String {
    let components = get_components_for_page(&current_page.name);

    let content = html! {
        (DOCTYPE)
        html lang="fr" {
            head {}
            body {
                main {
                    @for component in &components {
                        (PreEscaped(component.render(CDN_URL.into())))
                    }
                }
            }
        }
    };

    content.into_string()
}

fn get_components_for_page(name: &str) -> Vec<Box<dyn Component>> {
    match name.to_lowercase().as_str() {
        "home" => {
            let mut components: Vec<Box<dyn Component>> = vec![];

            match build_component("mhc-header".into(), json!({
                "brand_logo": "static/assets/mhc-typo-yellow.webp",
                "pages": get_pages()
            })) {
                Ok(header) => components.push(header),
                Err(error) => println!("error : {error}"),
            }

            for i in 0..3 {
                match build_component("mhc-article".into(), json!({
                    "title": format!("Titre {}", i),
                    "text": "Description"
                })) {
                    Ok(header) => components.push(header),
                    Err(error) => println!("error : {error}"),
                }
            }

            match build_component("mhc-carousel".into(), json!({
                "direction": "column",
                "reverse": false,
                "center": false,
                "slides": ["1", "2", "3"]
            })) {
                Ok(header) => components.push(header),
                Err(error) => println!("error : {error}"),
            }

            components
        }
        "about" => {
            let mut components: Vec<Box<dyn Component>> = vec![];

            match build_component("mhc-header".into(), json!({
                "brand_logo": "static/assets/mhc-typo-yellow.webp",
                "pages": get_pages()
            })) {
                Ok(header) => components.push(header),
                Err(error) => println!("error : {error}"),
            }

            if let Ok(article) = build_component("mhc-article".into(), json!({
                "title": "Titre",
                "text": "Description"
            })) {
                components.push(article);
            }

            components
        }
        _ => vec![],
    }
}

In the last block of code, I put the function get_components_for_page to show you how I build components. Just imagine I get json data from database and not hard coded.

So I want to simplify code in the main.rs file to register builders, maybe thanks to a macro ? (like #[register_component] on top of structs that implements ComponentBuilder)

Another change i'd suggest (especially if components and component builders are to be made by other people), is to separate the builder trait from the object-safe builder. This way, you can be a little more precise in the API.

pub trait ComponentBuilder: Send + Sync + 'static {
    type Component: Component;
    type Error: Error + Send + Sync + 'static;
    fn build() -> Result<Self::Component, Self::Error>;
    // Other methods...
}

This specifies that the component builder determines the component type as well as the error type. Obviously, this trait is not object-safe, so we need an object-safe version of it to save the builders in the registry.

pub trait ComponentBuilderObj: Send + Sync + 'static {
    fn registry_entry() -> (&'static str, Box<dyn ComponentBuilderObj>)
    where
        Self: ComponentBuilder + Sized + Default;
    fn build(&self) -> Result<Box<dyn Component>, Box<dyn Error>>;
}

impl<T: ComponentBuilder> ComponentBuilderObj for T {
    fn registry_entry() -> (&'static str, Box<dyn ComponentBuilderObj>)
    where
        Self: Sized + Default,
    {
        (type_name::<Self>(), Box::new(Self::default()))
    }

    fn build(&self) -> Result<Box<dyn Component>, Box<dyn Error>> {
        let component = <Self as ComponentBuilder>::build().map_err(Box::new)?;
        Ok(Box::new(component))
    }
}

This is called "global registration" and is not currently possible short of proc macro hacks that will likely break in the future. You'll need some function that explicitly registers them or calls some other function that registers them.

6 Likes

Hi, thanks for your answer, but I don't really understand it.

Ok, thank you for your answer !
Too bad, it would have been cool to simplify things like that :pensive:

This was conscious decision. C++ have something like that and it may even be [ab]used to provide such facilities to Rust (essentially: register your types via FFI from C++ module or use platform-dependent asm to [ab]use these things that C++ used from Rust directly) but Rust developers have consciously decided not to go that way.

The problem here is it's but both pretty useful and convenient and incredibly fragile because sooner or later you would have many such registries and then contest of “who should register first” ensues.

As both someone who uses these facilities at my $DAYJOB and is forced to fix them when they, invariably, break… it's not worth it. Just register things manually, things are much simpler and more robust that way.

Thanks for your explanations.

In fact, in this project, I don't think I will have a lot of registries, but I understand that it can cause problems.

I have refactorized the code to be more clear in that way.

1 Like

Hey,

Just to be sure...
I was looking for serde trait impl, and I have found typetag that do it easily.
But when reading the doc, there is a paragraph saying :

We use the inventory crate to produce a registry of impls of your trait, which is built on the ctor crate to hook up initialization functions that insert into the registry. The first Box<dyn Trait> deserialization will perform the work of iterating the registry and building a map of tags to deserialization functions. Subsequent deserializations find the right deserialization function in that map. The erased-serde crate is also involved, to do this all in a way that does not break object safety.

So there is a crate (inventory) that let you collect all the implementations of a trait, and do something with it. Maybe it could be possible to use it to register automatically builders ?
Or maybe create a macro (like they create inventory::collect!(...); in inventory), and call it at the end of the implementation ?

Or it is something really complex and I maybe not understand the goal of this crate ?

That's the exact thing @SkiFire13 mentioned, a proc macro hack that will likely break. It involves some linker magic and pre main lifetime as well.

With that said, you still can use inventory if you see it fits your need. Just don't be surprised when it breaks.

Where “do something” == “register your types via FFI from C++ module or use platform-dependent asm to [ab]use these things that C++ used from Rust directly”.

That's exactly what I was talking in comment there.

The main problem: since it [ab]uses facility that was only designed for C++ (which officially supports “life before main”) but people are, naturally, expecting that Rust would be functional there it does some nasty hacks to bring Rust into semi-functional state for these.

There are fresh blog post which explains the issues with such feature (and these hacks) in many details.

Yeah. I understand that. It's built upon something that not designed for Rust (at least for now). It is fragile in this sense.

But sometimes people can accepted some degree of fragility. Not everything are meant to be perfect and indefinitely future-proof. They just want something working right now. And there's a difference between "this might silently break between any rustc versions (or even different compile sessions)" and "this might loudly break when rustc make major changes".

When used with care (who am I kidding), I think it's fine™. But yeah, if you want to write any serious program, it is not really worth it. It's not like the ordinary way is that unbearable anyway.

If all the relevant implementations are contained within a single crate, it’s also possible via a build script that scans the codebase for the registration marks and emits a .rs file which contains a function to register everything (which will need to be called explicitly).

2 Likes

Oh ok thank you all

I do not understand that it was what you were talking about.
I don't know C++, it is my first language where I have to handle memory with ref and clone, I'm a bit lost sometimes ^^'

Thank you again for your explanations ! I will stay with a simple function that register builders.

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.