← In Class CS 326 · Week 02 · Code and output Space/j/k sections · click to pin · n/p example · i edit · l copy link

Week 02 · Rust: Types, Ownership, and Borrowing

Every program in examples/src/bin/ beside the output it printed, and every file in examples/broken/ beside what rustc said about it. Each colored, numbered section of code sits in the same row as the output it produced. Click an output line to light up the println! that printed it, or click a println! to find its output. Click a section's number to copy a link straight to that section, to paste where students will follow it. Press i on a program to edit it and run it. On a phone the two columns do not fit, so a Code/Output switch shows one at a time.

The programs

ProgramThe one ideaThe line to point atSections
01scalarsa value is its bits; usize is an addressthe PTE decoded from a u646
02compoundstructs/arrays/enums are plain layoutssize_of::<Option<&u8>>() == 85
03stack_and_heapthe handle is not the bufferpush printing a new buffer address6
04movea move copies 3 words, not the data8 MiB "moved" with the same pointer5
05copy_typesCopy XOR Droptwo independent Pte copies at different addresses6
06dropfree() became a scopekfree lines in reverse declaration order6
07borrow_sharedmany readers, no ownershipthree references printing one buffer address5
08borrow_mut&mut means exclusivesplit_at_mut — two writers, provably disjoint6
09slicesptr + len, checkedone checksum over an array, a Vec, and a slice6
10how_to_passchoosing T / &T / &mut Tthe mini PageAlloc with &self and &mut self2

Programs that must not compile

ErrorWhat rustc saysThe fix
E0106missing lifetime specifier (and, once you add one, error[E0515]: cannot return reference to local)return the value, or borrow from a parameter
E0382borrow of moved valueuse t, clone(), or borrow
E0499cannot borrow v as mutable more than once at a timesequence them, or split_at_mut
E0502cannot borrow v as mutable because it is also borrowed as immutablefinish the read first; or copy the value out
E0505cannot move out of s because it is borroweduse the reference before the move
E0507cannot move out of index of Vec<String>borrow, clone(), or remove()
E0596cannot borrow v as mutable, as it is not declared as mutableadd mut

Keys

Space j ↓ →next section (dims the rest)
Shift+Space k ↑ ←previous section
0–9jump to that section
clickpin an output line and the code that printed it (click again to unpin)
click a numbercopy a link to that section, or to the whole example from the number beside its title
lcopy a link to the pinned line, else the section in focus, else this example
Escunpin, then leave section focus, and drop it from the link so a reload does not bring it back
n pnext or previous example
rnext run, for programs run more than once
eopen or close the explanation (broken files) or the build warnings
+ -larger or smaller text (remembered)
iedit this program and run it
⌘/Ctrl+↵run what you have edited
ccompare: what you changed in the code, and what changed in the output (press Esc first if you are typing)
oon a narrow screen, switch between the code and the output column

01 Scalar types: what a value is in memory.

a value is its bits; usize is an address · point at the PTE decoded from a u64

$ cargo run --bin 01_scalars
//! 01 — Scalar types: what a value *is* in memory.▸ 8 more lines
//!
//! Every scalar has a size the compiler knows at compile time. Nothing is
//! boxed, nothing has a header, and nothing is scanned at run time.
//!
//! let x: u8 = 200; stack: [ 11001000 ] 1 byte
//! let y: u32 = 200; stack: [ 11001000 00000000 x2 ] 4 bytes
//!
//! Run: cargo run --bin 01_scalars

use std::mem::size_of;

fn main() {
1 println!("== integers ==");
// Signed and unsigned, 8 through 64 bits, plus the pointer-sized pair.
println!("u8 {} byte 0 ..= {}", size_of::<u8>(), u8::MAX);
println!("i32 {} bytes {} ..= {}", size_of::<i32>(), i32::MIN, i32::MAX);
println!("u64 {} bytes 0 ..= {}", size_of::<u64>(), u64::MAX);
// usize is as wide as an address on this machine: 8 bytes on x86-64 and
// on riscv64. This is the type an OS uses for addresses and lengths.
println!("usize {} bytes (one machine address)", size_of::<usize>());

1== integers ==
u8 1 byte 0 ..= 255
i32 4 bytes -2147483648 ..= 2147483647
u64 8 bytes 0 ..= 18446744073709551615
usize 8 bytes (one machine address)
2 println!("\n== literals are the same bits, written differently ==");
let pgsize_dec = 4096;
let pgsize_hex = 0x1000;
let pgsize_bin = 0b1_0000_0000_0000;
let pgsize_shift = 1 << 12;
println!(
"{pgsize_dec} == {pgsize_hex} == {pgsize_bin} == {pgsize_shift} -> {}",
pgsize_dec == pgsize_hex && pgsize_hex == pgsize_bin && pgsize_bin == pgsize_shift
);
// The underscore is nothing at all — just a reading aid.
let big = 0x8000_0000usize;
println!("0x8000_0000 = {big} = {big:#x} = {big:#b}");

2== literals are the same bits, written differently ==
4096 == 4096 == 4096 == 4096 -> true
0x8000_0000 = 2147483648 = 0x80000000 = 0b10000000000000000000000000000000
3 println!("\n== no implicit conversion, ever ==");
let small: u8 = 200;
let wide: u32 = small as u32 + 100; // `as` is the explicit request
println!("200u8 as u32 + 100 = {wide}");
// The reverse truncates, and Rust makes you say so out loud:
println!("300u32 as u8 = {} (bits above 8 are gone)", 300u32 as u8);

3== no implicit conversion, ever ==
200u8 as u32 + 100 = 300
300u32 as u8 = 44 (bits above 8 are gone)
4 println!("\n== overflow is not undefined behavior ==");
let x: u8 = 255;
// Debug builds panic on overflow; release builds wrap. Neither is silent
// corruption, and neither is UB. Say which one you want:
println!("255u8.checked_add(1) = {:?}", x.checked_add(1));
println!("255u8.wrapping_add(1) = {}", x.wrapping_add(1));
println!("255u8.saturating_add(1)= {}", x.saturating_add(1));
println!("255u8.overflowing_add(1)= {:?}", x.overflowing_add(1));

4== overflow is not undefined behavior ==
255u8.checked_add(1) = None
255u8.wrapping_add(1) = 0
255u8.saturating_add(1)= 255
255u8.overflowing_add(1)= (0, true)
5 println!("\n== the other scalars ==");
let ready: bool = true; // 1 byte
let mode: char = 'S'; // 4 bytes: a Unicode scalar value
let ratio: f64 = 4096.0 / 3.0; // 8 bytes, IEEE-754
println!("bool {} ({} byte) char {mode:?} ({} bytes) f64 {ratio:.3} ({} bytes)",
ready, size_of::<bool>(), size_of::<char>(), size_of::<f64>());

5== the other scalars ==
bool true (1 byte) char 'S' (4 bytes) f64 1365.333 (8 bytes)
6 println!("\n== a u64 that is really 64 flags: an SV39 page-table entry ==");
// V=1 R=1 W=1, PPN = physical page 0x8_0005
let pte: u64 = (0x8_0005 << 10) | 0b111;
println!("pte = {pte:#018x}");
println!("valid = {}", pte & 1 == 1);
println!("readable = {}", (pte >> 1) & 1 == 1);
println!("writable = {}", (pte >> 2) & 1 == 1);
println!("ppn = {:#x} -> phys addr {:#x}", pte >> 10, (pte >> 10) << 12);
6== a u64 that is really 64 flags: an SV39 page-table entry ==
pte = 0x0000000020001407
valid = true
readable = true
writable = true
ppn = 0x80005 -> phys addr 0x80005000
}
exit status 0

