-3

In the code below, I use unsafe code to convert an immutable reference into a mutable pointer and then I try to edit the inner value by means of this mutable pointer.

fn main() {
    #[repr(transparent)]
    struct Item(isize);

    impl Item {
        #[inline]
        fn ptr(&self) -> *mut isize {
            self as *const Item as *mut isize
        }
        fn increment(&self) {
            let val = self.0 + 1;
            unsafe {std::ptr::write(self.ptr(), val)}
        }
    }

    let item = Item(22);
    println!("before = {}", item.0);
    item.increment();
    println!("after = {}", item.0);
}

When I compile this under debug mode, the results are as expected and the value is indeed incremented. However, under release mode,the value is not incremented at all although that section of the code is run. Also, the following other types of mutating the value don't seem to work either

unsafe {*&mut *(self.ptr()) += 1;}

std::mem::replace()

std::mem::swap()

  1. What is the reason for this?
  2. Is what I seek to do impossible under the --release mode?
Isaac Dzikum
  • 184
  • 9
  • 5
    There is no actual question here. "convert an immutable reference into a mutable pointer and then I try to edit the inner value by means of this mutable pointer" is Undefined Behavior, and the compiler *will* produce seemingly nonsensical machine code in this sort of situation. The fact that it seems to work in non-release mode is that the compiler simply doesn't bother to do the work - but the code is wrong in any case. – user2722968 Apr 07 '23 at 21:32
  • Added: if you run the code on the playground via `Tools -> Miri`, the interpreter catches the UB cold: `error: Undefined Behavior: attempting a write access using <5863> at alloc1500[0x0], but that tag only grants SharedReadOnly permission for this location` – user2722968 Apr 07 '23 at 21:34
  • Mutating behind an immutable (shared) reference is only allowed via `Cell`, `RefCell`, or `UnsafeCell` (or the thread-safe synchronized versions `Atomic*` and `Mutex`). This is called "interior mutability". https://doc.rust-lang.org/stable/std/cell/index.html – PitaJ Apr 07 '23 at 21:37
  • This is undefined behavior for the same reason that mutating a const value via a reference obtained with `const_cast` is undefined behavior in C++. You can cast away the const-ness with unsafe code, but you still can't modify the resulting value. – Silvio Mayolo Apr 08 '23 at 00:59
  • 1
    For "why this happens", it's almost certainly because in debug mode, it's reading from the memory location holding `item.0` live for each `println!`. In release mode, it says "man, that's stupid, the value can't change, so let's cache it to a register and reuse it for both `println!`s" (or even just hardcode the initial value into the instructions when it prints). It still *has* a memory location (to allow for pointer-taking shenanigans), but within `main`, it reads it into a register once and never bothers to check again. – ShadowRanger Apr 08 '23 at 01:32
  • 3
    In short, you lied to the compiler when you said it was immutable, and the compiler trusted you because `unsafe` means "trust me, I'm being careful, so don't check me". Release mode is skipping a lot of work by taking advantage of the guarantees that you lied about. – ShadowRanger Apr 08 '23 at 01:33

1 Answers1

5

Rust does not allow you to mutate a value obtained from an immutable reference, ever, even if you cast it to into a mutable reference or mutable raw pointer. The compiler is allowed to assume that a value that's passed around using an immutable reference will never change, and it can optimize with this in mind. Breaking this assumption is undefined behavior, and can break your program in very unexpected ways. This is probably what happens in your example: the compiler sees that increment takes an immutable reference, and therefore item must be the same before and after the call, and it can optimize the code as if that's the case.

There's only one exception to this: UnsafeCell. UnsafeCell is a special type that allows for interior mutability, letting you get a &mut T from an &UnsafeCell<T> (with unsafe code), and telling the compiler that it can't assume that the content is unchanged even if you have an immutable reference to the cell.

The problem with UnsafeCell though is that you still need to uphold Rust's borrowing guarantees (e.g. mutable references must be unique while they exists), but the compiler will rely on you as a programmer to make sure these are upheld. For that reason, it's generally recommended that you instead use safe types, such as Cell, RefCell, Mutex, RwLock or atomic types. These all have different trade-offs, but they're all built as safe wrappers around UnsafeCell to allow for interior mutability while having some checks for the borrowing guarantees (some at compile time, some at runtime).

Cell, for example, could work for your example:

fn main() {
    #[repr(transparent)]
    struct Item(Cell<isize>);

    impl Item {
        fn increment(&self) {
            let val = self.0.get() + 1;
            self.0.set(val);
        }
    }

    let item = Item(Cell::new(22));
    println!("before = {}", item.0.get());
    item.increment();
    println!("after = {}", item.0.get());
}
Frxstrem
  • 38,761
  • 9
  • 79
  • 119