Open Michaelschnabel-DM opened 1 year ago
The single writer rules in Rust essentially make it impossible to use anything other than Rc<T>
or Arc<T>
because a singleton and scoped service instance need to be shared by design. I've run up against this issue myself. There are a few possible workarounds.
If you're in control of registration and consumption, then Rc<T>
and Arc<T>
offer several options to get a mutable pointer if, and only if, the service is transient. These include:
Rc::get_mut(this: Rc<T>)
Rc::make_mut(this: Rc<T>)
Rc::into_inner(this: Rc<T>).unwrap()
In other contexts, you run the danger of a panic if you perform one of these operations on a non-transient service. I don't know that it is helpful, but strong_count()
should always == 1
for a transient service and >= 2
for a singleton or scoped service (the container owns a copy). Any type of branching logic or conditions based on this knowledge likely over-complicates things.
Another option is to use interior mutability within the service. This tends to be the most straight forward approach to deal with this challenge, even if it's not ideal.
The OptionsMonitorCache
Refactor to have the injected service serve as a factory. The factory itself can be of any scope and shouldn't have any issue being shared behind Rc<T>
or Arc<T>
. The thing the factory creates and returns can then be mutable without any other coupling. A zero-byte struct will have very low overhead beyond the requirement of creating it. The viability of this approach will depend on the thing created. It's certainly inconvenient, but then you can create the thing you really want and have it be lock-free.
The biggest challenge about making any approach work is that the lifetime of the service is intentionally opaque. That's par for the course with DI. I'm open to other ideas, but I'm not sure how else this can be addressed. I've mostly gone with Option 2 myself and on occasion used Option 3.
I wasn't completely satisfied with my answer and I've been thinking about this for a long, long time. There actually is one more option. Honestly, it's probably the most correct way albeit also the ugliest.
The following will work in long-form. The #[injectable]
macro will not work - yet, but you could implement Injectable
on your own if you want that capability. It's very straight forward.
use std::sync::Mutex;
#[derive(Default)]
struct Counter {
count: usize,
}
impl Counter {
pub fn increment(&mut self) {
self.count += 1;
}
pub fn value(&self) -> usize {
self.count
}
}
// optional: if you want the associative functions
impl Injectable for Counter {
fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
ServiceDescriptorBuilder::<Mutex<Self>, Mutex<Self>>::new(lifetime, Type::of::<Mutex<Self>>())
.from(|_| ServiceRef::new(Mutex::new(Self::default())))
}
}
Now you can use it as:
#[test]
fn get_required_should_allow_mutable_service() {
// arrange
let provider = ServiceCollection::new()
.add(Counter::singleton())
.build_provider()
.unwrap();
// act
let counter1 = provider.get_required::<Mutex<Counter>>();
let counter2 = provider.get_required::<Mutex<Counter>>();
counter1.lock().unwrap().increment();
counter2.lock().unwrap().increment();
// assert
assert_eq!(2, counter1.lock().unwrap().value());
}
The one thing I don't really like about this approach is that an extra level of indirection is required through Mutex<T>
. It's not entirely obvious that you need to register or request Mutex<TService>
to get a writable instance. Aside for the ugliness of registration, it is fairly clear what is happening. If you don't want to hide writes with interior mutability, this is probably the next best approach.
I need to think whether there is better way to make this natural so there isn't a composition duality between registering Counter
and Mutex<Counter>
, but this is certainly one more workable option.
This question has been asked before, so I spent quite a bit of time thinking about it, experimenting (based on the previous response), and I've now added first-class support for mutable services in the 3.0 release. ServiceRef<T>
is now Ref<T>
(see the release notes). There is also now RefMut<T>
. This will map to Rc<RefCell<T>>
for sync and Arc<RwLock<T>>
for async. They are all supported by macro code generation and services can be registered as read-only or read-write for the same type.
use di::*;
#[injectable]
struct Counter {
count: usize,
}
impl Counter {
pub fn increment(&mut self) {
self.count += 1;
}
pub fn value(&self) -> usize {
self.count
}
}
You can now register and request a mutable service with first-class support.
fn main() {
let provider = ServiceCollection::new()
.add(Counter::transient().as_mut())
.build_provider()
.unwrap();
let counter = provider.get_required_mut::<Counter>();
counter.borrow_mut().increment();
}
Unfortunately, there still isn't a way to get around Arc
because Singleton
and Scoped
have shared lifetimes and need to be safe in asynchronous contexts. I don't see away around that. Arc
for Transient
should have little-to-no overhead if it is never cloned (e.g. reference count increased). The container will never hold a reference to it. If you really wanted it out of Arc
, the only other approach would be taking it out as described in Option 1. I'm not sure that is worth it, but perhaps.
Hi,
is there a way to resolve a transient object as mutable with this package? Right now i use the async feature because some stuff needs to be Arc. But now everything is contained within an Arc but i need to modify some objects after resolution (they are only used locally afterwards) so they would not require an Arc container.
Regards Michael