Learning Rust - Lifetimes and Scope

Learning Rust - Lifetimes and Scope

I've been livestreaming malware development in Rust several times per week over the last two weeks over on YouTube and Twitch. There was a bit of consternation about me switching over to Rust early on: a lot of people really wanted to see me stick it out with C/C++, and I explained why I switched to Rust in the latest stream. That's a topic for another blog, though, and I am starting to piece together some of my thoughts on the matter as I learn more about Rust.

Instead, we're going to talk about one of the quirks to Rust that makes it much more safe and efficient than out-of-the-box C/C++.

The way Rust deals with scope is essentially the reason why it's considered significantly more efficient and memory safe compared to it's decades-old counterparts in C/C++. It doesn't have a garbage collector, and also doesn't depend on the developer to manage memory manually.

Rust essentially frees memory allocated for local variables as soon as it runs out of scope. It also passes ownership of variables to calling functions that take those variables as input. This is best exemplified using the code below.


pub fn taker(inp : String){
    println!("Have ownership of {}!", inp);
}

pub fn borrower(inp : &String){
    println!("Borrowed {}!",inp);
}

pub fn give_back(inp : String) -> String {
    println!("We took {} but we're giving it back!",inp);
    inp
}


fn main() {
    let starter = String::from("initial_var");
    println!("Out initial variable is {} and it's owned by the main function!", starter);
    let given_back = give_back(starter);
    println!("We got back {} and now the main function owns it!", given_back);

    let ptr : &String = &String::from("pointer_var");
    borrower(ptr);
    println!("We still have ownership of {} in main!",ptr);


}

If we run the code above, we get the following:

Out initial variable is initial_var and it's owned by the main function!
We took initial_var but we're giving it back!
We got back initial_var and now the main function owns it!
Borrowed pointer_var!
We still have ownership of pointer_var in main!

Typos aside, this is what we expected. Let's walk through the code and explain what's going on.

The simplest implementation that we're probably most used to seeing is give_back. This function will print out the input and then return the input to be stored in the given_back variable.

Next we take a look at borrower(). Borrower takes a pointer to the string variable in question and prints it out. As an aside, one of the things I love about Rust is that it infers that I want to dereference the pointer, without me having to use any explicit symbols to dereference it.

Where we can get into trouble is with the taker() function. Let's add a function call for taker into our code.

pub fn taker(inp : String){
    println!("Have ownership of {}!", inp);
}

pub fn borrower(inp : &String){
    println!("Borrowed {}!",inp);
}

pub fn give_back(inp : String) -> String {
    println!("We took {} but we're giving it back!",inp);
    inp
}


fn main() {
    let starter = String::from("initial_var");
    println!("Out initial variable is {} and it's owned by the main function!", starter);
    let given_back = give_back(starter);
    println!("We got back {} and now the main function owns it!", given_back);

    let ptr : &String = &String::from("pointer_var");
    borrower(ptr);
    println!("We still have ownership of {} in main!",ptr);
    
    let taken = String::from("taken_var");
    taker(taken);
    println!("This line of code won't work, because {} entered a dead scope...", taken);

}

When we try to run this code, we get the following:

Compiling blog_scope v0.1.0 (C:\Users\mgedw\Documents\RE_Stuff\rust_learning\blog_scope)
error[E0382]: borrow of moved value: `taken`
  --> src\main.rs:28:82
   |
26 |     let taken = String::from("taken_var");
   |         ----- move occurs because `taken` has type `String`, which does not implement the `Copy` trait
27 |     taker(taken);
   |           ----- value moved here
28 |     println!("This line of code won't work, because {} entered a dead scope...", taken);
   |                                                                                  ^^^^^ 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)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `blog_scope` due to previous error

Here's why this doesn't work. When we pass taken to the taker() function, it is now owned by the taker() function... meaning when the taker() function ends, taken, which is not copied but instead is the literal block of memory we allocated in the main() function, goes out of scope and is thus destroyed.

The reason why the borrower() function works is that instead of passing the block of memory, we pass a reference (or a pointer) to that block of memory. References also have a whole slew of rules that I'll go over in a later blog.

The point being is that a given block of memory denoted by a variable-length variable (like a string) is not copied by default, so when you pass said block of memory to another function, it is now owned by that function, and when that function scope ends, that block of memory is essentially freed and is no longer accessible in the caller function or any other one.

This may seem annoying, and it's certainly a quirk that is taking some getting used to, but it's a hell of a lot better than managing memory yourself, or passing off that functionality to a garbage collector at a performance cost.

If you want to learn more about scope from someone who knows far more about it that I do, check out Let's Get Rusty's video on ownership.