Interior Mutability in Rust
In this article, I will talk about what Interior mutability in Rust is and where its needed with some practical examples and how it can be implemented.
Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data. This sounds a bit counter-intuititve to the whole Rust ownership and borrow philosophy. But there are practical use cases where such an implementation is required and we will explore them first.
Implementing a cache which can be shared between multiple parts of the same application.
Lets say, we have simple cache which is implemented as a Hashmap between two strings and we want this to be shared between multiple parts of the same application. It can be implemented as follows.
// A cache that we want to share between multiple owners
struct Cache {
data: HashMap<String, String>,
}
impl Cache {
fn new() -> Self {
Cache {
data: HashMap::new(),
}
}
fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
// This needs &mut self to modify the HashMap
fn insert(&mut self, key: String, value: String) {
self.data.insert(key, value);
}
}
Now lets implement a user of this Cache in a simple main() function. For simplicity sake, we will create the multiple users in the main directly. In Rust, if we want make a data structure of type T a shared data structure, we can start with an Rc<T> which is a reference counting smart pointer. This allows use to make clones of the reference to our Cache. So a possible implementation would look as follows.
fn main() {
// We want multiple owners of the same cache
let cache = Rc::new(Cache::new());
let cache_clone1 = Rc::clone(&cache);
let cache_clone2 = Rc::clone(&cache);
// ERROR: Cannot borrow as mutable through Rc!
// Rc only gives us shared references (&T), never mutable ones (&mut T)
cache_clone1.insert("key1".to_string(), "value1".to_string());
// Even this won't work:
let mut cache = Rc::new(Cache::new());
cache.insert(...); // Still can't mutate through Rc
println!("Problem: We have multiple owners (Rc) but need to mutate the cache!");
println!("Rc<T> only provides shared access, never mutable access.");
}
Here we have two clones which are immutable references to our cache, but we can’t modify the cache using the insert method as the clones are not mutable. Rust’s borrowing rules prevent getting &mut T when multiple owners exist. This is a fundamental conflict: multiple ownership vs. mutation.
Here is the error that the compiler will give if you compile the above code.
error[E0596]: cannot borrow data in an `Rc` as mutable
--> src/CacheWithRc/cache_with_Rc.rs:34:5
|
34 | cache_clone1.insert("key1".to_string(), "value1".to_string());
| ^^^^^^^^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Cache>`
So, we need a way to modify the internal contents of a data structure(the hashmap inside the cache) even though the external container(the reference to cache) is immutable.
Implementing Mock objects for testing.
This is an example from The Rust Programming Language — The Rust Programming Language in the section about Interior mutablity. During testing we often need a test double, which is a different type than the original type. The internal implementation of these test doubles will enable better testability. We implement these test doubles as Mock Objects.
In the below example, let’s say we have a library that is being implemented to support a limit tracking functionality where if the usage crosses certain limits a message(could be a warning, error etc) is to be generated. The implementation of the library expects the user to provide the mechanism for sending/consuming the messages. The library doesn’t need to know that detail. All it needs is something that implements a trait we’ll provide called Messenger.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
This Messenger trait has one method called send that takes an immutable reference to self and the text of the message as arguments. This trait is the interface our mock object needs to implement so that the mock can be used in the same way a real object is.
The other important part is that we want to test the behavior of the set_value method on the LimitTracker.
The simplest way to do this would be to create a MockMessenger structure and have these messages stored for any test validation purposes and implement the Messenger trait for this structure.
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
Now we can implement a test case which validates that the library API set_value generates a specific message when its called with certain value.
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
assert_eq!(mock_messenger.sent_messages[0], "Warning: You've used up over 75% of your quota!");
}
But, if you observe carefully, the send method implemented for our MockMessenger doesn’t compile due to the following error.
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference --> src/MockWithOutRefCell/mock_without_refCell.rs:58:13 | 58 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable |
Here we have an immutable reference to self (MockMessenger) which is passed by the test case and Rust compiler doesn’t allow us to modify the contents inside this MockMessenger object. Here again we see an use case where we want a mutable access to some data inside a container for which we have a immutable reference.
Now, lets quickly look at what Interior mutability is and how it helps us to address both these use cases.
In Rust std library we already have a construct called RefCell<T> which implements this Interior mutability pattern. With references and Box<T>, the borrowing rules’ invariants are enforced at compile time. With RefCell<T>, these invariants are enforced at runtime and your program will panic and exit if you break these rules. RefCell<T> provides two methods to access T. These methods apply the borrow checking rules at runtime and the program might panic if you violate them.
borrow() -> returns the smart pointer type Ref<T> borrow_mut() -> returns the smart pointer type RefMut<T>
Implementing a Cache that is shared between multiple parts of the same program. We need to wrap the cache inside a RefCell<T> and then use an Rc<T>. Then we can clone this immutable reference multiple times and thereby have multiple parts access the same cache.
// RefCell provides interior mutability!
// Rc provides shared ownership, RefCell allows mutation through shared reference
let cache = Rc::new(RefCell::new(Cache::new()));
let cache_clone1 = Rc::clone(&cache);
let cache_clone2 = Rc::clone(&cache);
Then we can use borrow_mut() when we want to call the insert() method and borrow() when we want call the get() method as shown below.
// borrow_mut() gives us a mutable reference at runtime
cache_clone1.borrow_mut().insert("user:1".to_string(), "Alice".to_string());
cache_clone2.borrow_mut().insert("user:2".to_string(), "Bob".to_string());
// borrow() gives us a shared reference at runtime
if let Some(name) = cache.borrow().get("user:1") {
println!("Found user:1 = {}", name);
}
Please note that we cannot have multiple mutable references at any time or have an immutable reference when there is already a mutable reference. The borrowing rules remains the same, but these are applied at runtime and the following code will panic.
//Attempt to have two mutable borrow at the same time.
let mut cache_clone1_mut_borrow = cache_clone1.borrow_mut();
cache_clone1_mut_borrow.insert("user:1".to_string(), "Alice".to_string());
//Panic in the below line as already a mutable borrow is active
cache_clone2.borrow_mut().insert("user:2".to_string(), "Bob".to_string());
//Attempt to have an immutable borrow when a mutable borrow already exists.
let mut cache_clone1_mut_borrow = cache_clone1.borrow_mut();
cache_clone1_mut_borrow.insert("user:1".to_string(), "Alice".to_string());
//The below line will Panic as we are trying to take an immutable borrow
//when a mutable borrow is still active
let cache_clone1_borrow = cache_clone1.borrow();
Implementing the Mock Object for testing
We need to store the sent_messages vector as RefCell<T> inside the MockMessengeer, so that we can get a mutable reference to it from the send() method.
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
...
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
Internals of Interior mutability
Now lets briefly explore on how a RefCell<T> is implemented to bypass the Rust’s compile time borrow checking rules to get the desired behavior.
Rust std library provides a core primitive called UnsafeCell<T> which enables the implementation of the Interior Mutability pattern. It’s a language feature that tells the compiler: “mutation through &T is possible here”. It provides .get() method that returns *mut T (raw mutable pointer) from &self.
The below code will print 100 as we are able to modify the value pointed to by the reference cell.
let cell = UnsafeCell::new(42);
// UnsafeCell::get() takes &self but returns *mut T (raw mutable pointer)
// This is the ONLY safe way to get interior mutability
unsafe {
let ptr = cell.get();
*ptr = 100; // Mutate through shared reference!
println!("Value after mutation: {}", *ptr);
}
The implementations which provide the Interior Mutability pattern needs to use UnsafeCell<T> internally, have some code under the unsafe blocks. But they provide a safe API to the callers.
A simple way to implement our own SimpleRefCell<T> would be as follows. It stores the value of type T in an UnsafeCell and also maintains a simple state to enforce the borrow checking principles at runtime.
use std::cell::UnsafeCell;
// Simplified version of RefCell to understand the internals
// The real RefCell is more complex but follows this pattern
#[derive(Copy, Clone)]
enum BorrowFlag {
Unused, // No borrows
Reading(usize), // N shared borrows (count of readers)
Writing, // 1 exclusive borrow
}
pub struct SimpleRefCell<T> {
// UnsafeCell is the ONLY primitive that allows interior mutability
// It's the magic ingredient that opts out of compile-time aliasing rules
value: UnsafeCell<T>,
// Runtime borrow tracking - this is the key!
borrow_flag: UnsafeCell<BorrowFlag>,
}
// Key insight: RefCell itself doesn't need to be mut to provide mut access
impl<T> SimpleRefCell<T> {
pub fn new(value: T) -> Self {
SimpleRefCell {
value: UnsafeCell::new(value),
borrow_flag: UnsafeCell::new(BorrowFlag::Unused),
}
}
// Takes &self (shared reference) but returns mutable access!
pub fn borrow_mut(&self) -> Result<&mut T, &'static str> {
unsafe {
let flag = &mut *self.borrow_flag.get();
// Runtime check: ensure no other borrows exist
match *flag {
BorrowFlag::Unused => {
// Safe to give mutable access
*flag = BorrowFlag::Writing;
Ok(&mut *self.value.get())
}
_ => {
// Already borrowed! Panic in real RefCell
Err("Already borrowed!")
}
}
}
}
pub fn borrow(&self) -> Result<&T, &'static str> {
unsafe {
let flag = &mut *self.borrow_flag.get();
match *flag {
BorrowFlag::Unused => {
*flag = BorrowFlag::Reading(1);
Ok(&*self.value.get())
}
BorrowFlag::Reading(n) => {
// Multiple readers are OK
*flag = BorrowFlag::Reading(n + 1);
Ok(&*self.value.get())
}
BorrowFlag::Writing => {
Err("Already mutably borrowed!")
}
}
}
}
}
Conclusion
Interior mutability allows you to mutate the interiors (contents) of a data structure even when the exterior (container) is immutable. This is achieved by moving borrow checking from compile-time to runtime.
RefCell<T> is one implementation that provides this pattern.
Safety
RefCell is safe because it enforces Rust’s borrowing rules at runtime. Panics if borrowing rules are violated (better than undefined behavior) Use try_borrow() and try_borrow_mut() to avoid panic, but needs error handling by the user. Only works in single-threaded contexts (not Send or Sync)
Performance
Small runtime overhead for borrow checking as it checks current borrow state on each borrow() or borrow_mut(). Generally negligible unless in very hot code paths
Alternatives
For multi-threading: Use Arc<Mutex<T>> or Arc<RwLock<T>>. For simple cases: Consider restructuring to avoid interior mutability For performance-critical code: Unsafe code with proper synchronization
Here’s a text-based diagram summarizing the common Rust patterns involving interior mutability and shared ownership:
+------------------+ +------------------+ +-----------------------+
| Immutable | | Shared Ownership | | Interior Mutability |
| Ownership | | (Single-threaded)| | (Runtime borrow check)|
+------------------+ +------------------+ +-----------------------+
| | |
v v v
let x = T; let x = Rc<T>; let x = Rc<RefCell<T>>;
| | |
| | |
| | |
| | |
v v v
No mutation No mutation ✅ Mutation allowed
after borrow after borrow ❗ Panics on rule violation
(e.g., double mutable borrow)
+------------------+ +------------------+ +------------------+
| Multi-threaded | | Testing Patterns | | Unsafe Internals |
| Shared Ownership | | with Mutability | | of RefCell |
+------------------+ +------------------+ +------------------+
| | |
v v v
let x = Arc<Mutex<T>>; let mock = RefCell<Mock>; RefCell<T> uses UnsafeCell<T>
| | |
| | |
v v v
✅ Thread-safe ✅ Mutable access ✅ Safe abstraction
✅ Mutation allowed ✅ Useful for mocking ❗ UnsafeCell is unsafe
❗ Locking overhead ❗ Panics possible ❗ Use with care
Read the full article here: https://medium.com/nagarjuna-reddy/interior-mutability-in-rust-fad21a1f9ca8