Cell in Rust

George Shuklin
journey to rust
Published in
4 min readAug 1, 2022

Rehearsal of the things I learned. If you are learning yourself, I advise you to go for the original: https://www.youtube.com/watch?v=8O0Nt9qY_vo

There is a very tight knot in Rust around Cell, RefCell and Deref trait.

After the excellent video by Jon Gjengset (see italic on top) I got some key insights, which I’ll try to rehearse slowly. This is part 1, about the Cell type.

Cell

Cell is a type which holds another type without an overhead. It’s just a type wrapper for some types T; internally (at byte level) it consists of T and only T. The reason for such wrapper existence is:

  1. It gives the magical power to change value of T via shared (non-mut) reference to the value of Cell<T> .
  2. In exchange it takes away some abilities of T.
  3. And it also required that T: Copy (can be copied cheaply) to get copy of the value back without destroying Cell.
  4. It also does not work with threads (no Sync, no Send traits)

So, what the magic?

Too many generics is hard for mortals, so I monomorphise it into some pesky u128 as a showcase. It is applicable to any small type which is suitable for copy between CPU registers. ‘CPU registers’ is not a hard requirement, but it gives the scale of ‘copied cheaply’ idea.

So, we have:

let x: u128 = 0xDEADBEAFDEADBEAF;
let cell_x: Cell<u128> = Cell::new(x);
let cell_ref = &cell_x;

Now we have cell_ref, which is a shared/immutable reference to the value in a cell_x variable which, in turn, contains the value from immutable variable x. Variable x is uninitialized after second line, because it ‘passed’ its value to Cell::new() and has nothing to hold.

Now, look at the magic:

cell_ref.set(cell_ref.get()/0xBEAF);
println!("0x{:x?}", cell_ref.get());

(permalink to playground)

It compiles. It prints a reasonable result (0x12af4633346e8). We got zero mutable variables in our program and we got a shared reference (&cell_x) which is absolutely does not allow modifications to a value it references to. This is the core Rust idea. Shared values are immutable! WTF?

Inner mutability

This is ‘the magic’ Cell gives us. It’s called ‘inner mutability’. We can not modify Cell (because it’s behind a shared reference), but we can change something ‘inside’ of it. Even if on byte-level Cell<u128> occupy the same 128 bits as u128, there is a magical type layer which allow us to do this.

We can (with a big help from Cell type) to change value inside of Cell value without changing the Cell value. Sounds crazy. Let’s rephrase:

Cell value (Cell itself) can not be modified via shared reference to it. But it can allow changes to the inner value (not the value of Cell itself!). If you subtract (bit-wise) Cell::new(0xDEADBEAFDEADBEAF) from 0xDEADBEAFDEADBEAF, there’ll be nothing. But! This ‘nothing’ has meaning to Rust. And that nothing is allowing to formally satisfy borrowing rules of Rust.

Sounds like $10000-per-hour-lawyer trick to me.

At any moment, anyone, having shared reference to value of Cell type can change value of the type which is inside of the Cell.

Now, about those $10000-per-hour services you’ve used…

The price for this magic is that you never can get a reference to value inside T. .get returns you a copy of the value. You can take reference of that copy, but you can’t do it to the original value. The value is hidden. It can be extracted (by destroying Cell), copied, replaced, but you can’t have a reference to Cell.T. &Cell is the reference to the Cell, there is no magic for &Cell (… Deref magic), there is no ‘as_ref’, there is no ‘as_mut’, nope, nope, nope.

I won’t repeat all explanations by Jon Gjengset on how it works and why it’s sound, but the basic idea is that if you forbid having references to T, you are free to modify it, because there are no pointers to break, and because you are in single-threaded mode (no Sync, no Send), every modification happens in between accesses via .get() methods and never in parallel.

This contract (no references to my client according to cease and desist letter from my compiler) is given for exchange to permission to modify (my client’s) internals at any time.

And the main magic here is not that we’ve agreed on that, but the compiler guarantee. Compiler does not allow to break the contract.

The Cell construct this contract by not having Deref trait and having all fields private. You own Cell, cell owns Value, but you can’t make reference to the Value.

Conclusion

By wrapping your value to Cell…

you get:

  • Ability to change it via shared reference.

you keep:

  • Ability to extract value ownership back from Cell.
  • Ability to get copy of the value from &Cell.

you loose:

  • Ability to have reference to your value.
  • Threads (including opportunistic parallelism via job stealing from many libraries) — but only for Cell’ed values.

Also: this is the reason why many people don’t like to call ‘&’ an immutable reference, but calling it ‘shared’, because, there are many cases when ‘&’ and ‘.set()’ work together, like this one.

--

--

George Shuklin
journey to rust

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.