02 Compound types: tuples, arrays, structs, enums.

structs/arrays/enums are plain layouts · point at size_of::<Option<&u8>>() == 8

$ cargo run --bin 02_compound
//! 02 — Compound types: tuples, arrays, structs, enums.▸ 10 more lines
//!
//! All of these are *plain layouts*. A struct is its fields laid end to end
//! (with padding); an array is n elements back to back. No pointers appear
//! unless you write one.
//!
//! struct Frame { addr: usize, free: bool }
//!
//! stack: [ addr: 8 bytes ][ free: 1 ][ 7 bytes padding ] = 16 bytes
//!
//! Run: cargo run --bin 02_compound

use std::mem::size_of;

/// A fixed-size record. Copy: 16 bytes of plain data, no owned resource.
#[derive(Debug, Clone, Copy)]
struct Frame {
addr: usize,
free: bool,
}

/// An enum is a tag plus the largest variant's payload.
#[derive(Debug)]
enum Request {
Read { block: u32 },
Write { block: u32, len: u32 },
Flush,
}

fn main() {
1 println!("== tuples: a fixed group, positional ==");
let split: (usize, usize) = (0x8000_0000, 0x8800_0000);
let (start, end) = split; // destructuring
println!("ram {start:#x}..{end:#x} = {} MiB", (end - start) / (1024 * 1024));
println!("tuple.0 = {:#x}, size_of = {} bytes", split.0, size_of::<(usize, usize)>());

1== tuples: a fixed group, positional ==
ram 0x80000000..0x88000000 = 128 MiB
tuple.0 = 0x80000000, size_of = 16 bytes
2 println!("\n== arrays: [T; N], N is part of the type ==");
// Fixed size, known at compile time, lives right here on the stack.
let mut ptable: [Frame; 4] = [Frame { addr: 0, free: true }; 4];
for (i, f) in ptable.iter_mut().enumerate() {
f.addr = 0x8000_0000 + i * 4096;
}
ptable[2].free = false;
println!("size_of::<[Frame; 4]>() = {} bytes (4 x {})",
size_of::<[Frame; 4]>(), size_of::<Frame>());
for f in &ptable {
println!(" {:#x} {}", f.addr, if f.free { "free" } else { "in use" });
}
// Bounds are checked. ptable[9] would panic, not read the neighbor.
println!("get(9) = {:?} <- no out-of-bounds read", ptable.get(9).map(|f| f.addr));

2== arrays: [T; N], N is part of the type ==
size_of::<[Frame; 4]>() = 64 bytes (4 x 16)
0x80000000 free
0x80001000 free
0x80002000 in use
0x80003000 free
get(9) = None <- no out-of-bounds read
3 println!("\n== structs: named fields, still just a layout ==");
let f = ptable[0];
println!("{f:?}\n addr in hex = {:#x}, size_of::<Frame>() = {} bytes", f.addr, size_of::<Frame>());

3== structs: named fields, still just a layout ==
Frame { addr: 2147483648, free: true }
addr in hex = 0x80000000, size_of::<Frame>() = 16 bytes
4 println!("\n== enums: one of several shapes, with the tag included ==");
let queue = [
Request::Read { block: 7 },
Request::Write { block: 7, len: 512 },
Request::Flush,
];
for r in &queue {
// `match` must cover every variant — the compiler checks that.
let described = match r {
Request::Read { block } => format!("read block {block}"),
Request::Write { block, len } => format!("write {len}B to block {block}"),
Request::Flush => "flush".to_string(),
};
println!(" {described}");
}
println!("size_of::<Request>() = {} bytes", size_of::<Request>());

4== enums: one of several shapes, with the tag included ==
read block 7
write 512B to block 7
flush
size_of::<Request>() = 12 bytes
5 println!("\n== Option: the null pointer, made a type you must open ==");
// There is no null. "Maybe a value" is spelled in the type system.
let head: Option<usize> = Some(0x8000_5000);
let empty: Option<usize> = None;
for slot in [head, empty] {
match slot {
Some(addr) => println!(" free list head at {addr:#x}"),
None => println!(" free list is empty"),
}
}
// And it can cost nothing extra: the niche optimization uses the
// impossible value 0 as the None tag for a reference.
println!("size_of::<&u8>() = {}", size_of::<&u8>());
println!("size_of::<Option<&u8>>() = {} <- same! None is the 0 bit pattern",
size_of::<Option<&u8>>());
5== Option: the null pointer, made a type you must open ==
free list head at 0x80005000
free list is empty
size_of::<&u8>() = 8
size_of::<Option<&u8>>() = 8 <- same! None is the 0 bit pattern
}
exit status 0

03 Where values live: the stack handle and the heap buffer.

the handle is not the buffer · point at push printing a new buffer address

$ cargo run --bin 03_stack_and_heap
//! 03 — Where values live: the stack handle and the heap buffer.▸ 10 more lines
//!
//! A `String` is three words on the stack that describe bytes on the heap:
//!
//! stack heap
//! s: [ ptr | len=6 | cap=6 ] ─────────▶ [ k e r n e l ]
//!
//! Ownership is about that arrow. The three words are cheap to copy; the
//! buffer they point at must be freed exactly once, by exactly one owner.
//!
//! Run: cargo run --bin 03_stack_and_heap

use std::mem::size_of;

