Jump to content

Fighting the Rust Borrow Checker (and Winning)

From JOHNWICK
Revision as of 17:40, 15 November 2025 by PC (talk | contribs) (Created page with "If you’ve written Rust for more than five minutes, you’ve probably met the borrow checker. It’s opinionated. It’s meticulous. And sometimes, when your code looks perfectly innocent, it still says “nope.” I recently bumped into this while hacking together a tiny pager UI with rustbox. The idea was simple: open a file, show a screenful of lines, and on spacebar show the next screenful. Nothing exotic. Here’s the shape of the first attempt: extern crate rus...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

If you’ve written Rust for more than five minutes, you’ve probably met the borrow checker. It’s opinionated. It’s meticulous. And sometimes, when your code looks perfectly innocent, it still says “nope.” I recently bumped into this while hacking together a tiny pager UI with rustbox. The idea was simple: open a file, show a screenful of lines, and on spacebar show the next screenful. Nothing exotic. Here’s the shape of the first attempt: extern crate rustbox; use std::default::Default; use std::error::Error; use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; use rustbox::{Color, RustBox}; use rustbox::Key; fn display_screenful(rb: &RustBox, fr: BufReader<&'static File>, offset: usize) {

   for (rline, idx) in fr.lines().zip(0..).skip(offset).take(rb.height()) {
       match rline {
           Ok(line) => rb.print(0, idx, rustbox::RB_NORMAL, Color::White, Color::Black, &line),
           Err(_)   => rb.print(0, idx, rustbox::RB_NORMAL, Color::White, Color::Black, "")
       }
   }

} fn main() {

   let rustbox = RustBox::init(Default::default()).unwrap();
   let path = env::args().nth(1).unwrap();
   let file = File::open(&path).unwrap();
   let file_reader = BufReader::new(&file);
   display_screenful(&rustbox, file_reader, 0);
   rustbox.present();
   // ... poll events, call display_screenful again on space ...

} It felt fine. The compiler disagreed: error: `file` does not live long enough note: reference must be valid for the static lifetime... error: use of moved value: `file_reader` [E0382] note: `file_reader` moved here because it has type `BufReader<&'static File>`, which is non-copyable Ouch. Two errors, two important lessons about lifetimes and moves.


Lesson 1: 'static means forever, not “for a while” The original display_screenful signature asked for a BufReader<&'static File>. That means “a BufReader holding a reference to a File that is valid for the entire duration of the program.” But the file variable is created inside main and is only guaranteed to live until main ends—not forever. The borrow checker rightly says: “You promised me 'static. You can’t deliver.” Fix the lifetime You almost never want to demand 'static here. You want to accept whatever lifetime the caller can provide: // long form fn display_screenful<'a>(rb: &RustBox, fr: BufReader<&'a File>, offset: usize) { /* ... */ } // or short form (let the compiler infer 'a) fn display_screenful(rb: &RustBox, fr: BufReader<&File>, offset: usize) { /* ... */ } But there’s a second snag coming…


Lesson 2: Passing by value moves ownership BufReader<&File> is a non-Copy type. When you pass file_reader by value to display_screenful, you move it into the function. That means you can’t use file_reader again later to draw the next page—thus the “use of moved value” error (E0382). Borrow the reader mutably BufRead::lines() requires &mut self because reading advances the cursor. So your function should borrow the reader, not take ownership of it: fn display_screenful(rb: &RustBox, fr: &mut BufReader<&File>, offset: usize) {

   // ...

}Now you can keep using the same reader across calls. Great! But we can make this even cleaner.


A more idiomatic (and simpler) version Instead of borrowing a File inside BufReader, just let the BufReader own the File. This removes lifetime noise altogether: use std::default::Default; use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; use rustbox::{Color, RustBox, Key, Event, RB_NORMAL}; fn display_screenful(rb: &RustBox, fr: &mut BufReader<File>, offset: usize) {

   // Note: offset is explained below; often you'll call with offset = 0
   for (idx, rline) in fr.lines().skip(offset).take(rb.height()).enumerate() {
       let text = rline.as_deref().unwrap_or("");
       rb.print(0, idx, RB_NORMAL, Color::White, Color::Black, text);
   }

} fn main() {

   let rustbox = RustBox::init(Default::default()).expect("rustbox init failed");
   let path = env::args().nth(1).expect("provide a file path");
   let file = File::open(&path).expect("open file");
   let mut reader = BufReader::new(file);
   display_screenful(&rustbox, &mut reader, 0);
   rustbox.present();
   loop {
       match rustbox.poll_event(false) {
           Ok(Event::KeyEvent(Some(Key::Char('q')))) => break,
           Ok(Event::KeyEvent(Some(Key::Char(' '))))) => {
               // We already advanced the reader; pass offset = 0 to get the *next* lines.
               display_screenful(&rustbox, &mut reader, 0);
               rustbox.present();
           }
           Ok(_) => {}
           Err(e) => panic!("event error: {e}"),
       }
   }

} Why this is nicer:

  • No explicit lifetime parameters anywhere in your public signature.
  • reader stays in main and is borrowed mutably when needed.
  • The borrow checker is happy because lifetimes and ownership are crystal clear.


