Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A cell with interior mutability allowing arbitrary mutation actions

Standard Cell struct provides interior mutability but allows only a few mutation methods such as set(), swap() and replace(). All of these methods change the whole content of the Cell. However, sometimes more specific manipulations are needed, for example, to change only a part of data contained in the Cell.

So I tried to implement some kind of universal Cell, allowing arbitrary data manipulation. The manipulation is represented by user-defined closure that accepts a single argument - &mut reference to the interior data of the Cell, so the user itself can deside what to do with the Cell interior. The code below demonstrates the idea:

use std::cell::UnsafeCell;

struct MtCell<Data>{
    dcell: UnsafeCell<Data>,
}

impl<Data> MtCell<Data>{
    fn new(d: Data) -> MtCell<Data> {
        return MtCell{dcell: UnsafeCell::new(d)};
    }

    fn exec<F, RetType>(&self, func: F) -> RetType where
        RetType: Copy,
        F: Fn(&mut Data) -> RetType 
    {
        let p = self.dcell.get();
        let pd: &mut Data;
        unsafe{ pd = &mut *p; }
        return func(pd);
    }
}

// test:

type MyCell = MtCell<usize>;

fn main(){
    let c: MyCell = MyCell::new(5);
    println!("initial state: {}", c.exec(|pd| {return *pd;}));
    println!("state changed to {}", c.exec(|pd| {
        *pd += 10; // modify the interior "in place"
       return *pd;
    }));
}

However, I have some concerns regarding the code.

  1. Is it safe, i.e can some safe but malicious closure break Rust mutability/borrowing/lifetime rules by using this "universal" cell? I consider it safe since lifetime of the interior reference parameter prohibits its exposition beyond the closure call time. But I still have doubts (I'm new to Rust).

  2. Maybe I'm re-inventing the wheel and there exist some templates or techniques solving the problem?

Note: I posted the question here (not on code review) as it seems more related to the language rather than code itself (which represents just a concept).

[EDIT] I'd want zero cost abstraction without possibility of runtime failures, so RefCell is not perfect solution.

like image 588
user396672 Avatar asked Dec 19 '20 20:12

user396672


People also ask

What is interior mutability?

Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data; normally, this action is disallowed by the borrowing rules.

What is RefCell?

RefCell allows borrowing immutable or mutable reference to the contained value. It tracks the borrows at runtime, via borrow and borrow_mut . pub fn borrow(&self) -> Ref<T> pub fn borrow_mut(&self) -> RefMut<T> The method borrow grants temporary access to the contained value via immutable reference.


2 Answers

This is a very common pitfall for Rust beginners.

  1. Is it safe, i.e can some safe but malicious closure break Rust mutability/borrowing/lifetime rules by using this "universal" cell? I consider it safe since lifetime of the interior reference parameter prohibits its exposition beyond the closure call time. But I still have doubts (I'm new to Rust).

In a word, no.

Playground

fn main() {
    let mt_cell = MtCell::new(123i8);

    mt_cell.exec(|ref1: &mut i8| {
        mt_cell.exec(|ref2: &mut i8| {
            println!("Double mutable ref!: {:?} {:?}", ref1, ref2);
        })
    })
}

You're absolutely right that the reference cannot be used outside of the closure, but inside the closure, all bets are off! In fact, pretty much any operation (read or write) on the cell within the closure is undefined behavior (UB), and may cause corruption/crashes anywhere in your program.

  1. Maybe I'm re-inventing the wheel and there exist some templates or techniques solving the problem?

Using Cell is often not the best technique, but it's impossible to know what the best solution is without knowing more about the problem.

If you insist on Cell, there are safe ways to do this. The unstable (ie. beta) Cell::update() method is literally implemented with the following code (when T: Copy):

pub fn update<F>(&self, f: F) -> T
where
    F: FnOnce(T) -> T,
{
    let old = self.get();
    let new = f(old);
    self.set(new);
    new
}

Or you could use Cell::get_mut(), but I guess that defeats the whole purpose of Cell.

However, usually the best way to change only part of a Cell is by breaking it up into separate Cells. For example, instead of Cell<(i8, i8, i8)>, use (Cell<i8>, Cell<i8>, Cell<i8>).

Still, IMO, Cell is rarely the best solution. Interior mutability is a common design in C and many other languages, but it is somewhat more rare in Rust, at least via shared references and Cell, for a number of reasons (e.g. it's not Sync, and in general people don't expect interior mutability without &mut). Ask yourself why you are using Cell and if it is really impossible to reorganize your code to use normal &mut references.

IMO the bottom line is actually about safety: if no matter what you do, the compiler complains and it seems that you need to use unsafe, then I guarantee you that 99% of the time either:

  1. There's a safe (but possibly complex/unintuitive) way to do it, or
  2. It's actually undefined behavior (like in this case).

EDIT: Frxstrem's answer also has better info about when to use Cell/RefCell.

like image 67
Coder-256 Avatar answered Sep 27 '22 00:09

Coder-256


Your code is not safe, since you can call c.exec inside c.exec to get two mutable references to the cell contents, as demonstrated by this snippet containing only safe code:

let c: MyCell = MyCell::new(5);
c.exec(|n| {
    // need `RefCell` to access mutable reference from within `Fn` closure
    let n = RefCell::new(n);

    c.exec(|m| {
        let n = &mut *n.borrow_mut();
        
        // now `n` and `m` are mutable references to the same data, despite using
        // no unsafe code. this is BAD!
    })
})

In fact, this is exactly the reason why we have both Cell and RefCell:

  • Cell only allows you to get and set a value and does not allow you to get a mutable reference from an immutable one (thus avoiding the above issue), but it does not have any runtime cost.
  • RefCell allows you to get a mutable reference from an immutable one, but needs to perform checks at runtime to ensure that this is safe.

As far as I know, there's not really any safe way around this, so you need to make a choice in your code between no runtime cost but less flexibility, and more flexibility but with a small runtime cost.

like image 35
Frxstrem Avatar answered Sep 23 '22 00:09

Frxstrem