Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interior mutability abuse in API design?

My background in C++ makes me uncomfortable about interior mutability. The code below is my investigation around this topic.

I agree that, from the borrow checker point of view, dealing with many references on every single struct which internal state could be altered soon or later is impossible; that's clearly where interior mutability can help.

Moreover, in chapter 15.5 "RefCell and the Interior Mutability Pattern" of The Rust Programming Language, the example about the Messenger trait and its implementation on the MockMessenger struct makes me think that it is a common API design to systematically prefer &self over &mut self even if its quite obvious that some kind mutability will be mandatory soon or later. How could an implementation of Messenger not alter its internal state when sending a message? The exception is just printing the message, which is consistent with &self, but the general case would probably consist in writing to some kind of inner stream, which could imply buffering, updating error flags... All of this certainly requires &mut self, as for example impl Write for File.

Relying on interior mutability to solve this problem sounds to me like, in C++, const_casting or abusing of mutable members just because elsewhere in the application we were not consistent about constness (common mistake for learners of C++).

So, back to my example code below, should I:

  • use &mut self (the compiler doesn't complain, even if it's not mandatory) from change_e() to change_i() in order to keep consistent with the fact that I alter the values of the stored integers?
  • keep using &self, because interior mutability allows it, even if I actually alter the values of the stored integers?

This decision is not only local to the struct itself but will have a large influence on what could be expressed in the application using this struct. The second solution will certainly help a lot, because only shared references are involved, but is it consistent with what is expected in Rust.

I cannot find an answer to this question in Rust API Guidelines. Is there any other Rust documentation similar to C++CoreGuidelines?

/*
    $ rustc int_mut.rs && ./int_mut
     initial:   1   2   3   4   5   6   7   8   9
    change_a:  11   2   3   4   5   6   7   8   9
    change_b:  11  22   3   4   5   6   7   8   9
    change_c:  11  22  33   4   5   6   7   8   9
    change_d:  11  22  33  44   5   6   7   8   9
    change_e:  11  22  33  44  55   6   7   8   9
    change_f:  11  22  33  44  55  66   7   8   9
    change_g:  11  22  33  44  55  66  77   8   9
    change_h:  11  22  33  44  55  66  77  88   9
    change_i:  11  22  33  44  55  66  77  88  99
*/

struct Thing {
    a: i32,
    b: std::boxed::Box<i32>,
    c: std::rc::Rc<i32>,
    d: std::sync::Arc<i32>,
    e: std::sync::Mutex<i32>,
    f: std::sync::RwLock<i32>,
    g: std::cell::UnsafeCell<i32>,
    h: std::cell::Cell<i32>,
    i: std::cell::RefCell<i32>,
}

impl Thing {
    fn new() -> Self {
        Self {
            a: 1,
            b: std::boxed::Box::new(2),
            c: std::rc::Rc::new(3),
            d: std::sync::Arc::new(4),
            e: std::sync::Mutex::new(5),
            f: std::sync::RwLock::new(6),
            g: std::cell::UnsafeCell::new(7),
            h: std::cell::Cell::new(8),
            i: std::cell::RefCell::new(9),
        }
    }

    fn show(&self) -> String // & is enough (read-only)
    {
        format!(
            "{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
            self.a,
            self.b,
            self.c,
            self.d,
            self.e.lock().unwrap(),
            self.f.read().unwrap(),
            unsafe { *self.g.get() },
            self.h.get(),
            self.i.borrow(),
        )
    }

    fn change_a(&mut self) // &mut is mandatory
    {
        let target = &mut self.a;
        *target += 10;
    }

    fn change_b(&mut self) // &mut is mandatory
    {
        let target = self.b.as_mut();
        *target += 20;
    }

    fn change_c(&mut self) // &mut is mandatory
    {
        let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
        *target += 30;
    }

    fn change_d(&mut self) // &mut is mandatory
    {
        let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
        *target += 40;
    }

    fn change_e(&self) // !!! no &mut here !!!
    {
        // With C++, a std::mutex protecting a separate integer (e)
        // would have been used as two data members of the structure.
        // As our intent is to alter the integer (e), and because
        // std::mutex::lock() is _NOT_ const (but it's an internal
        // that could have been hidden behind the mutable keyword),
        // this member function would _NOT_ be const in C++.
        // But here, &self (equivalent of a const member function)
        // is accepted although we actually change the internal
        // state of the structure (the protected integer).
        let mut target = self.e.lock().unwrap();
        *target += 50;
    }

    fn change_f(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e)
        let mut target = self.f.write().unwrap();
        *target += 60;
    }

    fn change_g(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f)
        let target = self.g.get();
        unsafe { *target += 70 };
    }

    fn change_h(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g)
        self.h.set(self.h.get() + 80);
    }

    fn change_i(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g, h)
        let mut target = self.i.borrow_mut();
        *target += 90;
    }
}

