Is it possible to create a type with interior mutability without using `UnsafeCell`?

For example, is the following implementation of Mutex (playground link) sound? I use raw pointers for changing the value, and Miri doesn’t complain about it.

use std::marker::PhantomData;
use std::ptr::NonNull;
use std::sync::Mutex;
use std::thread;

pub struct MyMutex<T>
where
    T: ?Sized,
{
    mutex: Mutex<()>,
    value: NonNull<T>,
    _invariant_for_t: PhantomData<*mut T>,
}

impl<T> MyMutex<T>
where
    T: ?Sized,
{
    pub fn new(value: T) -> Self
    where
        T: Sized,
    {
        Self {
            mutex: Mutex::new(()),
            value: NonNull::new(Box::into_raw(Box::new(value))).unwrap(),
            _invariant_for_t: PhantomData,
        }
    }

    pub fn with_mut<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
        let _guard = self.mutex.lock().unwrap();

        f(unsafe { &mut *self.value.as_ptr() })
    }
}

impl<T> Drop for MyMutex<T>
where
    T: ?Sized,
{
    fn drop(&mut self) {
        drop(unsafe { Box::from_raw(self.value.as_ptr()) });
    }
}

unsafe impl<T> Send for MyMutex<T> where T: Send + ?Sized {}
unsafe impl<T> Sync for MyMutex<T> where T: Send + ?Sized {}

fn main() {
    let my_mutex = MyMutex::new(0);

    thread::scope(|scope| {
        for _ in 0..100 {
            scope.spawn(|| {
                for _ in 0..100 {
                    my_mutex.with_mut(|value| *value += 1);
                }
            });
        }
    });

    assert_eq!(my_mutex.with_mut(|value| *value), 10000);
}

You have to use UnsafeCell if and only if the data is to be stored inline, so your use of raw pointers is indeed another way. One can look at it as approaching the same result, “not breaking immutability of &T”, from opposite directions:

  • With struct MyStruct<T>(UnsafeCell<T>), you opt out of &MyStruct<T> automatically implying the effective existence of &T and its immutability property.
  • With struct MyStruct<T>(*mut T), Rust makes zero assumptions[1] about the relationship of MyStruct to T, so you get to choose whether you want to offer interior mutability, unguarded &T, or something else.

One can also look at FFI/OS wrappers like std::fs::File as being a kind of interior mutability, if you are considering the API (“I can store and retrieve data through a &File”) and not the implementation. Some would say that it’s only interior mutability if memory is being mutated, and some (such as the reference?) would say that it’s only interior mutability if it’s the kind of case where UnsafeCell must be used. But if those narrow definitions are used, we surely need a name for the API concept too.


  1. other than auto traits and variance ↩︎

5 Likes