Background:
- My goal is to start with a
std::sync::Weak<RwLock<T>>
and get an equivalent ofRwLock{Read,Write}Guard<T>
, without creating any intermediate objects that must be borrowed by the calling code. - I use
ouroboros
. It had a soundness issue #88 which is now fixed, but the fix removed support for type parameters. - My particular use case is not really about borrowing from data owned by the structure itself, but rather a
std::sync::Weak
that we need to keep alive. - I tried using
yoke
instead, but that doesn't work because it insists that mutable access should be through a'static
function, which is highly constraining.
It occurred to me that because my application only really needs to "strong-ify" the Weak
— not eliminate all borrowing — it would be possible to write more specific unsafe
code that has a simpler argument for soundness, and that's what I'm asking about here.
In particular, instead of synthesizing a lifetime from nowhere, and instead of using 'static
anywhere, we can use the actual pointer that Weak
makes available — and separately ensure that the pointer stays valid, as described in Weak::as_ptr()
's documentation (and example!). Therefore, the unsafely-produced reference lifetime matches the pointer provenance, and we separately make sure that the reference's referent stays valid (not dropped) as long as it is used.
Then, during drop glue execution, guard
(and the reference) is dropped before strong
, so the reference (in the guard) will always have been dropped before the referent (T
) is possibly dropped. It's still technically executing drop glue for a now-invalid type, but it can never call any function with an invalid lifetime parameter.
So, is this sound? Miri accepts the basic test I wrote, but I'm not sure which directions to push it harder.
#![allow(clippy::result_unit_err)]
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
pub fn read_from_weak<'a, T>(weak: &'a Weak<RwLock<T>>) -> Result<ArcReadGuard<'a, T>, ()> {
let strong = weak.upgrade().ok_or(())?;
// Per `as_ptr()` documentation, since the `upgrade()` succeeded, `ptr` is valid.
let ptr: *const RwLock<T> = weak.as_ptr();
// SAFETY:
// * `ptr` is valid now as per above
// * `ptr` will remain valid until we drop `strong`
// * we will not drop `strong` until after we drop `reference`
// * we will not let `reference` escape to be copied and potentially outlive `strong`
let reference: &'a RwLock<T> = unsafe { &*ptr };
let guard = reference.read().map_err(|_| ())?;
Ok(ArcReadGuard { guard, strong })
}
pub struct ArcReadGuard<'a, T> {
// SAFETY: `guard` must be dropped before `strong`.
guard: RwLockReadGuard<'a, T>,
#[allow(dead_code)] // used for drop effect
strong: Arc<RwLock<T>>,
}
impl<T> std::ops::Deref for ArcReadGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.guard
}
}
pub fn write_from_weak<'a, T>(weak: &'a Weak<RwLock<T>>) -> Result<ArcWriteGuard<'a, T>, ()> {
let strong = weak.upgrade().ok_or(())?;
// Per `as_ptr()` documentation, since the `upgrade()` succeeded, `ptr` is valid.
let ptr: *const RwLock<T> = weak.as_ptr();
// SAFETY:
// * `ptr` is valid now as per above
// * `ptr` will remain valid until we drop `strong`
// * we will not drop `strong` until after we drop `reference`
// * we will not let `reference` escape to be copied and potentially outlive `strong`
let reference: &'a RwLock<T> = unsafe { &*ptr };
let guard = reference.write().map_err(|_| ())?;
Ok(ArcWriteGuard { guard, strong })
}
pub struct ArcWriteGuard<'a, T> {
// SAFETY: `guard` must be dropped before `strong`.
guard: RwLockWriteGuard<'a, T>,
#[allow(dead_code)] // used for drop effect
strong: Arc<RwLock<T>>,
}
impl<T> std::ops::Deref for ArcWriteGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.guard
}
}
impl<T> std::ops::DerefMut for ArcWriteGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.guard
}
}
#[test]
fn test1() {
let thing = Arc::new(RwLock::new(String::from("hello")));
let weak = Arc::downgrade(&thing);
write_from_weak(&weak).unwrap().push_str(" world");
println!("{}", &*read_from_weak(&weak).unwrap());
let mut guard = write_from_weak(&weak).unwrap();
drop(thing);
guard.push('!');
drop(guard);
}