Return reference to data inside RwLock?

I am wanting to expose a struct (S) that wraps an Arc<RwLock<T>> and allow for getting a reference to T's private fields through methods on S. It doesn't look like this is possible through safe code but I was able to accomplish it using the code below.

struct Inner {
    a: String,
    _b: String,
}

pub struct MyValue(Arc<RwLock<Inner>>);

impl MyValue {
    pub fn new(a: String, _b: String) -> Self {
        Self(Arc::new(RwLock::new(Inner { a, _b })))
    }

    pub fn get_a(&self) -> ValueRef<'_, String> {
        let guard = self.0.read().expect("should not be poisoned");

        let mut value_ref = ValueRef {
            _guard: guard,
            value: NonNull::dangling(),
        };

        let value = NonNull::from(&value_ref._guard.a);
        value_ref.value = value;

        value_ref
    }
}

pub struct ValueRef<'a, T> {
    _guard: RwLockReadGuard<'a, Inner>,
    value: NonNull<T>,
}

impl<'a, T> Deref for ValueRef<'a, T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        unsafe { self.value.as_ref() }
    }
}
  • Can this result in undefined behavior? I ran it through miri without issues.
  • Do I need box and pin ValueRef?

Playground Link.

Rather than pulling the value out and unsafely storing it next to the guard, just store the guard in ValueRef and pull the value out of it in Deref. No unsafety required.

1 Like

That is true, but would require a struct for each field that I want to reference. I updated the question to better reflect my intention so ValueRef contains a NonNull to a generic T.

My Inner is actually significantly more complicated than the one from the example. Creating a struct for each field isn't ideal, even with macros.

This approach seems reasonable to me, but I’m not an expert on unsafe code.

I can think of a couple of safe alternatives, but they’re not perfect replacements:

  • Provide a method that returns the read guard wrapped inside a newtype, and let the user access Inner through methods on that object instead.
pub struct InnerHandle<'a>(RwLockReadGuard<'a, Inner>);

impl<'a> InnerHandle<'a> {
    fn get_a(&self)->&str { self.0.a.as_str() }
}
  • Depending on how expensive it is to get the value, you could store an accessor inside ValueRef instead of the result:
pub struct ValueRef<‘a, T, F:Fn(&Inner)->&T> {
    _guard: RwLockReadGuard<‘a, Inner>,
    accessor: F
}

parking_lot::RwLockReadGuard supports to map the reference it deref to.

1 Like

@Hyeonu this is really interesting and seems very similar to what I want to accomplish. Unfortunately, I was planning on using an async RwLock via async-std.

@2e71828 If I proceed with the ValueRef<'a, T>, do you know if this is a situation that would require returning a pinned ValueRef? Also, I appreciate the accessor suggestion; if it turns out I expose UB, this is very likely the solution I'll use.

This is unlikely to require pinning. A reference &’a T already ensures that the T will not be moved inside the region ’a. Because ValueRef carries the same lifetime annotation, the compiler won’t allow it to exist beyond the scope of this protection.

Before choose to use async lock, try read this article on tokio which explains why you should use sync lock even on async context in many cases.

2 Likes

Note also that the Tokio async rwlock can also be mapped with the RwLockReadGuard::map method. Interestingly, it is implemented in a very similar way to your ValueRef.

As for pinning, no it is not necessary. The lifetime ensures that the pointer is not invalidated in this case.

2 Likes