fn main() {
1 println!("== a fixed-size value is the whole value ==");
let n: u64 = 4096;
println!("n = {n} lives at stack address {:p} size {} bytes",
&n, size_of::<u64>());

1== a fixed-size value is the whole value ==
n = 4096 lives at stack address 0x16f685a68 size 8 bytes
2 println!("\n== a String is a handle to somewhere else ==");
let s = String::from("kernel");
println!("handle on the stack at {:p}, {} bytes wide (ptr, len, cap)",
&s, size_of::<String>());
println!(" ptr -> {:p} len = {} cap = {}", s.as_ptr(), s.len(), s.capacity());
println!(" the bytes {:?} are NOT at {:p}; they are at {:p}",
s.as_bytes(), &s, s.as_ptr());

2== a String is a handle to somewhere else ==
handle on the stack at 0x16f685b50, 24 bytes wide (ptr, len, cap)
ptr -> 0x100819e10 len = 6 cap = 6
the bytes [107, 101, 114, 110, 101, 108] are NOT at 0x16f685b50; they are at 0x100819e10
3 println!("\n== growth moves the buffer, not the handle ==");
let mut v: Vec<u32> = Vec::with_capacity(2);
println!("handle stays at {:p} for the whole loop", &v);
let mut cap = v.capacity();
for i in 0..6 {
let before = v.as_ptr();
v.push(i);
// Two separate events. `Vec` doubles its capacity when it fills up
// (2 -> 4 -> 8); the allocator then decides whether the bigger block
// fits where the old one was. A growth that does not move is still a
// growth, so watch the `cap` column, not the address.
let note = match (v.capacity() != cap, v.as_ptr() != before) {
(true, true) => " <- grew: new buffer, old one copied and freed",
(true, false) => " <- grew: allocator extended it in place, same address",
_ => "",
};
println!(" push({i}) len={} cap={} buf={:p}{note}", v.len(), v.capacity(), v.as_ptr());
cap = v.capacity();
}

3== growth moves the buffer, not the handle ==
handle stays at 0x16f685db8 for the whole loop
push(0) len=1 cap=2 buf=0x100819c90
push(1) len=2 cap=2 buf=0x100819c90
push(2) len=3 cap=4 buf=0x100819c90 <- grew: allocator extended it in place, same address
push(3) len=4 cap=4 buf=0x100819c90
push(4) len=5 cap=8 buf=0x100819ca0 <- grew: new buffer, old one copied and freed
push(5) len=6 cap=8 buf=0x100819ca0
4 println!("\n== &str: a borrowed view, no ownership at all ==");
let owned: String = String::from("0x8000_0000");
let view: &str = &owned[2..]; // ptr + len, points into `owned`
println!("owned buffer {:p}, view {:p} <- same memory, offset by 2",
owned.as_ptr(), view.as_ptr());
println!("size_of::<String>() = {} size_of::<&str>() = {}",
size_of::<String>(), size_of::<&str>());

4== &str: a borrowed view, no ownership at all ==
owned buffer 0x100819c90, view 0x100819c92 <- same memory, offset by 2
size_of::<String>() = 24 size_of::<&str>() = 16
5 println!("\n== a literal is not on the heap either ==");
// "..." is baked into the binary; the &str just points at it.
let lit: &'static str = "compiled into .rodata";
println!("{lit:?} at {:p}", lit.as_ptr());

5== a literal is not on the heap either ==
"compiled into .rodata" at 0x1007b3a83
6 println!("\n== the release point is visible ==");
{
let scoped = String::from("freed at the closing brace");
println!(" inside the block: {:p} holds {scoped:?}", scoped.as_ptr());
} // <- the buffer is freed HERE. No free(), no collector, no delay.
println!(" after the block: that allocation is already gone");
6== the release point is visible ==
inside the block: 0x100819cc0 holds "freed at the closing brace"
after the block: that allocation is already gone
}
exit status 0

04 Move: the owner changes, the bytes do not.

a move copies 3 words, not the data · point at 8 MiB "moved" with the same pointer

$ cargo run --bin 04_move
//! 04 — Move: the owner changes, the bytes do not.▸ 13 more lines
//!
//! BEFORE a: [ ptr | 6 | 6 ] ────▶ [ k e r n e l ]
//! b: (does not exist)
//!
//! let b = a;
//!
//! AFTER a: struck off by rustc — using it is a compile error
//! b: [ ptr | 6 | 6 ] ────▶ [ k e r n e l ] (same buffer!)
//!
//! Run time: three words copied, usually optimized away.
//! Compile time: `a` is removed from the set of usable names.
//!
//! Run: cargo run --bin 04_move

fn main() {
1 println!("== assignment moves ==");
let a = String::from("kernel");
let a_buf = a.as_ptr();
let b = a; // `a` is moved out of, right here
println!("a's buffer was {a_buf:p}");
println!("b's buffer is {:p} <- identical: nothing was copied", b.as_ptr());
//println!("{a}"); // error[E0382]: borrow of moved value: `a`
println!("but `a` is no longer a usable name (uncomment the line to see E0382)");

1== assignment moves ==
a's buffer was 0x100e55e30
b's buffer is 0x100e55e30 <- identical: nothing was copied
but `a` is no longer a usable name (uncomment the line to see E0382)
2 println!("\n== passing to a function moves too ==");
let page = String::from("page 0x8000_5000");
println!("caller owns {:p}", page.as_ptr());
consume(page); // ownership transfers to the parameter
// page is gone here; the value was dropped inside `consume`.
println!("caller no longer owns anything");

2== passing to a function moves too ==
caller owns 0x100e55e40
consume() now owns 0x100e55e40 ("page 0x8000_5000")
caller no longer owns anything
3 println!("\n== if the caller still needs it, hand it back ==");
let mut log = String::from("boot");
log = annotate(log, "kalloc ready"); // move in, move out
log = annotate(log, "mmu on");
println!("returned to caller: {log:?}");

3== if the caller still needs it, hand it back ==
returned to caller: "boot | kalloc ready | mmu on"
4 println!("\n== moves out of collections are blocked, not silent ==");
#[allow(clippy::useless_vec)] // a Vec on purpose: the heap is the point
let v = vec![String::from("a"), String::from("b")];
// let first = v[0]; // error[E0507]: cannot move out of index
let first = &v[0]; // borrow instead
println!("borrowed v[0] = {first:?}; v is still whole with {} items", v.len());
let owned_first = v[0].clone(); // or pay for a copy, explicitly
println!("cloned v[0] = {owned_first:?} at a different buffer {:p} vs {:p}",
owned_first.as_ptr(), v[0].as_ptr());

4== moves out of collections are blocked, not silent ==
borrowed v[0] = "a"; v is still whole with 2 items
cloned v[0] = "a" at a different buffer 0x100e55d00 vs 0x100e55e40
5 println!("\n== a move is not a memcpy of the data ==");
let big = vec![0u8; 8 * 1024 * 1024]; // 8 MiB on the heap
let buf = big.as_ptr();
let moved = big; // 3 words move; 8 MiB stays put
assert_eq!(buf, moved.as_ptr());
println!("moved an 8 MiB Vec by copying 24 bytes of stack: buffer still {:p}",
moved.as_ptr());
5== a move is not a memcpy of the data ==
moved an 8 MiB Vec by copying 24 bytes of stack: buffer still 0x5f4000000
}

