Rung 2: Ownership and borrowing

Moves, &, &mut, slices and String vs &str. How the codec reads an entry without copying it.

This is the idea that makes Rust Rust. Every value has one owner. Others may borrow it: many may read at once, or one may write, never both. The compiler checks this before the program runs. It is why Gestalt's codec can read an entry straight out of the bytes it was handed, with no copies and no memory bugs.

The idea #

One owner, and moves #

let line = vec![0xa7, 0x00];
let kept = line;           // the Vec moves: `kept` owns it now
println!("{:?}", line);    // refused: `line` was moved

Passing a value to a function moves it too. In rung 1, to_hex(bytes: Vec<u8>) consumed your bytes: after calling it, they were gone. When the owner goes out of scope, the value is dropped and its memory freed. There is no garbage collector and no free to forget.

Small values like u64, u8 and bool are Copy: assigning them copies the number, and nothing moves.

Borrowing: & and &mut #

fn to_hex(bytes: &[u8]) -> String { ... }      // borrows: reads, gives back
fn append_line(log: &mut Vec<Vec<u8>>, line: &[u8]) { ... }  // may change `log`
WriteMeansHow many at once
Towned: moves in, the function decides its fateone owner
&Tshared borrow: read onlyas many as you like
&mut Tmutable borrow: read and writeexactly one, and no &T meanwhile

The rule is one writer or many readers, never both. That rules out a whole class of bugs. You can't have a reference to a Vec's first byte while a push moves its memory somewhere else.

Slices: views, not copies #

&[u8] is a slice: a pointer and a length, viewing bytes someone else owns. A &Vec<u8>, an array &[1, 2, 3] and a part of either &v[1..4] can all be passed as &[u8]. Taking a slice of a slice copies nothing.

&str is the same for text: a view into UTF-8 bytes. String owns its text and can grow. The habit is to take &str and &[u8] in, and give String and Vec<u8> out when you made something new.

s.len() counts bytes, not letters: "ü".len() is 2.

Copy only when you must keep it #

.to_vec() and .to_string() make owned copies. They are right when you need to keep data after the lender is done, like a log keeping a line. Otherwise borrow. .clone() to quiet the compiler is usually the wrong fix.

Lifetimes: how long a borrow lives #

struct Reader<'a> {
    bytes: &'a [u8],
    at: usize,
}

'a is a name for "as long as the bytes live". It says the reader can't outlive the bytes. A method returning &'a [u8] hands out slices that live as long as the bytes, not just as long as the reader. Most of the time Rust works lifetimes out by itself. You write them when a struct holds a borrow, as here.

Why Gestalt cares #

  • No copies on the hot path. A node reads entries all day. The decoder borrows the input and hands out slices of it, so reading an entry does not copy it.
  • Memory safety is proven, not hoped for. Because the compiler checks ownership and borrowing, the kernel crates can forbid(unsafe_code). Any memory bug would need unsafe, and the purity gate refuses it. The checker is the compiler, every build.
  • One writer or many readers. Rust's rule has the same shape as Gestalt's: one head orders a record's writes, while any number of copies read it.

In Gestalt's code #

encode borrows the value (&Value): it reads it and never takes it. It owns a fresh Vec, lends it to write as &mut, and hands it back. write calls itself for nested values, and they all append into the one buffer. That is exercises 5 and 6.

From engine/kernel/codec/src/cbor.rs:302:

pub fn encode(v: &Value) -> Vec<u8> {
    let mut out = Vec::new();
    write(v, &mut out);
    out
}

