Deep dive into Rust interior mutability — How Cell works?

Kevin Carvalho
3 min readJun 24, 2024

--

In this article i cover one of the Rust's interior mutability types, Cell.

What's interior mutability.

When reading the description of the std::cell module, where the Cell type resides, we get something that sounds a little bit strange at the first glance:

Shareable mutable containers.

What? In rust we know from the rules of referencing an borrowing that there's immutable or shared references and mutable or exclusive ones.

If we have a shared reference, we can have as many of them as we want. That's because those references don't allow us to mutate the value they point to, so it's fine to have several references at the same time.

The same is not true for exclusive references, as the name implies, those allow us mutate the value pointed by them. So it's not ok in this case to have more than one reference to the value. Think for example of 2 threads, each one of them holding an exclusive reference and changing the value at the same time. What should be the value after the threads run? Exactly, it's an undefined behavior!!

Ok, so why rust allows us to have shareable mutable containers? Doesn't it breaks the rust's borrowing rules? Well ... No because these containers have restrictions that allow them to be used in a safe way while still providing an api for allowing mutation. That's why those types provide “interior mutability”, when using them, they serve as containers that impose restrictions under which the types they hold can be safely modified!!

How Cell provides interior mutability?

So, how exactly does Cell provide interior mutability?

Basically, Cell does that by making sure there are no pointers to the data it holds and that it is executed in a single threaded environment.

With those restrictions, it's totally fine to change the data inside Cell. Think of it for a moment. If we know there are no pointers to the data inside Cell and it is not shared across threads, it's guaranteed that we have exclusive access to it.

Now the question is, how does Cell impose those constraints? Cell does this by never returning a reference to the data inside of it. It always returns a Copy of the data. So that already tells Cell is suited for cheap values that can be copied with small overhead, like integers for example.

Furthermore, Cell does not implement Sync, so it cannot be shared between thread boundaries.

The building block of Cell is UnsafeCell, that's one of the Rust's building blocks for interior mutability. UnsafeCell allows us to get a raw exclusive pointer to the data it holds at any time. That of course is an unsafe operation, so we must do that in an unsafe { } block.

The trick here is that, we can leverage UnsafeCell to implement Cell, under the constraints that we mentioned. A possible, simplified implementation of Cell would be:

use std::cell::UnsafeCell;

struct Cell<T> {
value: UnsafeCell<T>
}

// Disallow Cell to be used across thread boundaries
impl<T> !Sync for Cell<T> {}

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

pub fn set(self, value: T) {
// override the value cell poins to with a new one.
unsafe { *self.value.get() = value }
}

pub fn get(&self) -> T where T: Copy {
// return a copy of the data pointed by Cell
unsafe { *self.value.get() }
}
}

Here we see we're using UnsafeCell to store the data of Cell. We also disallow this type do be shared between threads and finally, we never give a reference to the data inside the Cell. Notice that, the get method works only with types that implement Copy and we return a copy of the internal type.

Conclusion

In this article we've explored the Rust's Cell type, we've learned that Cell allows interior mutability by imposing constraints on the data it holds. Furthermore, we came to the conclusion that Cell is suited for single threaded environments where the data is cheap to copy.

--

--