/// Takes ownership. The value dies at this closing brace.
fn consume(text: String) {
println!(" consume() now owns {:p} ({text:?})", text.as_ptr());
} // <- dropped here

/// Move in, move out: the classic "I need it back" signature.
fn annotate(mut text: String, note: &str) -> String {
text.push_str(" | ");
text.push_str(note);
text
}
exit status 0

05 Copy: values that do not move.

Copy XOR Drop · point at two independent Pte copies at different addresses

$ cargo run --bin 05_copy_types
//! 05 — Copy: values that do not move.▸ 13 more lines
//!
//! If a type owns no resource — no heap buffer, no file, no lock — then a
//! bitwise duplicate is a perfectly good second value. Those types are `Copy`,
//! and assignment duplicates instead of moving.
//!
//! let x = 5; let y = x; x: [ 5 ] y: [ 5 ] both usable
//! let a = s; let b = a; a: dead b: [ptr|len|cap]
//!
//! The rule that makes it sound: **Copy and Drop are mutually exclusive.**
//! If a copy existed of something with a destructor, the release would run
//! once per copy — a double free.
//!
//! Run: cargo run --bin 05_copy_types

/// Plain data: two integers. Duplicating the bits duplicates the value.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Pte {
ppn: usize,
flags: u8,
}

/// Same idea, but this one owns a heap buffer, so it cannot be Copy.
#[derive(Debug, Clone)]
struct Device {
name: String,
}

fn main() {
1 println!("== scalars copy ==");
let x: i32 = 5;
let y = x; // copy, not move
println!("x = {x}, y = {y} both still usable; separate slots {:p} {:p}", &x, &y);

1== scalars copy ==
x = 5, y = 5 both still usable; separate slots 0x16db4dc48 0x16db4dc4c
2 println!("\n== a Copy struct copies ==");
let mut a = Pte { ppn: 0x8_0005, flags: 0b111 };
let b = a; // copy
a.flags = 0; // mutating `a` does not touch `b`
println!("a = {a:?} at {:p}", &a);
println!("b = {b:?} at {:p} <- independent 16-byte copy", &b);

2== a Copy struct copies ==
a = Pte { ppn: 524293, flags: 0 } at 0x16db4dd68
b = Pte { ppn: 524293, flags: 7 } at 0x16db4dd78 <- independent 16-byte copy
3 println!("\n== arrays of Copy types copy ==");
let t1 = [Pte { ppn: 1, flags: 1 }; 3];
let mut t2 = t1; // whole array duplicated
t2[0].ppn = 99;
println!("t1[0] = {:?}", t1[0]);
println!("t2[0] = {:?} <- t1 untouched", t2[0]);

3== arrays of Copy types copy ==
t1[0] = Pte { ppn: 1, flags: 1 }
t2[0] = Pte { ppn: 99, flags: 1 } <- t1 untouched
4 println!("\n== a type that owns something moves instead ==");
let d1 = Device { name: String::from("uart0") };
let d2 = d1; // move: Device is not Copy
// println!("{d1:?}"); // error[E0382]: borrow of moved value
println!("d2 = {d2:?} (d1 is moved-from; Device owns a String)");
let d3 = d2.clone(); // an explicit, visible allocation
println!("clone -> different buffers: {:p} vs {:p}",
d2.name.as_ptr(), d3.name.as_ptr());

4== a type that owns something moves instead ==
d2 = Device { name: "uart0" } (d1 is moved-from; Device owns a String)
clone -> different buffers: 0x102b55e70 vs 0x102b55cf0
5 println!("\n== Copy XOR Drop ==");
// Uncomment either half of this pair and it will not compile:
// #[derive(Clone, Copy)] struct Guard;
// impl Drop for Guard { fn drop(&mut self) {} }
// error[E0184]: the trait `Copy` cannot be implemented for this type;
// the type has a destructor
println!("A destructor means one release point. A copy means many values.");
println!("Rust refuses the combination — that is the double free, rejected.");

5== Copy XOR Drop ==
A destructor means one release point. A copy means many values.
Rust refuses the combination — that is the double free, rejected.
6 println!("\n== references are Copy; what they point at is not ==");
let owner = String::from("free list");
let r1: &String = &owner;
let r2 = r1; // copying a reference is fine: many readers
println!("r1 = {r1:?} r2 = {r2:?} both point at {:p}", r1.as_ptr());
6== references are Copy; what they point at is not ==
r1 = "free list" r2 = "free list" both point at 0x102b55d00
}
exit status 0

06 Drop: where free() went.

free() became a scope · point at kfree lines in reverse declaration order

$ cargo run --bin 06_drop
//! 06 — Drop: where free() went.▸ 10 more lines
//!
//! The owner's scope ends, so the resource is released — right there, at a
//! point you can see in the source. No collector decides when. In a kernel
//! this is how you get "the lock is released at the end of this block" as a
//! fact instead of a comment.
//!
//! Watch the output: drops run in REVERSE declaration order, because later
//! values may refer to earlier ones.
//!
//! Run: cargo run --bin 06_drop

struct Page {
addr: usize,
}

impl Page {
fn alloc(addr: usize) -> Page {
println!(" kalloc {addr:#x}");
Page { addr }
}
}

impl Drop for Page {
/// This is the destructor. You never call it; the compiler emits the call
/// at the exact point the owner's scope ends.
fn drop(&mut self) {
println!(" kfree {:#x}", self.addr);
}
}

