OK, it took me some time, but I believe I may have something that would work and be safe without using closures (save for the visit/map
method of Sc
).
The solution I found involves three objects, not two like the previous solution.
The first object is Sc
, and it is completely unchanged:
pub struct Sc<T>(Cell<Option<*const T>>);
The second object is our dropper, which is simpler than ever:
struct Dropper<'sc, T: 'sc> {
sc: &'sc Sc<T>,
}
impl<'sc, T> Drop for Dropper<'sc, T> {
fn drop(&mut self) {
self.sc.0.set(None)
}
}
(Dropper
is not public anymore in this design)
Then, I introduce a third object that I call Locker
:
pub struct Locker<'sc, 'auto, T: 'sc + 'auto> {
sc: Option<Dropper<'sc, T>>,
marker: Marker,
autoref: Option<&'auto Marker>,
}
This object is self-referential, Locker::autoref
will eventually reference Locker::marker
.
Then, I replace the Sc::set
method with the following Locker::lock
method:
impl<'sc, 'auto, T> Locker<'sc, 'auto, T> {
pub fn new() -> Self {
Self {
sc: None,
marker: Marker,
autoref: None,
}
}
pub fn lock(&'auto mut self, val: &'auto T, sc: &'sc Sc<T>) {
let ptr = val as *const T;
sc.0.set(Some(ptr));
self.sc = Some(Dropper { sc });
self.autoref = Some(&self.marker);
}
}
Because Locker
becomes a self-borrowing struct after a call to Locker::lock
, it is not possible to call mem::forget()
on it (it is borrowed).
(for clarity, here is Sc
's full impl block):
impl<T> Sc<T> {
pub fn new() -> Self {
Self { 0: Cell::new(None) }
}
unsafe fn get(&self) -> Option<&T> {
self.0.get().map(|ptr| mem::transmute(ptr))
}
pub fn map<U, F: Fn(&T) -> U>(&self, f: F) -> Option<U> {
unsafe { self.get().map(f) }
}
pub fn is_none(&self) -> bool {
unsafe { self.get().is_none() }
}
}
You can then use Locker
in the following way:
fn it_works() {
let sc = Sc::new();
assert!(sc.is_none());
{
let s = String::from("foo");
{
let mut locker = Locker::new();
locker.lock(&s, &sc);
// cannot mem::forget(locker) here, because locker borrows itself
// cannot mem::forget(s) either, because it is borrowed by locker
assert!(!sc.is_none());
}
assert!(sc.is_none());
{
let mut locker = Locker::new();
locker.lock(&s, &sc);
assert!(!sc.is_none());
}
assert!(sc.is_none());
}
assert!(sc.is_none());
}
For convenience, I also provide a Wrapper
type:
pub struct Wrapper<'sc, 'auto, T: 'sc + 'auto> {
locker: Locker<'sc, 'auto, T>,
data: T,
}
impl<'sc, 'auto, T> Wrapper<'sc, 'auto, T> {
pub fn new(data: T) -> Self {
Self {
locker: Locker::new(),
data,
}
}
pub fn lock(this: &'auto mut Self, sc: &'sc Sc<T>) {
this.locker.lock(&this.data, sc);
}
}
// also impl Deref<Target=T> and DerefMut for Wrapper<T>
This enables the following:
#[test]
fn with_wrapper() {
let sc = Sc::new();
assert!(sc.is_none());
{
let mut s = Wrapper::new(String::from("bar"));
{
Wrapper::lock(&mut s, &sc);
assert!(!sc.is_none());
}
// actually here we are still locked because a wrapper automatically locks for its whole lifetime
assert!(!sc.is_none());
}
assert!(sc.is_none());
}
I pushed this as a branch on my repository (I only updated the Observer
example, because I broke Sc
with trait objects with this release).
What do you think?