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 Dropper
s, because they are struct
s with lifetimes, but is this true?
Thank you for the answers. I hope I’m not doing something too silly .