BYU logo Computer Science
CS 465 Introduction to Security and Privacy

Learning Rust 1

Introductions

  • introductions of two students

Learning Rust, Sections 1 - 3

We are using The Rust Programming Language and rustlings.

Rust is all about memory safety.

Cargo

Rust comes with cargo to build code and manage dependencies:

  • cargo new PROJECT
  • cargo build
  • cargo build --release
  • cargo run
  • cargo check

Mutability

You must declare variables to be mutable. This doesn’t work:

fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}

This does:

fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}

Shadowing

You can shadow a variable by redeclaring it. Curly brackets create a new scope, and variable declarations go away when they are out of scope. So the below code prints 12 for the inner scope and 6 for the outer scope.

fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}

Namespaces

The :: accesses elements of a namespace. std is the Rust standard libary, and is always available through a use statement. std::io is the io module of the standard lbrary. The syntax io::stdin() accesses standard input in the io module.

use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}

Data types and integer overflow

Rust will infer data types where possible, but you must explicitly use type annotation where Rust cannot infer. In the below example, the guess variables needs a type annotation because parse() doesn’t know what type it should be converting to.

let guess: u32 = "42".parse().expect("Not a number!");

Rust has:

  • 8-bit to 128-bit signed and unsigned integers, e.g. i16, u32
  • floating-point types, e.g. f32, f64
  • boolean type, bool, which can be true or false
  • character type, char

Rust also has

  • tuples
  • arrays

For integer overflow, Rust provides wrappers for:

  • wrapping_ — wraps
  • checked_ — returns None if overflow
  • overflowing — returns the value and True if overflow
  • saturating — caps at max or min value

Return values

Rust will return the value of the last expression (leave off the semicolon!):

fn plus_one(x: i32) -> i32 {
x + 1
}

You can also use an explicit return.

Exercises

At this point, you are ready to complete exercises for sections 0-3, plus the first quiz.

Learning Rust, Section 4

Ownership

To preserve memory safety, Rust uses a set of ownership rules.

Each value in Rust has an owner.

There can only be one owner at a time.

When the owner goes out of scope, the value will be dropped.

This is OK, because the variables are on the stack:

let x = 5;
let y = x;

There are two variables, and each have the value 5.

This is not OK, because Rust does not want two references to the same memory on the heap:

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

When s1 and s2 go out of scope, Rust would try to free the memory twice, resulting in a double free error.

So, to solve this problem, Rust declares that s1 is no longer in scope. Thus only s2 is valid.

If your code really needs two pointers to two copies of the same string, use clone():

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

Ownership and functions

When a value is passed to a function, its ownership is moved into the function, unless the type is one that can be copied. Types that can be copied: integers, floats, booleans (true and false), and char.

fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.

Return values and scope

Rust functions can take ownership and return ownership.

fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}

Notice how you do not need to call a function to free memory! Scoping rules take care of it for you.

References

Use a reference when you want to allow a function to use a variable without claiming ownership of it. This is called borrowing.

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()
}

A reference is like a pointer, but guaranteed to be valid.

A function cannot modify a reference:

fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}

You can make a reference mutable:

fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}

If you have a mutable reference to a value, you can have no other references to that value.

So this is not allowed:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

This prevents data races! The Rust book has a good definition of when data races occur:

  • Two or more pointers access the same data at the same time.
  • At least one of the pointers is being used to write to the data.
  • There is no mechanism being used to synchronize access to the data.

Rust will refuse to compile code with data races.

At any given time, you can have either one mutable reference or any number of immutable references.

References must always be valid.

Exercises

At this point, you are ready to complete exercises for section 4, on primitive types and section 6, on move semantics.

Learning Rust, Section 5

Structs

Structs let you define custom types:

struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

Here is how you initialize a struct:

fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}

See the book for additional details.

You can also define methods on structs using impl:

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}

You can also define associated functions on structs. These are functions that don’t take a struct, but return one.

impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}

Exerises

Complete exerises for section 7, on structs.