0

I’m building a Rust library that manages multiton instances keyed by strings. Users of the library provide factory methods to create instances implementing a trait, and the library should:

  • Return the existing instance if one exists for a key.
  • Otherwise, call the user-supplied factory method to create and store the instance followed by return.

Library code (crate)

pub trait IFoo {
    // trait methods
}

pub struct Foo {
    key: String,
}

// question is how to implement Foo so it works as per usage example???
impl Foo { 
    // Static map of instances keyed by String
    // static instance_map: HashMap<String, Box<dyn IFoo>> = HashMap::new();

    // Factory method for users to create IFoo instances
    pub fn get_instance(
        key: String, 
        factory: impl Fn(String) -> Box<dyn IFoo> + 'static
    ) -> &Box<dyn IFoo> {
        // TODO: check instance_map for key,
        // if exists, return existing instance,
        // else create via factory, store, and return new instance.
    }
}

Users of the Library Code

struct MyFoo(Foo);

impl MyFoo {
    pub fn new(key: String) -> Self {
        Self(Foo { key })
    }
}

impl IFoo for MyFoo {
    // trait implementation
}

// Usage example:
let instance = Foo::get_instance("myfoo".to_string(), |k| Box::new(MyFoo::new(k)));
7
  • check out Lazylock to help with the static map problem. Do you need mutable reference to instance? If you're building a library, should there be generic type for Foo and IFoo that is needed to for user to use? Commented May 30 at 3:22
  • Are you struggling with the static mutability part? Or the logic to only conditionally run and store? Commented May 30 at 4:05
  • @kmdreko get_instance checks for existing key and returns the value (instance) if found, otherwise it calls the user-supplied factory method to create the instance, store in the map and return it. Commented May 30 at 4:41
  • the last-line code under Usage example implies how the function get_instance will be called, I'm looking for implementation for Foo Commented May 30 at 4:44
  • @啊鹿Dizzyi I am looking for Foo implementation, so it works as per the Usage example (under users of the library) Commented May 30 at 4:47

1 Answer 1

2

use LazyLock to make static variable, but that can only make immutable access to the map, so use Mutex to gain mutable access to the map, as for the Box change it to Arc to make it able to get over borrow checker, since if you want to borrow from static variable, it get weird, and since you don't need mutable reference to the object, Arc is good enough

Example

mod lib {
    use std::collections::HashMap;
    use std::fmt::Debug;
    use std::sync::{Arc, LazyLock, Mutex};

    pub trait IFoo: Sync + Send + Debug + 'static {}

    #[derive(Debug)]
    pub struct Foo {
        pub key: String,
    }

    static IFOO_REGISTRY: LazyLock<Mutex<HashMap<String, Arc<dyn IFoo>>>> =
        LazyLock::new(|| Default::default());
    // make it private to make sure user cannot access it and modify it

    // this doesn't need to memeber of Foo
    pub fn get_instance<T: IFoo>(
        key: String,
        factory: impl Fn(String) -> T,
    ) -> Arc<dyn IFoo> {
        let mut registry = IFOO_REGISTRY.lock().unwrap();
        match registry.get(&key) {
            Some(a) => a.clone(),
            None => {
                let arc = Arc::new(factory(key.clone()));
                registry.insert(key.clone(), arc.clone());
                arc
            }
        }
    }

    pub fn print_registry() {
        let registry = IFOO_REGISTRY.lock().unwrap();
        println!("Registry {{");
        for (k, v) in registry.iter() {
            println!("    {k} : {v:?}")
        }
        println!("}}");
    }
}

use lib::*;

#[derive(Debug)]
struct MyFoo(Foo);

impl MyFoo {
    pub fn new(key: String) -> Self {
        Self(Foo { key })
    }
}

impl IFoo for MyFoo {}

