#Deferring guard for cross-thread dependency injection

15 messages · Page 1 of 1 (latest)

icy nova
#

Hello, I am writing a dependency injection based workflow for a project. I have a

HashMap<TypeId, Arc<RwLock<dyn Any>>>

where injectable resources are stored. I'm basing my API off a similar style like that used in Bevy, something like

fn my_step(res1: &ResourceType1, res2: &mut ResourceType2) { ... }

It needs to support variadic argument lengths with any combo of & and &mut so instead of implementing for every combination I'm dealing with an abstraction for each parameter. The abstractions are responsible for retrieving the value from the map so it can be passed to the function, this is where my issue lies.

I'm effectively following this resource https://promethia-27.github.io/dependency_injection_like_bevy_from_scratch/introductions.html however I have one major change for my purposes, my system needs to work across threads for parallelization. So instead of a Box<dyn Any> I'm dealing with an Arc<RwLock<dyn Any>> which works up until this point without issue. However because the abstraction for a given parameter is responsible for knowing how to get the value, the abstraction needs to be the one to obtain the read or write lock (since outside the abstraction it doesn't know if it needs mutability or not). But the value is only valid as long as the lock is kept alive, and the value is needed outside the abstraction. In essence like the following:

let value1 = param1.get_value(&map); // RwLockReadGuard is obtained within get_value
let value2 = param2.get_value(&map); // RwLockWriteGuard is obtained within get_value

(self.func)(value1, value2); // Call the method with dependencies injected

// I need some way to defer the releasing of the guards to here

I've tried a variety of approaches so far, the one I felt had the most promise would be to have the value be returned wrapped in a struct that also takes over ownership of the guard, such that when it goes out of scope after the call to func() the lock would be released instead of within the get_value(). I don't know to get such a concept working and whether of not that's even the best approach.

unborn patrol
# icy nova Hello, I am writing a dependency injection based workflow for a project. I have ...

have the value be returned wrapped in a struct that also takes over ownership of the guard, such that when it goes out of scope after the call to func() the lock would be released instead of within the get_value()
That's one option. The other option is to become very bevy-esque again and use a closure:

map.with(|(value1, value2)| (self.func)(value1, value2))
```Basically, you'd have to do bevy-style dependency injection _inside `with`_. Then you can simply hold the guards until the closure returns
#

If you want to return the guards (or wrappers around them, I suppose) instead, that'll work too, but it might be harder, Unfortunately, the way I understand the bevy style of dependency injections is argument-based, and I'm not sure how to translate it to get_value calls. If you want to do that, could you elaborate on what you tried?

icy nova
# unborn patrol If you want to return the guards (or wrappers around them, I suppose) instead, t...

I only losely understand the concept myself at this point. The general gist is I'm writing a pipeline system, a series of steps that will be executed in order, and (when finished) will automatically determine which steps can run in parallel and which ones need to wait for previous steps to finish, using the presence of the mutable arguments. That's the target, so the goal is to allow the user to add any function where the arguments are of the type of resource requested, so for example:

pipeline.add_step(a_custom_step);

...

fn a_custom_step(ctx: &PipelineContext, cache: &mut PipelineCache) {
   ...
}

To facilitate allowing any number of args I have the pipeline defined as such:

struct Pipeline {
    steps: Vec<StoredStep>,
    resources: HashMap<TypeId, Arc<RwLock<dyn Any>>>
}
type StoredStep = Box<dyn Step>;

trait Step {
    fn run(&mut self, resources: &mut HashMap<TypeId, Arc<RwLock<dyn Any>>>);
}

And the add_step works like such:

pub fn add_step<I, S: Step + 'static>(&mut self, step: impl IntoStep<I, Step = S>) {
    self.steps.push(Box::new(step.into()));
}
...
trait IntoStep<Input> {
    type Step: Step;

    fn into(self) -> Self::Step;
}

Due to the need for varargs I'm implementing the IntoStep and Step in macros, where the output (in example for a 2-parameter function) is as such:

impl<F: FnMut(T1, T2), T1: StepParam + 'static, T2: StepParam + 'static> Step for FunctionStep<(T1, T2), F> {
    fn run(&mut self, resources: &mut HashMap<TypeId, Arc<RwLock<dyn Any>>>) {
        let mut lock1 = resources.get(&TypeId::of::<T1>()).unwrap().write().unwrap();
        let mut lock2 = resources.get(&TypeId::of::<T2>()).unwrap().write().unwrap();

        let mut value1 = T1::get_value(resources); // This is where the actual value is read, in an implementation dependant on T1 and T2's actual type
        let mut value2 = T2::get_value(resources);

        (self.f)(
            value1,
            value2,
        );
    }
}

StepParam has a mutable and immutable implementation. This part of the system isn't really complete yet as I need to resolve the issue of getting the values out before I can continue.

icy nova
icy nova
icy nova
#

This is so annoying. All I wish to do if have some abstract way to get a value out of the RwLock, the lock would be safely released at the end of the usage of the value, but I keep running into many different issues, being either can't reverence a value owned by the function, or spending 5 hours messing around with lifetimes and still not having it work. In any other language this would be among the most trivial of tasks. The closest I've gotten so far was making the trait into two parts, one part to retrieve the lock similar to below:

trait StepParam {
    fn get_guard<'a: 'b, 'b>(lock: &'a RwLock<dyn Any>) -> GuardType<'b>;
    fn get_value<'b>(guard: &'b mut GuardType<'b>) -> Self;
}
enum GuardType<'a> {
    ReadGuard(RwLockReadGuard<'a, dyn Any>),
    WriteGuard(RwLockWriteGuard<'a, dyn Any>)
}

But I'm having issue with lifetimes, I get it seemly so close:

fn test() {
    let test: HashMap<TypeId, Arc<RwLock<dyn Any>>> = HashMap::new();

    let arc = test.get(&TypeId::of::<String>()).unwrap();

    let mut guard = ResourceMut::<String>::get_guard(arc);
    let value = ResourceMut::<String>::get_value(&mut guard); // `guard` does not live long enough
                                                              // borrowed value does not live long enough
}

I have spent the last 5 hours trying everything I can think of to get the lifetimes right but nothing works. And for the system to operate properly StepParam must not take any parameters, including lifetimes. I'm just at wits end at this point.

cinder oyster
#

How did you implement the get_value for ResourceMut?

icy nova
#

in the example above basically like the following:

    fn get_value<'b>(guard: &'b mut GuardType<'b>) -> Self {
        match guard {
            GuardType::ReadGuard(_guard) => panic!("Pipeline passed Read guard into mutable resource"),
            GuardType::WriteGuard(guard) => ResourceMut {
                value: guard.downcast_mut::<T>().unwrap(),
            }
        }
    }

Ignore the lifetimes, i've tried many different things that's just what it's as at the moment.

As a note panicking is actually intended here if it's not the right type of lock. The main issue I'm having with this approch is with lifetimes, I can't get Rust to understand the value will not last as long as the guard.

cinder oyster
#

Have you tried having the Resource mut have lifetimes based on the Guard?

struct ResourceMut<'a, T> {
  value: &'a T
}```
Why doesn't this work? The resourcemut is dropped before the guard, right?

(I'm not sure if I'm being helpful here, just throwing ideas against the wall lol)
icy nova
#

I currently have a lifetime specified on the resource:

struct ResourceMut<'a, T: 'static> {
    value: &'a mut T,
}

My main problem with lifetimes is coming from the trait I'm implementing

trait StepParam {
    fn get_guard<'b>(lock: &'b RwLock<dyn Any>) -> GuardType<'b>;
    fn get_value<'b>(guard: GuardType<'b>) -> (GuardType<'b>, Self);
}