fn main() {
    let mut t = Thing::new();
    println!(" initial: {}", t.show());
    t.change_a();
    println!("change_a: {}", t.show());
    t.change_b();
    println!("change_b: {}", t.show());
    t.change_c();
    println!("change_c: {}", t.show());
    t.change_d();
    println!("change_d: {}", t.show());
    t.change_e();
    println!("change_e: {}", t.show());
    t.change_f();
    println!("change_f: {}", t.show());
    t.change_g();
    println!("change_g: {}", t.show());
    t.change_h();
    println!("change_h: {}", t.show());
    t.change_i();
    println!("change_i: {}", t.show());
}
like image 823
prog-fh Avatar asked Aug 19 '20 12:08

prog-fh


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?

A RefCell is another way to change values without needing to declare mut . It means "reference cell", and is like a Cell but uses references instead of copies.

How to implement interior mutability in rust data structures?

Fortunately for us, the Rust standard library provides two wrappers, std::cell::Cell and std::cell::RefCell, that allow us to introduce interior mutability in externally immutable instances of data structures. With Cell<T> and RefCell<T> in our collective toolbelts, we can harness the power of interior mutability.

Should you avoid mutability in data structures?

The unspoken heuristic is that avoiding mutability when possible is good. And yet, in some cases you need a few mutable fields in data structures, whether they’re mutable or not.

What is API abuse and how to prevent it?

A thriving API abuse permits hackers to achieve admin-like access to the targeted API. This access endows hackers to make API work as per their will. Hackers make use of existing API vulnerabilities to rob crucial private or business information while corrupting your websites or applications.

What would happen if there was no interior mutability?

Without interior mutability, you would be forced to always use mutable pointers, which would allow mutation of the inner value and may be undesired. For instance, consider the following naive reference counted wrapper:


1 Answers

Relying on interior mutability to solve this problem sounds to me like, in C++, const_casting or abusing of mutable members just because elsewhere in the application we were not consistent about constness (common mistake for learners of C++).

This is a completely understandable thought in the context of C++. The reason it isn't accurate is because C++ and Rust have different concepts of mutability.

In a way, Rust's mut keyword actually has two meanings. In a pattern it means "mutable" and in a reference type it means "exclusive". The difference between &self and &mut self is not really whether self can be mutated or not, but whether it can be aliased.

In the Messenger example, well, first let's not take it too seriously; it's meant to illustrate the language features, not necessarily system design. But we can imagine why &self might be used: Messenger is meant to be implemented by structures that are shared, so different pieces of code can hold references to the same object and use it to send alerts without coordinating with each other. If send were to take &mut self, it would be useless for this purpose because there can only be one &mut self reference in existence at a time. It would be impossible to send messages to a shared Messenger (without adding an external layer of interior mutability via Mutex or something).

On the other hand, every C++ reference and pointer can be aliased.¹ So in Rust terms, all mutability in C++ is "interior" mutability! Rust has no equivalent to mutable in C++ because Rust has no const members (the catchphrase here is "mutability is a property of the binding, not the type"). Rust does have an equivalent to const_cast, but only for raw pointers, because it's unsound to turn a shared & reference into an exclusive &mut reference. Conversely, C++ has nothing like Cell or RefCell because every value is implicitly behind an UnsafeCell already.

So, back to my example code below, should I[...]

It really depends on the intended semantics of Thing. Is it the nature of Thing to be shared, like a channel endpoint or a file? Does it make sense for change_e to be called on a shared (aliased) reference? If so, then use interior mutability to expose a method on &self. Is Thing primarily a container for data? Does it sometimes make sense for it to be shared and sometimes exclusive? Then Thing should probably not use interior mutability and let the user of the library decide how to deal with shared mutation, should it be necessary.

See also

  • Accurate mental model for Rust's reference types
  • What's the difference between placing "mut" before a variable name and after the ":"?
  • Why is the mutability of a variable not reflected in its type signature in Rust?
  • Need holistic explanation about Rust's cell and reference counted types

¹ Actually, C++ does have a feature that makes pointers work similar to references in Rust. Kind of. restrict is a non-standard extension in C++ but it's part of C99. Rust's shared (&) references are like const *restrict pointers, and exclusive (&mut) references are like non-const *restrict pointers. See What does the restrict keyword mean in C++?

When was the last time you deliberately used a restrict (or __restrict, etc.) pointer in C++? Don't bother thinking about it; the answer is "never". restrict enables more aggressive optimizations than regular pointers, but it is very hard to use it correctly because you have to be extremely careful about aliasing, and the compiler offers no assistance. It's basically a massive footgun and hardly anyone uses it. In order to make it worthwhile to use restrict pervasively the way you use const in C++, you'd need to be able to annotate onto functions which pointers are allowed to alias other ones at which times, make some rules about when pointers are valid to follow, and have a compiler pass that checks whether the rules are being followed in each function. Like some kind of... checker.

like image 197
trent Avatar answered Oct 21 '22 17:10

trent