1

Any reference obtained through the interior mutability pattern, be it Rc<RefCell<T>> or Arc<RwLock<T>>, will have to be dropped as the enclosing function returns. When you want to use this reference to return a reference to some data that this RefCell or RwLock protects, it is not allowed.

I think it is right to NOT ALLOW it: because I am using interior mutability, any reference associated with the "interior" should be delegated to the runtime checker, and thus a pure reference as &T should not be exposed to the outside.

However, I could not figure out a good alternative that serves the same purpose, other than wrap the underlying data with Rc<RefCell<T>> as well.

Code that demonstrates the issue:

use std::string::String;
use std::sync::{Arc, RwLock};

struct MyStruct {
    data: MyData,
}

struct MyData {
    val: String,
}

fn main() {
    let my_struct = MyStruct {
        data: MyData {
            val: String::from("hi"),
        },
    };
    let obj = Arc::new(RwLock::new(my_struct));

    let data = get_data_ref(&obj);
    println!("{}", data);
}

fn get_data_ref(obj: &Arc<RwLock<MyStruct>>) -> &String {
    let obj = Arc::clone(obj);

    let data = obj.read().unwrap(); //data is created
    data.get_data()
} //data dropped here, thus lives not long enough.

impl MyStruct {
    fn get_data(&self) -> &String {
        self.data.get_val()
    }
}

impl MyData {
    fn get_val(&self) -> &String {
        &self.val
    }
}

(playground)

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Xu Chen
  • 407
  • 4
  • 10
  • Are you looking for a way to do this generically for lots of fields, or just a single field? And do you actually want a string, or is this just an example? You could for instance make specific wrapper types, like https://play.rust-lang.org/?gist=c5d6f121227890dbdc6f7ea1d97883b3&version=stable&mode=debug&edition=2015 but you can't return a simple reference. – loganfsmyth Jul 05 '18 at 05:50
  • @loganfsmyth Thanks for your snippet! Seems to be a much better way of passing reference around. Of course, I would not be able to elaborate on the whole context here, but to answer your question: 1) I do have some other fields that are protected by the struct. 2) And the String is just an example, it is arbitrary. – Xu Chen Jul 05 '18 at 06:58
  • @loganfsmyth I might not want to delegate the locking to the top level, like in your `read` function, so if I get the lock somewhere down the function call tree, I would want to pass the "reference" up, and releases the lock (the program has explicit synchronization that guarantees data race free). – Xu Chen Jul 05 '18 at 07:06

1 Answers1

4

There are 2 issues here:

  1. Lifetime
  2. Inner Reference

Let's tackle them in order.

Lifetime

First of all, there is a lifetime issue inherited from let obj = Arc::clone(obj);.

You are anchoring all lifetime references to a variable, instead of simply directly reading from the parameter.

You can NEVER return a reference to a local variable; so let's not. Simply removing this line yields:

fn get_data_ref(obj: &Arc<RwLock<MyStruct>>) -> &String {
    let data = obj.read().unwrap();
    data.get_data()
}

Which is already better.

Inner Reference

The second issue is the inner reference. RwLock.read() returns a guard, which will unlock in its destructor. In return, the guard only returns references which live as long as it lives, thus ensuring that no access occurs after the guard is destroyed (and the lock unlocked).

There is, unfortunately, no way to go from RwLockReadGuard<'a, MyStruct> to some kind of RwLockReadGuardInner<'a, String> which would magically grant you access to a field of MyStruct.

It's possible in theory, but no support has been implemented for it.


If you wanted to make it work...

The idea of RwLockReadGuardInner<'a, String> is essentially:

struct RwLockReadGuardInner<'a, T> {
    lock: *const sys::RWLock,
    poison: *const poison::Flag,
    data: &'a T,
}

impl<'a, T> Drop for RwLockReadGuardInner<'a, T> {
    //  magic happens
}

Then, you'd implement a map operation on RwLockReadGuard:

impl<'a, T> RwLockReadGuard<'a, T> {
    fn map<F, U>(self, f: F) -> RwLockReadGuardInner<'a, U>
    where
        F: FnOnce(&T) -> &U,
    {
        let result = RwLockReadGuardInner {
            lock: &self.__lock.inner as *const _,
            poison: &self.__lock.poison as *const _,
            data: f(unsafe { &*self.__lock.data.get() }),
        };

        std::mem::forget(self);

        result
    }
}

I find it interesting to note that slightly changing the definition of RwLock (packing inner and poison in a single struct) would allow changing RwLockReadGuard to both reference the main field and any inner field (at the cost of doubling its size).

If you want to, you could make a RFC to support this case. RwLockReadGuard is not the only one for which this may be useful either, most guards for interior mutability would benefit from such a map operation.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • thanks for pointing out the Lifetime issue. In order to implement the Drop trait properly for RwLockReadGuardInner, I assume I would also need to 1) call the `destroy` on the _lock.inner, 2) deallocate the poison explicitly? – Xu Chen Jul 07 '18 at 12:30
  • @XuChen: You would essentially need to replicate all the work done in `RwLockReadGuard`'s `Drop` implementation; I've checked exactly what that meant, but what you propose seems sensible. – Matthieu M. Jul 07 '18 at 13:20