fn main() {
    print_registry();
    // Registry {
    // }

    let instance = get_instance("myfoo".to_string(), |k| MyFoo::new(k));
    println!("{:?}", instance);
    // MyFoo(Foo { key: "myfoo" })

    print_registry();
    // Registry {
    //     myfoo : MyFoo(Foo { key: "myfoo" })
    // }

    // second call
    let instance = get_instance("myfoo".to_string(), |k| MyFoo::new(k));
    println!("{:?}", instance);
    // MyFoo(Foo { key: "myfoo" })

    let instance = get_instance("myfoo2".to_string(), |k| MyFoo::new(k));
    println!("{:?}", instance);
    // MyFoo(Foo { key: "myfoo2" })

    print_registry();
    // Registry {
    //     myfoo2 : MyFoo(Foo { key: "myfoo2" })
    //     myfoo : MyFoo(Foo { key: "myfoo" })
    // }
}

Edit

updated to get rid of function type param, change to FnOnce, and Box<T>

mod lib {
    use std::collections::HashMap;
    use std::fmt::Debug;
    use std::sync::{Arc, LazyLock, Mutex};

    pub trait IFoo: Sync + Send + Debug + 'static {}

    #[derive(Debug)]
    pub struct Foo {
        pub key: String,
    }

    static IFOO_REGISTRY: LazyLock<Mutex<HashMap<String, Arc<Box<dyn IFoo>>>>> =
        LazyLock::new(|| Default::default());
    // make it private to make sure user cannot access it and modify it

    // this doesn't need to memeber of Foo
    pub fn get_instance(
        key: String,
        factory: impl FnOnce(String) -> Box<dyn IFoo>,
    ) -> Arc<Box<dyn IFoo>> {
        let mut registry = IFOO_REGISTRY.lock().unwrap();
        match registry.get(&key) {
            Some(a) => a.clone(),
            None => {
                let arc = Arc::new(factory(key.clone()));
                registry.insert(key.clone(), arc.clone());
                arc
            }
        }
    }

    pub fn print_registry() {
        let registry = IFOO_REGISTRY.lock().unwrap();
        println!("Registry {{");
        for (k, v) in registry.iter() {
            println!("    {k} : {v:?}")
        }
        println!("}}");
    }
}

use lib::*;

#[derive(Debug)]
struct MyFoo(Foo);

impl MyFoo {
    pub fn new(key: String) -> Box<Self> {
        Box::new(Self(Foo { key }))
    }
}

impl IFoo for MyFoo {}

fn main() {
    print_registry();
    // Registry {
    // }

    let instance = get_instance("myfoo".to_string(), |k| MyFoo::new(k));
    println!("{:?}", instance);
    // MyFoo(Foo { key: "myfoo" })

    print_registry();
    // Registry {
    //     myfoo : MyFoo(Foo { key: "myfoo" })
    // }

    // second call
    let instance = get_instance("myfoo".to_string(), |k| MyFoo::new(k));
    println!("{:?}", instance);
    // MyFoo(Foo { key: "myfoo" })

    let instance = get_instance("myfoo2".to_string(), |k| MyFoo::new(k));
    println!("{:?}", instance);
    // MyFoo(Foo { key: "myfoo2" })

    print_registry();
    // Registry {
    //     myfoo2 : MyFoo(Foo { key: "myfoo2" })
    //     myfoo : MyFoo(Foo { key: "myfoo" })
    // }
}
Sign up to request clarification or add additional context in comments.

7 Comments

Thanks. loved it. Is it possible to remove the T and inline types?
my attempt to give you an idea: pub fn get_instance(key: String, factory: impl Fn(String) -> Arc<dyn IFoo>) -> Arc<dyn IFoo> {
It may be more concise to use the entry api and do .entry(key).or_insert_with(factory) (or something similar).
@Dizzyi I worked out the signature without the T, please review key: String, factory: impl Fn(String) -> Box<dyn IFoo>) -> Arc<dyn IFoo> {
I just change my original answer to impl FnOnce and it still works. Why do you want to get rid of the inline type <T>? If you want this you will need to wrap it around Arc<Box<_>>. and since Box<MyFoo> does not implement IFoo you can't just use Arc<dyn IFoo>. I updated the answer
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.