Why is my zero-cost alternative to RefCell not the standard way of achieving interior mutability?
Asked Answered
K

1

6

I've been thinking about why interior mutability in Rust in most cases requires runtime checks (e.g. RefCell). It looks like I've found a safe alternative without a runtime cost. I've called the type SafeCell (mainly because it is a safe wrapper around UnsafeCell), and it allows you to apply any function to the wrapped value without the risk of having the reference escape:

struct SafeCell<T> {
    inner: UnsafeCell<T>,
}

impl<T> SafeCell<T> {
    pub fn new(value: T) -> Self {
        Self {
            inner: UnsafeCell::new(value),
        }
    }

    pub fn apply<R, F>(&self, fun: F) -> R
    where
        F: FnOnce(&mut T) -> R,
    {
        // Reference below has a lifetime of the current scope, so if
        // user tries to save it somewhere, borrow checker will catch this.
        let reference: &mut T = unsafe { &mut *self.inner.get() };
        fun(reference)
    }
}

This type can be used for interior mutability like this:

pub struct MySet {
    set: HashSet<i32>,
    unique_lookups: SafeCell<HashSet<i32>>,
}

impl MySet {
    pub fn contains(&self, value: i32) -> bool {
        self.unique_lookups.apply(|lookups| lookups.insert(value));
        self.set.contains(value)
    }

    pub fn unique_lookups_count(&self) -> usize {
        self.unique_lookups.apply(|lookups| lookups.len())
    }
}

Or in conjunction with Rc:

fn foo(rc: Rc<SafeCell<String>>) {
    rc.apply(|string| {
        if string.starts_with("hello") {
            string.push_str(", world!")
        }
        println!("{}", string);
    });
}

Playground

  1. Are there any safety/soundness issues with this type?
  2. If not, why is a type like this not a standard way of achieving interior mutability? It looks like it is as usable as RefCell while providing static lifetime checks as opposed to runtime checks.
Katinakatine answered 11/4, 2020 at 15:47 Comment(0)
F
15

There is nothing in your API stopping a user from calling apply again in the closure provided to apply. This allows there to be multiple simultaneous mutable references to the same data, which is undefined behavior.

let x = SafeCell::new(0);
x.apply(|y| {
    x.apply(|z| {
        // `y` and `z` are now both mutable references to the same data
        // UB!
        *y = 1;
        *z = 2;
    })
});
x.apply(|y| println!("x: {}", y));

(playground)

Miri correctly calls this out when it sees the second mutable reference being made.

error: Undefined Behavior: not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
  --> src/main.rs:20:42
   |
20 |         let reference: &mut T = unsafe { &mut *self.inner.get() };
   |                                          ^^^^^^^^^^^^^^^^^^^^^^ not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
   |
Forme answered 11/4, 2020 at 17:55 Comment(1)
For an example of a data race using this idea, check out this.Forme

© 2022 - 2024 — McMap. All rights reserved.