fn main() {
1 println!("== reverse declaration order ==");
{
let _first = Page::alloc(0x8000_1000);
let _second = Page::alloc(0x8000_2000);
let _third = Page::alloc(0x8000_3000);
println!(" -- end of block --");
} // third, second, first

1== reverse declaration order ==
kalloc 0x80001000
kalloc 0x80002000
kalloc 0x80003000
-- end of block --
kfree 0x80003000
kfree 0x80002000
kfree 0x80001000
2 println!("\n== a move changes WHO drops it, not whether ==");
{
let p = Page::alloc(0x8000_4000);
stash(p); // ownership moves in; the drop happens there
println!(" back in main; nothing left to free here");
}

2== a move changes WHO drops it, not whether ==
kalloc 0x80004000
stash() owns 0x80004000
kfree 0x80004000
back in main; nothing left to free here
3 println!("\n== returning ownership defers the drop ==");
{
let p = make(); // the drop follows the value out
println!(" main owns {:#x} now", p.addr);
} // freed here

3== returning ownership defers the drop ==
kalloc 0x80005000
main owns 0x80005000 now
kfree 0x80005000
4 println!("\n== drop() is just a move into a function that ends ==");
{
let p = Page::alloc(0x8000_6000);
println!(" releasing early");
drop(p); // std::mem::drop: takes ownership, returns ()
println!(" ... rest of the block runs with the page already freed");
}

4== drop() is just a move into a function that ends ==
kalloc 0x80006000
releasing early
kfree 0x80006000
... rest of the block runs with the page already freed
5 println!("\n== early return still frees everything ==");
maybe_fail(true);
maybe_fail(false);

5== early return still frees everything ==
kalloc 0x80007000
error path, returning early (owner: 0x80007000)
kfree 0x80007000
kalloc 0x80007000
success path
kfree 0x80007000
6 println!("\n== even a panic unwinds and frees (this one is caught) ==");
let _ = std::panic::catch_unwind(|| {
let _p = Page::alloc(0x8000_9000);
panic!("simulated fault");
});
println!(" page above was freed during unwinding");
6== even a panic unwinds and frees (this one is caught) ==
kalloc 0x80009000

thread 'main' panicked at src/bin/06_drop.rs:69:9:
simulated fault
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
kfree 0x80009000
page above was freed during unwinding
}

fn stash(p: Page) {
println!(" stash() owns {:#x}", p.addr);
} // dropped here

#[allow(clippy::let_and_return)] // the named binding is the teaching point
fn make() -> Page {
let p = Page::alloc(0x8000_5000);
p // moved out: NOT dropped here
}

fn maybe_fail(fail: bool) {
let p = Page::alloc(0x8000_7000);
if fail {
println!(" error path, returning early (owner: {:#x})", p.addr);
return; // dropped on this path
}
println!(" success path");
} // and dropped on this one
exit status 0

07 Shared borrows: &T. Lend it, don't give it away.

many readers, no ownership · point at three references printing one buffer address

$ cargo run --bin 07_borrow_shared
//! 07 — Shared borrows: `&T`. Lend it, don't give it away.▸ 10 more lines
//!
//! owner: s: [ ptr | len | cap ] ────▶ [ k e r n e l ]
//! ▲ ▲
//! r1: [ ptr ]────┘ │ many &T at once: all read-only
//! r2: [ ptr ]────────┘
//!
//! A reference is one machine word (for a sized type). It does not own; when
//! it goes out of scope nothing is freed.
//!
//! Run: cargo run --bin 07_borrow_shared

#[derive(Debug)]
struct FreeList {
pages: Vec<usize>,
}

fn main() {
1 println!("== lending instead of giving ==");
let list = FreeList { pages: vec![0x8000_1000, 0x8000_2000, 0x8000_3000] };
println!("count = {}", count(&list)); // & = "borrow, please"
println!("first = {:#x}", first(&list).unwrap());
println!("the caller still owns `list`: {} pages", list.pages.len());

1== lending instead of giving ==
count = 3
first = 0x80001000
the caller still owns `list`: 3 pages
2 println!("\n== many readers at once ==");
let r1 = &list;
let r2 = &list;
let r3 = &list.pages;
println!("r1 {:p} r2 {:p} r3 -> buffer {:p}", r1, r2, r3.as_ptr());
println!("all three point at the same memory; none of them can change it");

2== many readers at once ==
r1 0x16cf2de60 r2 0x16cf2de60 r3 -> buffer 0xb430009a0
all three point at the same memory; none of them can change it
3 println!("\n== a borrow cannot outlive the owner ==");
// The classic dangling pointer, rejected at compile time:
// fn dangle() -> &String { let s = String::from("x"); &s }
// error[E0106]: missing lifetime specifier
// The fix is to return the value itself — see `own_it` below.
let owned = own_it();
println!("returned an owned String {owned:?} instead of a reference to dead stack");

3== a borrow cannot outlive the owner ==
returned an owned String "kernel" instead of a reference to dead stack
4 println!("\n== borrows end at their LAST USE, not the closing brace ==");
let mut v = vec![1, 2, 3];
let sum: i32 = v.iter().sum(); // borrow starts
println!("sum = {sum}"); // borrow's last use
v.push(4); // legal: the borrow is already over
println!("pushed; v = {v:?}");
// If `println!("{sum} {:?}", v.iter().max())` came AFTER the push it would
// simply create a new borrow — also fine. What is rejected is overlap.

4== borrows end at their LAST USE, not the closing brace ==
sum = 6
pushed; v = [1, 2, 3, 4]
5 println!("\n== &T is Copy, so passing it along is free ==");
let text = String::from("uart0");
show(&text);
show(&text); // borrow again, as often as you like
println!("still owned by main: {text:?}");
5== &T is Copy, so passing it along is free ==
show() sees "uart0" at 0x103781c50
show() sees "uart0" at 0x103781c50
still owned by main: "uart0"
}

fn count(l: &FreeList) -> usize {
l.pages.len()
} // `l` goes out of scope: nothing is freed, it never owned anything

fn first(l: &FreeList) -> Option<&usize> {
l.pages.first()
}

fn own_it() -> String {
String::from("kernel")
}

fn show(s: &str) {
println!(" show() sees {s:?} at {:p}", s.as_ptr());
}
exit status 0

08 Exclusive borrows: &mut T. Read it as exclusive, not "mutable".

&mut means exclusive · point at split_at_mut — two writers, provably disjoint

$ cargo run --bin 08_borrow_mut
//! 08 — Exclusive borrows: `&mut T`. Read it as *exclusive*, not "mutable".▸ 14 more lines
//!
//! The whole rule, on one line:
//!
//! ALIASING XOR MUTATION
//!
//! many &T ..... or ..... exactly one &mut T ..... never both
//!
//! &T &T &T &mut T
//! \ | / |
//! ▼ ▼ ▼ ▼
//! [ the value ] [ the value ]
//! read-only read/write, nobody else looking
//!
//! Run: cargo run --bin 08_borrow_mut

#[derive(Debug)]
struct Allocator {
free: Vec<usize>,
handed_out: usize,
}

