How can I reduce my use of Arc here?

Hi everyone! I'm hoping someone will be able to help me out here.

In the internals of coi, I unfortunately have to rely heavily on a double Arc. Here's some details about the internals of coi:

It's a dependency injection crate, so users will register a mapping of keys to providers, and will later try to resolve some type T with a previously registered key. The former is done through the container registration (example here) and the latter through resolving, e.g. container.resolve::<T>("some_key");.

The Provide type is responsible for providing the specific type T once requested. This is setup with an associated type on the Provide trait: Output. So when the providers are registered, they're stored as Arc<dyn Provide<Output = T>...> to hide the actual provide impl. Then those are stored in Arc<dyn Any...> so that the kind of Provide can also be erased so they're all the same size (the Output=T is what made it complicated for me) and can be stored in a container (the type that's converted to Arc<dyn Any> is an Arc<Arc<dyn Provide<Output=T>...>>).

The container takes ownership over the providers for now, but I'm open to alternatives. The container does usually need to be moved though, sometimes to another thread (see here in coi-actix-sample), so it's not always possible to just use a shared reference.

When the objects are requested through the resolve call, we first lookup the key to ensure we're actually storing a provider for that value (there's also other work that happens first to check for existing resolutions, but that's separate from this request for help, I think). That stored value is still just Arc<dyn Any...> so we get a reference to the inner dyn Any and attempt to call downcast_ref::<Arc<dyn Provide<Output = T>...>>() on it.

I really dislike the multiple usages of Arc here. Is there any obvious way to improve this that I'm missing, or will this require a lot more in depth analysis to fix?

This is where registration occurs: coi/lib.rs at main · Nashenas88/coi · GitHub
And this is where the downcasting and providing occurs: coi/lib.rs at main · Nashenas88/coi · GitHub

The container should be able to take ownership of the providers by taking a Box<dyn Provide<_>> and storing it as Arc<dyn Provide<_>>. If you're already providing shared ownership of the container, then instead of passing the Provide instances between threads, you could pass the container and reresolve it. I don't know if that is efficient or convenient... the way you have it now, once you resolve a provider, you can pass it around and do whatever with it without any consideration to who has access to the container, and maybe that's on purpose? If so, you need the inner Arc to enable that.

The other alternative is to remove the outer Arc, but that means someone is going to have to actually own the container. It seems odd to me that you're using Arc for the container, but without a Mutex/RwLock inside, it seems like you could probably just pass around shared references to it instead.

Right now containers are the ones passed between the threads. It's really just a newtype around Arc<Mutex<InnerContainer>> in order to simplify the scoping behavior (this is used, e.g. when a web request comes in, or a pubsub listener receives a new message, and you want to minimize the number of live objects used in the dependency tree).

That structure also causes issues with storing the providers of Arc<dyn Any> (of Arc<Box<dyn Provide<_>>>) because once I do downcast_ref, I need to expose a shared reference that originates from the container. Since resolution is recursive, and might go up the container scoping path (scoped containers have references to the parent containers that spawned them), I can't hold on to a shared reference of the container. Each provider might have been called for the first time and the container might need to be modified to store the resolved version (this is also where it gets more complicated because, depending on how a provider was registered, it's output may or may not be stored for future resolution).

I thought your ideas did give me a hint though, and I tried storing them as Box<dyn Any> (of Box<Arc<dyn Provide<_>>>, but then this breaks my Clone requirement on Container, and I can't make the box Box<dyn Any + Clone> because Clone isn't an auto trait.

Clone is needed because when spawning additional worker threads in web frameworks, I really prefer that they all share the same container for cases like singleton registrations where it's important that only one instance of a provider is available to all threads.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.