“But I wanted to page with an offset!” In the original code you called: display_screenful(&rustbox, file_reader, 0); // first page display_screenful(&rustbox, file_reader, rustbox.height()); // next page? Here’s the gotcha: calling .lines() consumes from the current cursor. After the first call you’re already at “line N”. If you then skip(rb.height()) you’ll jump another screenful ahead, effectively skipping a page. You probably want one of these strategies:

  • Sequential paging (simplest): keep advancing the same BufReader and always call with offset = 0. That prints the next height() lines each time.
  • Random access paging: pre-read the file into memory once and display slices by index:
  • let lines: Vec<String> = std::io::BufRead::lines(BufReader::new(File::open(path)?)) .collect::<Result<_, _>>()?; fn display_slice(rb: &RustBox, lines: &[String], start: usize) { for (idx, line) in lines.iter().skip(start).take(rb.height()).enumerate() { rb.print(0, idx, RB_NORMAL, Color::White, Color::Black, line); } }
  • This avoids I/O during paging and makes offsets intuitive.
  • Seek-based paging: if files are large and you don’t want to store everything in memory, you can maintain an index of byte offsets per line and seek to the right place. That’s more work but very scalable.

For a tiny pager, #1 or #2 is perfect.


Generalizing like a pro: accept any BufRead One of Rust’s joys is abstracting over behavior instead of concrete types. If you want display_screenful to work with anything that implements BufRead (files, memory buffers, network streams), make it generic: use std::io::BufRead; fn display_screenful<R: BufRead>(rb: &RustBox, fr: &mut R, offset: usize) {

   for (idx, rline) in fr.lines().skip(offset).take(rb.height()).enumerate() {
       let text = rline.as_deref().unwrap_or("");
       rb.print(0, idx, rustbox::RB_NORMAL, Color::White, Color::Black, text);
   }

}

  • Still a mutable borrow because reading advances the cursor.
  • No lifetimes in sight because we’re borrowing something that lives in the caller.


A quick mental model for borrow-checker peace

  • Own or borrow? If your function just uses something temporarily, borrow it (&T or &mut T). If it’s going to store or manage it, own it (T).
  • 'static is special. It means “lives for the whole program.” Don’t demand 'static unless you really mean it (e.g., string literals).
  • Non-Copy types move by default. Passing by value transfers ownership; you can’t use the old binding afterward. Borrow instead.
  • Methods tell you what you need. If a method takes &mut self (like lines()), your function needs a &mut too.

When you align your API with those facts, the borrow checker feels less like a gatekeeper and more like a helpful teammate that keeps you from subtle bugs — like using a reader after it was consumed or holding a reference that can dangle.


Summary: the minimal fix Your original intuition (“this code looks fine”) was almost right. The two tiny changes that make it compile and behave:

  • Don’t ask for &'static File. Take a reference with whatever lifetime the caller can give (or, better, own the File inside BufReader).
  • Don’t move the reader into the function. Borrow it mutably: &mut BufReader<_>.

After that, your pager works and you’ve added another notch to your borrow-checker belt. 🦀


Bonus: final minimal diff - fn display_screenful(rb: &RustBox, fr: BufReader<&'static File>, offset: usize) { + fn display_screenful(rb: &RustBox, fr: &mut BufReader<File>, offset: usize) {

    // ...
}

- let file_reader = BufReader::new(&file); - display_screenful(&rustbox, file_reader, 0); + let mut reader = BufReader::new(file); + display_screenful(&rustbox, &mut reader, 0); - display_screenful(&rustbox, file_reader, rustbox.height()); + display_screenful(&rustbox, &mut reader, 0); // continue sequentially If you prefer to keep the borrowed File approach, just adjust the signature to &mut BufReader<&File>—the core idea (mutable borrow, not move) stays the same. Happy paging — and happy borrowing!