kota's memex

Ownership is rusts most unique feature. It enables rust to make memory safety guarantees without needing a garbage collector.

rules

allocation and drop trait

Rust has a String type which will create a mutable string by holding a pointer to a location on the heap as well as a length and capacity.

When the scope the string was declared in exits rust automatically calls a special function drop which is implemented by String to clean up the memory on the heap.

copy trait

For stack-only types which have not implemented drop we can instead implement the copy trait which documents that our type can be copied on the stack without needing to perform a deep copy.

double free

In non-GC languages freeing the same memory twice can lead to memory corruption and as a result security vulnerabilities.

let s1 = String::from("hello")
let s2 = s1;

println!("{s1}"); // compile error!

The above code will not compile and the reason why reveals the way rust prevents double free bugs. Remember that s1 does not itself contain the string's data, but rather is just a length, capacity and pointer to an address on the heap.

When we copy s1 into s2 rust considers s1 as no longer valid. Remember each value in rust can only have one owner.

deep copy

If we did want to instead make an actual deep copy we would just use the method .clone():

let s1 = String::from("hello")
let s2 = s1.clone();

println!("{s1}"); // works!

references and borrowing

references When passing a heap allocated type to a function you are moving it to that function. You can move it back as a return type, but this is often a bit too much ceremony and references are used instead.

borrowing

A reference is like a pointer in that it's an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

This process, of creating a reference, is called borrowing. When you borrow something you give it back when you're finished. Passing a reference does not change ownership and as a result means the value cannot be modified!

fn main() {
    let s = String::from("hello");

    change(&s); // This does not compile!
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

mutable references

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

If we declare s as mut and update the function to accept a mutable reference we can pass in a mutable value to the function. This also makes it very clear the function with mutate the value it borrows.

only one mutable reference

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. The following code results in a compile error:

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:

Multiple mutable references can exist so long as they are not inside the same scope:

let mut s = String::from("hello");

{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

only mutable OR immutable references

Similarly to creating multiple mutable references in a single scope, you also cannot created immutable AND mutable references:

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM! COMPILE ERROR!

println!("{}, {}, and {}", r1, r2, r3);

Users of an immutable reference don’t expect the value to suddenly change out from under them!

However, an immutable reference's scope ends when it is last used, as a result the following is allowed because the immutable and mutable variables don't exist in the same scope:

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{r3}");

dereference

The opposite of referencing by using & is dereferencing, which is accomplished with the dereference operator *.

dangling references

Rust prevents dangling references (dangling pointers in languages with pointers):

fn main() {
    // THIS CAUSES A COMPILE ERROR!
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello"); // s is a new String

    &s
    // we return a reference to the String, s
    // However, s goes out of scope, and is dropped. Its memory goes away.
}

Because s is created inside dangle, when the code of dangle is finished, s will be deallocated. But we tried to return a reference to it. That means this reference would be pointing to an invalid String.

The solution here would be to just return the String directly (rather than a reference to the string).