fn main() {
1 println!("== mutating through &mut ==");
let mut a = Allocator { free: vec![0x8000_1000, 0x8000_2000], handed_out: 0 };
println!("before: free={} handed_out={}", hex(&a.free), a.handed_out);
let page = take_page(&mut a); // lend it out, exclusively
println!("took {:#x}", page.unwrap());
println!("after: free={} handed_out={} <- main still owns the allocator", hex(&a.free), a.handed_out);

give_back(&mut a, 0x8000_9000);
println!("gave back: free={} handed_out={}", hex(&a.free), a.handed_out);

1== mutating through &mut ==
before: free=[0x80001000, 0x80002000] handed_out=0
took 0x80002000
after: free=[0x80001000] handed_out=1 <- main still owns the allocator
gave back: free=[0x80001000, 0x80009000] handed_out=0
2 println!("\n== exactly one &mut at a time ==");
{
let m = &mut a;
// let m2 = &mut a; // error[E0499]: second mutable borrow
m.handed_out += 100;
println!("mutated through m: handed_out = {}", m.handed_out);
}
println!("borrow over, `a` usable again: handed_out = {}", a.handed_out);

2== exactly one &mut at a time ==
mutated through m: handed_out = 100
borrow over, `a` usable again: handed_out = 100
3 println!("\n== &T and &mut T cannot overlap ==");
{
let snapshot = a.free.len(); // read finishes here
// let r = &a.free; // if this borrow were still live...
a.free.push(0x8000_A000); // ...this would be error[E0502]
println!("len was {snapshot}, now {}", a.free.len());
}
// Why it matters: `push` may reallocate. A live &T would then point at a
// freed buffer. That is a use-after-free the compiler just prevented.

3== &T and &mut T cannot overlap ==
len was 2, now 3
4 println!("\n== the iterator-invalidation bug, in the language ==");
let mut v = vec![1, 2, 3];
// for x in &v { v.push(*x); } // error[E0502] — and an infinite
// // loop over a moving buffer in C++
let extra: Vec<i32> = v.iter().map(|x| x * 10).collect(); // borrow ends here
v.extend(extra); // now mutate
println!("v = {v:?}");

4== the iterator-invalidation bug, in the language ==
v = [1, 2, 3, 10, 20, 30]
5 println!("\n== reborrowing: passing a &mut along ==");
let m = &mut a;
bump(m); // implicit reborrow: `m` is usable again after the call
bump(m);
println!("after two bumps: handed_out = {}", m.handed_out);

5== reborrowing: passing a &mut along ==
after two bumps: handed_out = 102
6 println!("\n== two &mut into DISJOINT parts is fine ==");
let mut table = [0usize; 6];
let (lo, hi) = table.split_at_mut(3); // std proves they do not overlap
lo[0] = 0xAAAA;
hi[0] = 0xBBBB;
println!("table = {table:#x?}");
6== two &mut into DISJOINT parts is fine ==
table = [
0xaaaa,
0x0,
0x0,
0xbbbb,
0x0,
0x0,
]
}

/// `&mut` in a signature is a promise: while this runs, nobody else can even
/// *look* at the allocator.
fn take_page(a: &mut Allocator) -> Option<usize> {
let p = a.free.pop()?;
a.handed_out += 1;
Some(p)
}

fn give_back(a: &mut Allocator, addr: usize) {
a.free.push(addr);
a.handed_out -= 1;
}

fn bump(a: &mut Allocator) {
a.handed_out += 1;
}

/// Takes a slice — it does not care whether the pages live in a Vec, an array,
/// or a window into either. See 09_slices.
fn hex(pages: &[usize]) -> String {
let items: Vec<String> = pages.iter().map(|p| format!("{p:#x}")).collect();
format!("[{}]", items.join(", "))
}
exit status 0

09 Slices: the systems shape.

ptr + len, checked · point at one checksum over an array, a Vec, and a slice

$ cargo run --bin 09_slices
//! 09 — Slices: the systems shape.▸ 14 more lines
//!
//! A slice is a borrowed window: pointer + length, two words, no ownership.
//!
//! buf: [ 10 11 12 13 14 15 16 17 ] (owned, on the stack or heap)
//! ▲ ▲
//! &buf[2..5] = [ ptr ][ len=3 ] ──────────┘
//!
//! This is what a kernel actually passes around: "these bytes, this many",
//! with the length checked instead of trusted. Compare with C:
//!
//! void write(char *buf, int n) // two arguments, no relationship
//! fn write(buf: &[u8]) // one value, length can't disagree
//!
//! Run: cargo run --bin 09_slices

fn main() {
1 println!("== a window into an array ==");
let buf: [u8; 8] = [10, 11, 12, 13, 14, 15, 16, 17];
let middle: &[u8] = &buf[2..5];
println!("buf {:?} at {:p}", buf, buf.as_ptr());
println!("&buf[2..5] {:?} at {:p} <- same memory, offset 2, len {}",
middle, middle.as_ptr(), middle.len());
println!("size_of::<&[u8]>() = {} bytes (ptr + len)",
std::mem::size_of::<&[u8]>());

1== a window into an array ==
buf [10, 11, 12, 13, 14, 15, 16, 17] at 0x16f751a60
&buf[2..5] [12, 13, 14] at 0x16f751a62 <- same memory, offset 2, len 3
size_of::<&[u8]>() = 16 bytes (ptr + len)
2 println!("\n== one function, any backing storage ==");
let heap: Vec<u8> = vec![1, 2, 3, 4];
let stack: [u8; 3] = [9, 9, 9];
println!("checksum(array) = {}", checksum(&buf));
println!("checksum(vec) = {}", checksum(&heap));
println!("checksum(array) = {}", checksum(&stack));
println!("checksum(slice) = {}", checksum(middle));

2== one function, any backing storage ==
checksum(array) = 108
checksum(vec) = 10
checksum(array) = 27
checksum(slice) = 39
3 println!("\n== &str is a slice of UTF-8 bytes ==");
let line = String::from("kalloc: 0x8000_5000");
let (tag, addr) = line.split_at(7);
println!("tag = {tag:?}");
println!("addr = {addr:?}");
println!("both point into the ONE buffer at {:p}: {:p} and {:p}",
line.as_ptr(), tag.as_ptr(), addr.as_ptr());

3== &str is a slice of UTF-8 bytes ==
tag = "kalloc:"
addr = " 0x8000_5000"
both point into the ONE buffer at 0x100f25ca0: 0x100f25ca0 and 0x100f25ca7
4 println!("\n== mutable slices write through ==");
let mut page = [0u8; 16];
fill(&mut page[..8], 0xAA);
fill(&mut page[8..], 0x55);
println!("page = {page:02x?}");

4== mutable slices write through ==
page = [aa, aa, aa, aa, aa, aa, aa, aa, 55, 55, 55, 55, 55, 55, 55, 55]
5 println!("\n== length is checked, not trusted ==");
println!("buf.get(20) = {:?} <- None, not a wild read", buf.get(20));
// buf[20] would panic with a clear message instead of reading a neighbor.
match buf.get(2..5) {
Some(s) => println!("buf.get(2..5) = {s:?}"),
None => println!("out of range"),
}

5== length is checked, not trusted ==
buf.get(20) = None <- None, not a wild read
buf.get(2..5) = [12, 13, 14]
6 println!("\n== chunks: how you walk a page table or a disk block ==");
for (i, chunk) in page.chunks(4).enumerate() {
println!(" entry {i}: {chunk:02x?}");
}
6== chunks: how you walk a page table or a disk block ==
entry 0: [aa, aa, aa, aa]
entry 1: [aa, aa, aa, aa]
entry 2: [55, 55, 55, 55]
entry 3: [55, 55, 55, 55]
}

