Search code examples
rustsynchronizationlockingtraitsdereference

Deref Mutex/RwLock to inner object with implicit locking


I have a struct Obj with a large API that I would also like to use through an Arc<RwLock<Obj>>. I have defined a struct ObjRef(Arc<RwLock<Obj>>) and want to call functions of Obj on ObjRef without needing to call .read().unwrap() or .write().unwrap() every time. (ObjRef implements Deref<Target=Arc<RwLock<Obj>>, so I only have to call objRef.read().unwrap())

I could implement all of the functions of Obj again for ObjRef in a manner like this:

impl ObjRef {
    fn foo(&self, arg: Arg) -> Ret {
        self.read().unwrap().foo(arg)
    }
}

But doing this for every function results in a lot of boilerplate.

Implementing Deref<Target=Obj> for ObjRef does not work, because the deref implementation would be returning a reference into the RwReadLockGuard object returned by .read().unwrap() which would be dropped after deref:

impl Deref for ObjRef {
    type Target = Obj;
    fn deref(&self) -> &Self::Target {
        let guard: RwLockReadGuard<'_, Obj> = self.0.read().unwrap();
        &*guard // <- return reference to Obj referenced by guard
        // guard is dropped now, reference lifetime ends
    }
}

Is there a way to call the Obj Api through a low boilerplate implementation doing the locking internally?

I am thinking about introducing a trait for the Api with a default implementation of the Api and a single function to be implemented by users of the trait for getting a generic Deref<Target=Obj>, but this puts the Api under some limitations, for example using generic parameters will become much more complicated.

Is there maybe a better way, something that lets me use any Obj through a lock without explicitly calling the locking mechanism every time?


Solution

  • It is not possible to do this through Deref, because Deref::deref always returns a plain reference — the validity of it must only depend on the argument continuing to exist (which the borrow checker understands), not on any additional state such as "locked".

    Also, adding locking generically to an interface designed without it is dangerous, because it may accidentally cause deadlocks, or allow unwanted outcomes due to doing two operations in sequence that should have been done using a single locking.

    I recommend considering making your type internally an Arc<RwLock, that is,

    pub struct Obj {
        lock: Arc<RwLock<ObjData>>,
    }
    
    // internal helper, not public
    struct ObjData {
        // whatever fields Obj would have had go here
    }
    

    Then, you can define your methods once, directly on Obj, and the caller never needs to deal with the lock explicitly, but your methods can handle it exactly as needed. This is a fairly common pattern in problem domains that benefit from it, like UI objects.

    If there is some reason why Obj really needs to be usable with or without the lock, then define a trait.