Hello, I've been inspired by this thread to write a "new" (as in "I don't know of prior art, but I didn't look that hard either and I'm fairly new to rust so may be missing the obvious") kind of reference type that can be stored in a struct without being parameterized by a lifetime and without allocating its referent (unlike Rc, which is shared ownership).
In short, the idea behind it is that when a Sc is set to point to an object, it returns an intermediate Dropper type that retain the lifetime of the reference, while the Sc itself contains a raw pointer to the object.
When the Dropper instance is dropped, it sets the pointer in Sc to null.
When the user of an Sc wants to query its reference, they call Sc::get() which returns an Option<&T> that is either a reference to the object if the pointer is not null (the dropper is still alive), or None if the pointer is null (the dropper is dead).
let sc = Sc::new();
assert_eq!(sc.get(), None); // Nothing in Sc, initially
{
let s = String::from("foo");
let _dropper = sc.set(&s); // set a reference to s in Sc
assert_eq!(sc.get(), Some(&s)); // access the reference
}
assert_eq!(sc.get(), None); // After s is dead, nothing in Sc again
I set up a small repository with some code for Sc, and a small example of use.
However, I know that using this type is unsafe
: if the user mem::forget the dropper instance, then the pointer inside Sc will never be invalidated as it should. Furthermore, the naive design with a public Sc::get() method allows code like the following:
let s_ref : &str;
let sc = Sc::new();
{
let s = String::from("foo");
let _dropper = sc.set(&s);
s_ref = sc.get()?;
}
println!("Happily prints a dead reference: {}!", s_ref);
For now, I tried to prevent this kind of misuse by replacing the Sc::get() method with a Sc::visit() method that accepts a closure taking a &T as parameter, and that calls the closure if reference is Sc is still alive (e.g. Dropper is still alive).
impl<T> for Sc<T> {
pub fn visit<'a, U, F: Fn(&'a T) -> U>(&'a self, f: F) -> Option<U> {
unsafe {
match self.get() {
Some(x) => Some(f(x)),
None => None,
}
}
}
}
So, I guess, my questions are:
1° Is there any way to force a reference returned by get() to be attached to the inner scope, in the example given above, or am I stuck with a visit() method as the sole means of enforcing this? Passing _dropper to get() is a no-go, because then the caller would have to keep _dropper and the Sc around, which kind of defeat the purpose (see the example for a sense of what that would be annoying).
2° Are there still ways to invoke UB even by having a visit() method rather than a get() method (other than by calling mem::forget(_dropper))?
3° Is there any way to design _dropper so that mem::forget(_dropper) isn't unsafe to call? If there isn't (which I fear), how serious are the consequences? I believe the user cannot easily create cycles containing Droppers, because they are structs with lifetimes, but is this true?
Thank you for the answers. I hope I'm not doing something too silly
.