/// Takes a *borrowed* window. Works for arrays, Vecs, and other slices.
fn checksum(bytes: &[u8]) -> u32 {
bytes.iter().map(|&b| b as u32).sum()
}

fn fill(bytes: &mut [u8], value: u8) {
for b in bytes.iter_mut() {
*b = value;
}
}
exit status 0

10 Choosing a signature: the whole decision, in one file.

choosing T / &T / &mut T · point at the mini PageAlloc with &self and &mut self

$ cargo run --bin 10_how_to_pass
//! 10 — Choosing a signature: the whole decision, in one file.▸ 10 more lines
//!
//! Does the callee need to KEEP it? -> take T (move)
//! Does it only need to READ it? -> take &T (shared)
//! Does it need to CHANGE the caller's copy? -> take &mut T (exclusive)
//! Does the caller need it back afterwards? -> take T, return T
//!
//! Default to `&T`. Reach for `T` when you mean "this is mine now" — which,
//! in a kernel, is exactly what a page allocator or a queue insert means.
//!
//! Run: cargo run --bin 10_how_to_pass

#[derive(Debug)]
struct Buffer {
bytes: Vec<u8>,
}

fn main() {
0 let mut b = Buffer { bytes: vec![1, 2, 3] };

// READ: borrow.
println!("len via &Buffer = {}", len(&b));

// CHANGE IN PLACE: exclusive borrow.
append(&mut b, 4);
println!("after append(&mut) = {:?}", b.bytes);

// KEEP IT: move. The caller is done with the value.
let handle = enqueue(b);
println!("enqueued, queue owns it now: {handle}");

// GIVE IT BACK: move in, move out.
let mut b2 = Buffer { bytes: vec![7, 7] };
b2 = zero_and_return(b2);
println!("after move-in/move-out = {:?}", b2.bytes);

0len via &Buffer = 3
after append(&mut) = [1, 2, 3, 4]
enqueued, queue owns it now: Buffer(4 bytes) parked in the queue
after move-in/move-out = [0, 0]
1 println!("\n== the same four, with the errors they prevent ==");
let owned = String::from("kernel");

// &T: the callee cannot change it and cannot free it.
let n = count_bytes(&owned);
println!("count_bytes(&owned) = {n}, owned still here: {owned:?}");

// T: after this line `owned` is not a name you can use.
let shouty = consume_and_upper(owned);
println!("consume_and_upper -> {shouty:?}");
// println!("{owned}"); // error[E0382]: borrow of moved value: `owned`

1== the same four, with the errors they prevent ==
count_bytes(&owned) = 6, owned still here: "kernel"
consume_and_upper -> "KERNEL"
2 println!("\n== a full mini-allocator, borrow-checked ==");
let mut alloc = PageAlloc { free: vec![0x8000_3000, 0x8000_2000, 0x8000_1000] };
let a = alloc.take(); // &mut self
let c = alloc.take();
println!("took {:#x} and {:#x}, {} left", a.unwrap(), c.unwrap(), alloc.len());
if let Some(p) = a {
alloc.give(p); // &mut self again
}
println!("returned one, {} left, top of list = {:#x}", alloc.len(), alloc.peek().unwrap());
2== a full mini-allocator, borrow-checked ==
took 0x80001000 and 0x80002000, 1 left
returned one, 2 left, top of list = 0x80001000
}

struct PageAlloc {
free: Vec<usize>,
}

impl PageAlloc {
/// `&self` — reads only. Many of these may coexist.
fn len(&self) -> usize {
self.free.len()
}

/// `&self` returning a borrow tied to self: the result cannot outlive the
/// allocator, and the allocator cannot be mutated while it is alive.
fn peek(&self) -> Option<&usize> {
self.free.last()
}

/// `&mut self` — exclusive. Nothing else may touch the free list here,
/// which is precisely the invariant a real kalloc() needs.
fn take(&mut self) -> Option<usize> {
self.free.pop()
}

fn give(&mut self, page: usize) {
self.free.push(page);
}
}

fn len(b: &Buffer) -> usize {
b.bytes.len()
}

fn append(b: &mut Buffer, byte: u8) {
b.bytes.push(byte);
}

fn enqueue(b: Buffer) -> String {
format!("Buffer({} bytes) parked in the queue", b.bytes.len())
} // dropped here — the caller could not have used it anyway

fn zero_and_return(mut b: Buffer) -> Buffer {
for x in b.bytes.iter_mut() {
*x = 0;
}
b
}

fn count_bytes(s: &str) -> usize {
s.len()
}

fn consume_and_upper(s: String) -> String {
s.to_uppercase()
}
exit status 0

E0106 missing lifetime specifier (and, once you add one, error[E0515]: cannot return reference to local)

missing lifetime · fix: return the value, or borrow from a parameter

$ ./show-errors.sh e0106
// error[E0106]: missing lifetime specifier
// (and, once you add one, error[E0515]: cannot return reference to local)
//
// This is C's returning-a-pointer-to-a-stack-local. Rust asks the question
// that exposes it: borrowed from WHAT? There is no input to borrow from, and
// `s` dies at the closing brace.
//
// FIX 1: return the String itself — move the ownership out.
// FIX 2: take a parameter and return a slice of it:
// fn head(s: &str) -> &str { &s[..4] }
fn dangle() -> &String {
let s = String::from("kernel");
&s
}

fn main() {
println!("{}", dangle());
}
Why, and 2 fixes e

This is C's returning-a-pointer-to-a-stack-local. Rust asks the question that exposes it: borrowed from WHAT? There is no input to borrow from, and s dies at the closing brace.

  1. return the String itself — move the ownership out.
  2. take a parameter and return a slice of it: fn head(s: &str) -> &str { &s[..4] }
