0

The following code doesn't compile (as expected):

use sdl2::event::Event;
use sdl2::pixels::Color;

pub fn main() {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();

    let window = video_subsystem
        .window("rust-sdl2 demo", 800, 600)
        .build()
        .unwrap();

    let mut canvas = window.into_canvas().build().unwrap();
    canvas.set_draw_color(Color::RGB(0, 0, 0));
    canvas.clear();
    canvas.present();

    let mut event_pump = sdl_context.event_pump().unwrap();
    let keyboard = sdl2::keyboard::KeyboardState::new(&event_pump);

    'running: loop {
        if keyboard.is_scancode_pressed(sdl2::keyboard::Scancode::Space) {
            println!("test");
        }
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit { .. } => break 'running,
                _ => {}
            }
        }
    }
}

The compiler shows this error message:

error[E0502]: cannot borrow `event_pump` as mutable because it is also borrowed as immutable
  --> src\main.rs:25:22
   |
19 |     let keyboard = sdl2::keyboard::KeyboardState::new(&event_pump);
   |                                                       ----------- immutable borrow occurs here
...
22 |         if keyboard.is_scancode_pressed(sdl2::keyboard::Scancode::Space) {
   |            ------------------------------------------------------------- immutable borrow later used here
...
25 |         for event in event_pump.poll_iter() {
   |                      ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

Why does it compile if I move the let keyboard line inside the loop? I guess it has probably something to do with the lifetime of the objects. But the error it shows doesn't make sense to me. From my understanding, I don't see why it matters if let keyboard is outside or inside the loop.

tgonzalez89
  • 621
  • 1
  • 6
  • 26

1 Answers1

1

Rust tries it's best to see if it can find a way to scope things for you so that lifetimes are not violated within a function body. When you include let keyboard inside of the loop, Rust takes a look at the possible valid ways it can scope things to control drop order and finds a possibility like this:

'running: loop {
    {
        let keyboard = sdl2::keyboard::KeyboardState::new(&event_pump);
        if keyboard.is_scancode_pressed(sdl2::keyboard::Scancode::Space) {
            println!("test");
        }
    }
    for event in event_pump.poll_iter() {
        match event {
            Event::Quit { .. } => break 'running,
            _ => {}
        }
    }
}

In other words it notices that if you're calling let each time through the loop you're effectively dropping and reallocating the keyboard every time. It then goes on to notice that it's semantically equivalent to what your wrote if it orders that drop to get rid of the immutable reference to event loop stored INSIDE keyboard before you go and mutate what the reference is pointing to later on. This keeps the requirements of the reference satisfied and is not at odds with the implementation you wrote, so the compiler silently substitutes it for you so that your code can compile, but it's still completely memory safe.

That makes the problem with keeping the let outside of the loop obvious, because keyboard is both 1. not dropped and 2. is storing that immutable reference to event_pump internally, so there's no possible drop ordering it can insert for you to make it memory safe because there's no drop anymore. The function signature of keyboard::new (the contract) implies that you cannot mutate event_pump so long as keyboard remains alive.

If keyboard is used again after the event for/match, then it will fail because it'll need to keep the immutable reference.

So, basically the compiler puts "artificial" scopes where it makes sence to be able to satisfy its own rules, so that the programmer doesn't have to do it manually.

Rust is allowed to reorder operations as long as the observable outcome is exactly equivalent to what you wrote, pretty much all compiled languages do this. Latency between operations is not considered something "observable", so deallocation is something it's usually allowed to move around. That's the underlying property that allows the artificial scoping to work.

As a historical note, Rust 2015 did NOT do this. The original releases of Rust used the "lexical lifetime" borrow checker, which did not have this capability. The borrow checker got a big upgrade to the "non-lexical lifetime" checker with the 2018 edition of Rust that no longer strictly relied on the lexical order of your tokens, so it could do this type of substitution.

tgonzalez89
  • 621
  • 1
  • 6
  • 26