Explain to me how this compiles (value being modified through immutable ref)

Hello everyone :wave:,
while writing a bit of Rust code I noticed that I could replace a move of a value in a certain function with an immutable reference to it and still call a method on it inside the function which takes &mut self. Here's a somewhat minimal example:

use std::fs::File;
use std::io::Read;

fn read_fn(mut input: impl Read) {
    let mut buf = Vec::new();
    input.read_to_end(&mut buf).unwrap();
}

fn main() {
    let file = File::open("test").unwrap();
    read_fn(&file);
    file.metadata().unwrap();
}

In the real code, I just added the & to the argument in read_fn() and did not expect that it would compile just like that, but it did! As can be seen here.
What I actually expected was the following: due to the fact that the variable file has not been declared mutable you cannot take a mutable reference out of it (and I clearly do not do this here), so the code should fail at the call to read_to_end(). Furthermore I thought that the mut parameter in read_fn only turns the variable input into a mutable variable of immutable references to File.
The fact that I can still call a method on file after read_fn() means that no weird move happened here either so what is going on here exactly? I just cannot wrap my head around it and searching the internet did not yield any results for me either. I'm not even sure what I need to search for exactly.
Also note that impl Read can just be replaced with &File in this particular case and it will still be fine, so the generic parameter isn't doing anything unexpected for me but I just left it in to indicate that I could just add the & and the code would still compile.

What you're witnessing here is &Files Read implementation.

1 Like

&File implements Read, which is why this works. input.read_to_end is taking a &mut &File (which is why the param needs to be annotated as mut.) Reading a File doesn't actually mutate any state in the File - at least not as Rust is concerned.

4 Likes

This is because, &T means shared reference, not immutable reference. There are plenty of ways to mutate through a shared references. See and Cell and Mutex from std

To read more: https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html

Because files are intrinsically shared, it should be fine to mutate them through a shared reference. Rust simply provides a safe api to do so.

2 Likes

Thanks for the responses guys, I really missed the part with &File implementing Read, which is why this code didn't already fail at the function signature :see_no_evil: . So I checked the actual source code right now and yes, the inner variable wrapped by the File struct has platform-specific implementations of a "read" function which takes &self as parameter.
@KrishnaSannasi Thanks for the pointer, I understand what you're saying but my brain thought for a moment that the code was doing essentially the equivalent of:

let file = File::open("test")?;
file.read_to_end(...)?;

which definitely does not compile, since read_to_end takes &mut self. But everything works due to the aforementioned impl Read for &File:

let file = File::open("test")?;
let mut f = &file;
f.read_to_end(...)?;

Still looks pretty weird to me but oh well. So thanks again for the responses. :+1:

1 Like