Yet Another "cannot borrow `x` as mutable more than once at a time"

Firstly, I think I think I understand ownership and borrowing concepts to a fair extent. So, my purpose in this post is not to ask "How do I fix this?", I'd rather like to ask "How do I design this in a way that I can avoid this trap to a fair extent?".

I try to write a yet another dependency injection library with Rust these days. Let me show the code, then elaborate:

use std::any::Any;

use crate::module::Module; // this is just an empty struct, a WIP, will be implemented later

/// A function that takes a reference of the module and
/// produces an instance of a dependency.
type Producer = Box<dyn Fn(&Module) -> Box<dyn Any>>;

/// A struct defining the dependency.
struct Dependency<'a> {
    /// The reference to the module that this dependency is under.
    module: &'a Module,
    /// The instance of the dependency.
    instance: Option<Box<dyn Any>>,
    /// Whether the dependency is singleton or not.
    /// If the dependency is singleton, it will return
    /// the same reference even if you produce it again.
    is_singleton: bool,
    /// The producer to produce the instance of dependency.
    producer: Producer,
}

impl<'a> Dependency<'a> {
    fn new(module: &'a Module, producer: Producer, is_singleton: bool, is_lazy: bool) -> Self {
        debug!("Constructing dependency...");

        let instance = if is_lazy {
            None
        } else {
            debug!("Initializing the dependency...");
            Some(producer(module))
        };

        Self {
            module,
            instance,
            is_singleton,
            producer,
        }
    }

    /// This method produces and/or returns the instance of the dependency.
    fn get(&mut self) -> &Box<dyn Any> {
        debug!("Getting the instance of dependency...");

        if self.is_singleton {
            match self.instance {
                Some(_) => {
                    debug!("Instance has already been created and is set as singleton.");
                    debug!("The reference to the singleton instance will return.");
                    self.instance.as_ref().expect("msg") // TODO msg
                }
                None => {
                    debug!("Instance has not been created yet.");
                    debug!("Initializing the singleton instance of dependency...");

                    self.instance = Some((self.producer)(self.module));
                    self.instance.as_ref().expect("msg") // TODO msg
                }
            }
        } else {
            debug!("Initializing the instance of dependency...");
            self.instance = Some((self.producer)(self.module));
            self.instance.as_ref().expect("msg") // TODO msg
        }
    }
}

Let me explain. The dependencies are stored in a struct conveniently named Dependency. This struct has a couple of properties, the rationale of which can be seen below:

Dependency Struct Rationale
producer A Box<dyn Fn(&Module) -> Box<Any> to produce a dependency.
is_singleton Whether it is a singleton or not.
instance An Option<Box<Any>> that represents the instance.
- If is_eager is passed to Dependency::new, this will be produced instantly on initialization.
- If is_singleton is passed, the instance will not be produced a second time, resulting in the same data in the same address.
module A &Module to pass down to producer invocation when Dependency::get is called.

The problematic method in question is Dependency::get. It is a &mut self method. Naturally, it will not compile if it's called the second time in the same scope, hence the test below...

    // compare if objs are in the same location in memory
    #[rstest]
    fn test_singleton(test_module: Module, #[values(true, false)] is_singleton: bool) {
        let mut dep = Dependency::new(&test_module, Box::new(|_| Box::new(0)), is_singleton, true);
        let obj1 = dep.get();
        let obj2 = dep.get(); // (a)
        let should_be_same = is_singleton;
        assert_eq!(obj1 as *const _ == obj2 as *const _, should_be_same);
    }

...will fail miserably on line (a) since we cannot borrow 'dep' as mutable more than once at a time.

On the other hand, Dependency::get has to be &mut self because (i) I set Dependency.is_instance in there and (ii) the instance has to be owned by some struct in order to be returned as reference in Dependency::get.

Possible Question: Why do you even return a reference from Dependency::get anyway?
That's because, if I return T and not &T, the instance will be owned by the caller. This is not a desirable approach for singletons.

So, as I've said in the beginning, "How do I fix this?" is not the problem here. Is this kind of architecture wrong? Should I take another path? Maybe, a single Dependency struct for both singleton dependencies and factory dependencies is not a good solution at all. Maybe, I need to define different struct for each of them such as SingletonDependency and FactoryDependency. I can't comprehend such an approach would solve these kind of issues though, what do you think?

Thanks in advance.

Environment
- rustc 1.54.0 (a178d0322 2021-07-26)

Yeah, I do think that the architecture here seems pretty bad. It has the following things, all of which I consider yellow or red flags:

  1. Use of dyn Any
  2. Factories/producers
  3. Lifetimes on long-lived structs.
1 Like

Building the dependency injection for Rust is like building a bridge for a ship.
Sometimes you have to do that (how else can you go from Caspian Sea to Red Sea) but 9 times out of 10 it's just wrong tool for a job. And when you do need it's much harder to do that build a bridge for car.
This the critical question is: why do you think you even need a dependency injection in your program?
Most languages which have extensive dependency injection libraries need them as a poor substitute to Hindley–Milner type system.
Rust's type system is not as complete as, e.g., Haskell's one, but 90% of time you just don't need dependency injection at all — thus I try to write a yet another dependency injection library with Rust is just insufficient explanation.

Factories/ServiceDirectories you typically need to return Arc. Scope-bound temporaries (&) will be cumbersome, and especially painful if you try to put them in structs.

In your case it breaks down because &mut self borrows all of self exclusively for one caller, and this "lock" has effect for as long as any reference it returned is still accessible. This prevents errors like:

let x = hash_map.get("x"); 
hash_map.clear(); 
deref(x);

but in your case it means that get can be used for only one object at a time. You could avoid that &mut with OnceCell, but I suggest avoiding singletons altogether. Rust doesn't like global variables.

Remember that returning objects "by reference" in other languages with a garbage collector is not the same as & in Rust. Rust's borrowing is not a general-purpose way to reference an object. It's a very limited feature applicable only in narrow circumstances. Java's Object by reference in Rust is closer to Arc<Mutex<Object>>, rather than &Object.

2 Likes

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.