A pretty critical piece of this system is this trait has to be parameterless or it can't easily be Boxed. But the methods that take in the guard are defined on the trait, not directly on the ResourceMut. I can't force ResourceMut to have the same lifetime as the guard because that would require that lifetime to be a parameter to the trait so that it can be used in the method signature.

Also technically no the resource isn't dropped before the guard. I tried two main approaches:

  1. Storing the guard in the resource to keep the guard alive until the resource is dropped
  2. As I have above where I can take in a RwLock and return the guard wrapped in an enum.

I was using a mutable reference to the guard, but above I tried having the guard moved into the method and returned back. The main issue here is there's no way I can find to get the value back, in that get_value method it needs to return a ResourceMut<'b> so that it has the same lifetime as the guard, but again that function is defined in the trait, which can't be parameterized, therefore I can not bind the lifetime 'b to the lifetime defined in the struct implementation:

impl<'a, T: 'static> StepParam for Resource<'a, T> {
    fn get_guard<'b>(lock: &'b RwLock<dyn Any>) -> GuardType<'b> {
        GuardType::ReadGuard(lock.read().unwrap())
    }
    fn get_value<'b>(mut guardType:  GuardType<'b>) -> (GuardType<'b>, Self) {
        match guardType {
            GuardType::ReadGuard(guard) => {
                (
                    guardType,
                    Resource {
                        value: guard.downcast_ref::<T>().unwrap()
                    }
                )
            }
            GuardType::WriteGuard(_guard) => panic!("pipeline passed Write guard into immutable resource")
        }
    }
}