/// Append the canonical bytes of `v` to `out`.
pub fn write(v: &Value, out: &mut Vec<u8>) {
    match v {
        Value::U(n) => head(out, 0, *n),
        Value::T(s) => {
            head(out, 3, s.len() as u64);
            out.extend_from_slice(s.as_bytes());
        }
        // ... the other kinds

The reader is exercise 7. It is a borrowed slice and a position, and take hands out slices of the input that live as long as the input ('a). Rung 3 explains the Result in its return type.

From engine/kernel/codec/src/cbor.rs:350:

struct Reader<'a> {
    b: &'a [u8],
    at: usize,
}

impl<'a> Reader<'a> {
    fn take(&mut self, n: u64) -> Result<&'a [u8], CborError> {
        if n > self.left() as u64 {
            return Err(CborError::Truncated);
        }
        let s = &self.b[self.at..self.at + n as usize];
        self.at += n as usize;
        Ok(s)
    }

Hashing only reads, so it borrows. Every digest in Gestalt is made by this function, and none of them copies the bytes it hashes.

From engine/kernel/codec/src/hash.rs:114:

pub fn digest(d: Domain, bytes: &[u8]) -> Digest {
    let mut h = Sha256::new();
    h.update(DOMAIN_PREFIX.as_bytes());
    h.update(d.name().as_bytes());
    h.update([0u8]);
    h.update(bytes);
    Digest(h.finalize().into())
}

Exercise #

cd ~/Documents/gestalt-learn
./ladder 2

Open rungs/02-ownership/src/lib.rs. The signatures are written for you, because they are the lesson: read each one before you write the body. Eight exercises end in your own Reader reading a CBOR text string without copying a byte. The tests check that with std::ptr::eq: your result must point into the input.

Then three drills, each refused by the compiler until you fix it:

./ladder drill 2 moved
./ladder drill 2 reader_and_writer
./ladder drill 2 dangling

Read the compiler's message each time. Rust's ownership errors explain themselves well. Learning to read them is half of this rung.

Stuck? rungs/02-ownership/solution/ has one way to do it, and ./ladder solution 2 runs the tests against it.

Check yourself #

  • What happens to a Vec you pass by value, and to a u64?
  • Why can't you push to a Vec while holding &v[0]?
  • Why does take return &'a [u8], not a slice tied to the reader?
  • Why can the kernel forbid unsafe code?

Sign in to mark your progress

Sign in or create a learner to mark the lesson and the exercise, and to review this rung's cards.

The cards (14)

Try each one before you open its answer. Review them on a schedule from the Review page.

  1. You pass a Vec<u8> to a function by value. What happens to it?

    Answer

    It moves: the function owns it now, and the caller can no longer use that name. When the new owner is done, the Vec is dropped and its memory freed.

  2. Why doesn't passing a u64 by value move it away?

    Answer

    u64 is Copy: assigning or passing it copies the number. Small plain values (u8, u64, bool, char) are Copy; a Vec or String is not.

  3. &T and &mut T: what may each do, and how many may exist at once?

    Answer

    &T only reads, and there may be any number. &mut T reads and writes, and there must be exactly one, with no &T alive at the same time.

  4. Why does Rust refuse a push while you hold &v[0]?

    Answer

    To grow, a push may move the Vec's bytes elsewhere. Your reference would then point at freed memory. One writer or many readers, never both.

  5. &[u8] or Vec<u8> for a function that only reads bytes?

    Answer

    &[u8]: a view (pointer plus length) into bytes someone else owns. A Vec, an array or part of either can be passed, and nothing is copied.

    See engine/kernel/codec/src/hash.rs:114

  6. String and &str: which goes in, which comes out?

    Answer

    Take &str for text you only read; return String for text you made. A String owns growable UTF-8; a &str is a view into UTF-8 bytes.

  7. What is "ü".len(), and why does it matter for CBOR?

    Answer

    2: len counts UTF-8 bytes, not letters. A CBOR text string's head carries its length in bytes, so s.len() is exactly right.

  8. When is .to_vec() the right call?

    Answer

    When you must keep the bytes after the lender is done, like a log keeping a line. Otherwise borrow. A .clone() added just to quiet the compiler is usually the wrong fix.

  9. What does 'a say in struct Reader<'a> { b: &'a [u8], at: usize }?

    Answer

    The reader holds a borrow of bytes that live for 'a, so the reader can never outlive those bytes.

    See engine/kernel/codec/src/cbor.rs:350

  10. Why does the reader's take return &'a [u8] rather than a slice tied to &mut self?

    Answer

    So the slice lives as long as the input, not as long as the reader's borrow. You can keep earlier slices while reading on, and keep them after the reader is gone.

    See engine/kernel/codec/src/cbor.rs:373

  11. Why does encode lend one &mut Vec<u8> to write instead of each nested value returning its own Vec?

    Answer

    Every nested value appends into the one buffer: no copies and no joining of pieces, just one growing allocation for the whole item.

    See engine/kernel/codec/src/cbor.rs:302

  12. Why can Gestalt's kernel crates forbid(unsafe_code)?

    Answer

    Ownership and borrowing let the compiler prove memory safety. With no unsafe, no memory bug can get past it. The purity gate checks that every kernel crate forbids it.

    See engine/README.md

  13. Can a function return a reference to a variable it created?

    Answer

    No. The variable is dropped when the function returns, so the reference would dangle. Return the owned value instead: ownership moves to the caller, and nothing is copied.

  14. Rust's rule for references has the same shape as which Gestalt rule?

    Answer

    One writer or many readers. A record has one head that orders its writes, while any number of copies read it.