$ rustc --edition 2021 --emit=metadata -o /dev/null e0106_dangling_reference.rs
error[E0106]: missing lifetime specifier
|
11 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
11 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
11 - fn dangle() -> &String {
11 + fn dangle() -> String {
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0106`.
exit status 1

E0382 borrow of moved value

use of moved value · fix: use t, clone(), or borrow

$ ./show-errors.sh e0382
// error[E0382]: borrow of moved value
//
// The use-after-free, caught at compile time. `s` gave the buffer away on the
// `let t = s;` line, so the name `s` is struck off from there on.
//
// FIX 1: use `t` instead.
// FIX 2: `let t = s.clone();` — pay for a second buffer, visibly.
// FIX 3: `let t = &s;` — borrow instead of taking.
fn main() {
let s = String::from("kernel");
let t = s;
println!("{s} {t}");
}
Why, and 3 fixes e

The use-after-free, caught at compile time. s gave the buffer away on the let t = s; line, so the name s is struck off from there on.

  1. use t instead.
  2. let t = s.clone(); — pay for a second buffer, visibly.
  3. let t = &s; — borrow instead of taking.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0382_use_after_move.rs
error[E0382]: borrow of moved value: `s`
|
10 | let s = String::from("kernel");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
11 | let t = s;
| - value moved here
12 | println!("{s} {t}");
| ^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
11 | let t = s.clone();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0382`.
exit status 1

E0499 cannot borrow v as mutable more than once at a time

two &mut at once · fix: sequence them, or split_at_mut

$ ./show-errors.sh e0499
// error[E0499]: cannot borrow `v` as mutable more than once at a time
//
// &mut means EXCLUSIVE. Two writers to the same value at the same time is the
// data race, and it is rejected even in single-threaded code.
//
// FIX 1: end the first borrow before starting the second (separate blocks, or
// just use them one after the other).
// FIX 2: for disjoint pieces of a slice, use `split_at_mut`.
fn main() {
let mut v = vec![1, 2, 3];
let a = &mut v;
let b = &mut v;
a.push(4);
b.push(5);
}
Why, and 2 fixes e

&mut means EXCLUSIVE. Two writers to the same value at the same time is the data race, and it is rejected even in single-threaded code.

  1. end the first borrow before starting the second (separate blocks, or just use them one after the other).
  2. for disjoint pieces of a slice, use split_at_mut.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0499_two_mut_borrows.rs
error[E0499]: cannot borrow `v` as mutable more than once at a time
|
11 | let a = &mut v;
| ------ first mutable borrow occurs here
12 | let b = &mut v;
| ^^^^^^ second mutable borrow occurs here
13 | a.push(4);
| - first borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0499`.
exit status 1

E0502 cannot borrow v as mutable because it is also borrowed as immutable

& and &mut overlap · fix: finish the read first; or copy the value out

$ ./show-errors.sh e0502
// error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
// immutable
//
// ALIASING XOR MUTATION. `first` points into the Vec's buffer; `push` may
// reallocate that buffer and free the old one. In C++ this compiles and
// `first` becomes a dangling pointer.
//
// FIX 1: finish with `first` before pushing (borrows end at their last use).
// FIX 2: copy what you need out: `let first = v[0];` (i32 is Copy).
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{first}");
}
Why, and 2 fixes e

ALIASING XOR MUTATION. first points into the Vec's buffer; push may reallocate that buffer and free the old one. In C++ this compiles and first becomes a dangling pointer.

  1. finish with first before pushing (borrows end at their last use).
  2. copy what you need out: let first = v[0]; (i32 is Copy).
$ rustc --edition 2021 --emit=metadata -o /dev/null e0502_shared_and_mut.rs
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
|
12 | let first = &v[0];
| - immutable borrow occurs here
13 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
14 | println!("{first}");
| ----- immutable borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0502`.
exit status 1

E0505 cannot move out of s because it is borrowed

move while borrowed · fix: use the reference before the move

$ ./show-errors.sh e0505
// error[E0505]: cannot move out of `s` because it is borrowed
//
// A borrow is a claim on the value staying put. You cannot hand the value to
// someone else while a reference to it is still alive.
//
// FIX: use `r` before the move, or borrow again after it.
fn main() {
let s = String::from("kernel");
let r = &s;
let t = s;
println!("{r} {t}");
}
Why, and the fix e

A borrow is a claim on the value staying put. You cannot hand the value to someone else while a reference to it is still alive.

  1. use r before the move, or borrow again after it.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0505_move_while_borrowed.rs
error[E0505]: cannot move out of `s` because it is borrowed
|
8 | let s = String::from("kernel");
| - binding `s` declared here
9 | let r = &s;
| -- borrow of `s` occurs here
10 | let t = s;
| ^ move out of `s` occurs here
11 | println!("{r} {t}");
| - borrow later used here
|
help: consider cloning the value if the performance cost is acceptable
|
9 | let r = &s.clone();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0505`.
exit status 1

E0507 cannot move out of index of Vec<String>

move out of a Vec index · fix: borrow, clone(), or remove()

$ ./show-errors.sh e0507
// error[E0507]: cannot move out of index of `Vec<String>`
//
// If this were allowed, the Vec would still believe it owns element 0 and
// would free it at the end of the scope — while `first` also freed it. A
// double free.
//
// FIX 1: borrow it: let first = &v[0];
// FIX 2: clone it: let first = v[0].clone();
// FIX 3: take it out for real: let first = v.remove(0); (Vec gives up the slot)
fn main() {
let v = vec![String::from("uart0"), String::from("virtio0")];
let first = v[0];
println!("{first}");
}
Why, and 3 fixes e

If this were allowed, the Vec would still believe it owns element 0 and would free it at the end of the scope — while first also freed it. A double free.

  1. borrow it: let first = &v[0];
  2. clone it: let first = v[0].clone();
  3. take it out for real: let first = v.remove(0); (Vec gives up the slot)
$ rustc --edition 2021 --emit=metadata -o /dev/null e0507_move_out_of_index.rs
error[E0507]: cannot move out of index of `Vec<String>`
|
12 | let first = v[0];
| ^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
|
help: consider borrowing here
|
12 | let first = &v[0];
| +
help: consider cloning the value if the performance cost is acceptable
|
12 | let first = v[0].clone();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0507`.
exit status 1

E0596 cannot borrow v as mutable, as it is not declared as mutable

not declared mutable · fix: add mut

$ ./show-errors.sh e0596
// error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
//
// Immutability is the default, and it is a real guarantee, not a style rule.
//
// FIX: `let mut v = ...`
fn main() {
let v = vec![1, 2, 3];
v.push(4);
println!("{v:?}");
}
Why, and the fix e

Immutability is the default, and it is a real guarantee, not a style rule.

  1. let mut v = ...
$ rustc --edition 2021 --emit=metadata -o /dev/null e0596_immutable_borrow.rs
error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
|
8 | v.push(4);
| ^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
7 | let mut v = vec![1, 2, 3];
| +++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0596`.
exit status 1