If I try to add a bounds like <'b: 'a> it errors that the function doesn't match the trait. I'm honestly at whits end here. In any other language this would be simple, I abstract out the retrieval of the guard, I get the value, use the value, and release the guard. And it should work just the same here, but I can't figure out how to let rust know that I will release the value before the guard is dropped, I know from a code structure/flow perspective as a person that the value will be dropped before the guard is, but Rust can't infer that, it has to be explicitly told this but I don't understand how to specify lifetimes well enough to get rust to understand my intents...

cinder oyster
#

Have you tried explicitly dropping the value? I.e. by using drop?

icy nova
#

Not sure I understand

#

I can't even get Rust to allow me to return the value from the get_value method, as it says value may not live long enough. if I manually drop it it'd be later than that

icy nova
#

It hasn't been fully tested, but I believe I have solved my issue.

Before, the Resource implemented StepParam which returned Self. This was done because the various implementations needed to return their own types, as a mutable parameter needs to store a mutable reference and etc. The problem I was facing was that because I needed to effectively box the step parameters and other details the StepParam couldn't have type annotations. Returning Self forced the returned Resource to have the same lifetime, now I wasn't dealing with an instance of resource as the get_value method doesn't take a reference to self, so I'm not sure what the lifetime is but I have a suspicion it's 'static or something similar, which of course would outlive the value from the map. But without type annotating StepParam I couldn't figure a way to return an appropriate lifetime. The solution to this issue was actually quite simple:

trait StepParam {
  type Item<'b>;

  fn get_value<'a>(resources: &'a HashMap<TypeId, Arc<RwLock<dyn Any>>>) -> Self::Item<'a>;
}

Instead of returning Self I use a GAT, the GAT can be type annotated and defined in each implementation, so for Res<'a, T> I could define type Item<'b> = Res<'b, T>; and return it from the function, thus it carries the proper lifetime. That was problem 1. Problem 2 was easy to solve after that, instead of storing the resources as follows:

struct Res<'a, T: 'static> {
    value: &'a T
}
struct ResMut<'a, T: 'static> {
    value: &'a mut T
}

I now store it as:

struct Res<'a, T: 'static> {
    value: RwLockReadGuard<'a, dyn Any>,
    _marker: PhantomData<&'a T>
}
struct ResMut<'a, T: 'static> {
    value: RwLockWriteGuard<'a, dyn Any>,
    _marker: PhantomData<&'a mut T>
}

Now the implementation could just work:

impl<'r, T: 'static> StepParam for Res<'r, T> {
  type Item<'b> = Res<'b, T>;

  fn get_value<'a>(resources: &'a HashMap<TypeId, Arc<RwLock<dyn Any>>>) -> Self::Item<'a> {
    return Res {
      value: resources.get(&TypeId::of::<T>()).unwrap().read().unwrap(),
      _marker: PhantomData
    }
  }
}
impl<'r, T: 'static> StepParam for ResMut<'r, T> {
  type Item<'b> = ResMut<'b, T>;

  fn get_value<'a>(resources: &'a HashMap<TypeId, Arc<RwLock<dyn Any>>>) -> Self::Item<'a> {
    ResMut {
      value: resources.get(&TypeId::of::<T>()).unwrap().write().unwrap(),
      _marker: PhantomData
    }
  }
}

There were other changes, but those two combined seem to allow the system to work now.

Thanks for the help everyone.