Skip to content

Latest commit

 

History

History
781 lines (549 loc) · 18.7 KB

2021-09-03_lecture1.md

File metadata and controls

781 lines (549 loc) · 18.7 KB

Reference

09/03 Lecture 1

In the first class, we should first do course introduction. it will take about 10 minutes.

Then, we will introdu the basic ideas in rust. I will follow TRPL to do this, which means I will introduce

  1. install/update rustc
  2. cargo basics
  3. variables and mutability
  4. data types
  5. comments
  6. control flow

Why Rust?

Rust Overview

"Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety."

In other words: Rust is designed for speed, safety, and concurrency.

What is "Systems Programming"?

Areas of systems programming:

  • Operating Systems
  • Device Drivers
  • Embedded Systems
  • Real Time Systems
  • Networking
  • Virtualization and Containerization
  • Scientific Computing
  • Video Games

Other systems languages include: C, C++, Ada, D.

Like C and C++, Rust gives developers fine control over the use of memory, and maintains a close relationship between the primitive operations of the language and those of the machines it runs on, helping developers anticipate the costs (time and space) of operations.

Who uses Rust?

"For five years running, Rust has taken the top spot as the most loved programming language."

Now a top 20 language in most language popularity rankings, and one of the fastest-growing: Second most used language for Advent of Code this year, after Python:

Why Rust?

  • Fast
  • Safe -> statically typed, and compile time checked for memory safety
  • Trustworthy concurrency

In particular, Rust's goals of memory safety and trustworthy concurrency are what makes it most unique. These concepts -- in particular the issues surrounding data ownership -- can be both the most surprising and the most rewarding parts of learning Rust.

Type Safety and Memory Safety

A language is said to be memory-safe if all programs written in that language have defined semantics for all possible states.

Aside: Typing

Rust is a type safe and statically typed language:

C and C++ are not type safe! Undefined Behavior is the root of all evil.

Undefined Behavior

  • buffer overruns. Your program will access elements beyond the end or before the start of an array.
int main() {
  int x = 1, *p = &x;
  int y = 0, *q = &y;
  *(q+1) = 5; // overwrites p
  return *p; // crash 
}
  • dangling pointers (uses of pointers to freed memory)

… which can happen via the stack, too:

int *foo(void) { int z = 5; return &z; }
void bar(void) {
  int *x = foo();
  *x = 5; /* oops! */
}

Heartbleed.

  • Problem: C and C++ allow for direct memory dereference, no checking. Systems languages are what we built everything on top of! Even other languages are built on top of C and C++:
    • Java Virtual Machine
    • CPython

Why then, do we not check things at runtime?

Null References -- Billion Dollar Mistake

I call it my billion-dollar mistake. It was the invention of the null reference in 1965... My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. [name=Tony Hoare]

Performance

  • Zero Cost Abstraction: In general, C++ and rust implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

Rust performs similar (even slightly better) than C++.

Concurrency

Examples of Rust Safety

No null pointer dereferences. (Not unique to Rust)

Your program will not crash because you tried to dereference a null pointer.

The problems with null

  1. Used to represent a missing value: String getValue(HashTable<String, String> t);
  2. Use to represent an error: void* malloc(size_t size)
  3. It is very easy to ignore.
// Optional Values:
enum Option<P> {
  Some(P),
  None
}

Still compiles down to pointer and null! But to use the value, one need to unwrap the value from Option<P>.

Handling Possible Errors

enum Result<T, E> {
  Ok(T),
  Err(E),
}

fn read(&mut fd: FileDescriptor, buf: &mut [u8]) -> Result<usize, std::io::Error>;

Memory Management

  • C and C++: Manual memory management.
  • Python and Java: Automatic Memory Management via a garbage collector.
    • A garbage collector traces pointers in use by the program, starting from the stack and global variables.
    • Drawback: Time critical code, performance, runtime/portability
  • Rust:

Rust install/update

To check whether you have Rust installed correctly, run this in your terminal:

$ rustc --version

you will see the version number

$ rustc 1.54.0 (a178d0322 2021-07-26)

If you have different version or you don't have rust installed:

  • If you have not installed Rust in your computer, install it through Rust-lang website.

  • If you have it installed in your computer, update it!

    $ rustup update stable

To play with Rust online in your browser, go rust-lang playground (https://play.rust-lang.org).

Hello Cargo

Cargo is Rust’s build system and package manager. Cargo comes installed with Rust if you used the official installers described above and in TRPL. Cargo is so frequently used so it has its own book! After installing Rust, You can check your Cargo version by:

$ cargo --version

Cargo integrated many useful commands to help you manage your projects. You can create, run, build, check and test your project with cargo's easy commands!

  • Creating a Project with Cargo
$ cargo new hello_cargo
$ cd hello_cargo
  • Building and Running a Cargo Project
$ cargo build
$ cargo run
  • Building for Release
$ cargo build --release
  • Check the format of your code
$ cargo fmt
  • check the syntax of your code
$ cargo clippy

build a project from GitHub

$ git clone example.org/someproject
$ cd someproject
$ cargo build

Hello VSCode

I suggest you to write your rust code in VS code with rust-analyzer extention.

Hello world!

Create a hello_world rust project using Cargo:

cargo new hello_world
cd hello_world
code .

You folder will contain the following files. No code command on mac? Here is the solution.

  • src/main.rs

  • Cargo.toml

check fmt

  • Check the format of your code
$ cargo fmt

If you add some empty line in the main(), cargo fmt will clean it for you.

clippy it!

$ cargo clippy

if you define an unused variable, for example

fn main() {
    let a = 1;
    println!("Hello, world!");
}

cargo clippy will complain.

Run it!

cargo run

Build it

cargo run

Build release

  • Building for Release
$ cargo build --release

Common Programming Concepts

Variables and Mutability

In rust, variables are defined via keyword let. By default, variables are immutable, except you specify it.

fn main() {
  let x = 37;
  let y = x + 5;
  y
} // 42

fn main() {
  let x = 37;
  x = x + 5;//err
  x
}

fn main() {//err:
  let x:u32 = -1;
  let y = x + 5;
  y
}

fn main() {
  let x = 37;
  let x = x + 5;
  x
}//42

fn main() {
  let mut x = 37;
  x = x + 5;
  x
}//42


fn main() {
  let x:i16 = -1;
  let y:i16 = x+5;
  y
}//4

Differences Between Variables and Constants

#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}

  1. constants are defined using the const keyword.
  2. You are not allowed to mut constants.
  3. You must annotate the type of constants.
  4. Constants are valid for the entire time a program runs, within the scope they were declared in.
  5. constants cannot be set as the result of a functional call or any other value that could only be computed at runtime.

Shadowing

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

The result will be 12.

Shadowing is useful if

  1. a value needs a few modifications in the whole program
  2. we want to change the type of the value but reuse the same name.
    let spaces = "   ";
    let spaces = spaces.len();

but rust will complain if we do

 let mut spaces = "   ";
    spaces = spaces.len();

because we want to change the value of the variable as well.

Data Type

Scalar Types

A scalar type represents a single value. Rust has four primary scalar types:

1. integers

The isize and usize types depend on the kind of computer your program is running on: 64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit architecture.

You can write integer literals in any of the forms below. Note that all number literals except the byte literal allow a type suffix, such as 57u8, and _ as a visual separator, such as 1_000.

2. floating-point numbers
fn main() {
    let x = 2.0; // f64, default

    let y: f32 = 3.0; // f32
}
Numeric Operations
fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;

    // remainder
    let remainder = 43 % 5;
}
  1. Booleans
fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

The main way to use Boolean values is through conditionals, such as an if expression.

  1. characters
fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

Rust’s char type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII. Accented letters; Chinese, Japanese, and Korean characters; emoji; and zero-width spaces are all valid char values in Rust. Unicode Scalar Values range from U+0000 to U+D7FF and U+E000 to U+10FFFF inclusive. However, a “character” isn’t really a concept in Unicode, so your human intuition for what a “character” is may not match up with what a char is in Rust.

Compound Types

Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

The Tuple Type

A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size. Tuple will be allocated on the stack.

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

destructuring

In addition to destructuring through pattern matching, we can access a tuple element directly by using a period (.) followed by the index of the value we want to access. For example:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}

This program creates a tuple, x, and then makes new variables for each element by using their respective indices. As with most programming languages, the first index in a tuple is 0.

The Array Type

Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. Arrays in Rust are different from arrays in some other languages because arrays in Rust have a fixed length, like tuples.

  1. Creating an array

    fn main() {
        let a: [i32; 5] = [1, 2, 3, 4, 5];
    }
    

    Here, i32 is the type of each element. After the semicolon, the number 5 indicates the array contains five elements.

        let a = [3; 5];
    

    The array named a will contain 5 elements that will all be set to the value 3 initially. This is the same as writing let a = [3, 3, 3, 3, 3]; but in a more concise way.

  2. Accessing array elements An array is a single chunk of memory allocated on the stack. You can access elements of an array using indexing, like this:

    pub fn array() {
        // Size is hardcoded for arrays.
        // There space is allocated directly in the binary.
        let a = [1, 2, 3];
        let zeroes = [0; 1000];
        let b: [i32; 3] = [1, 2, 3];
    
        // Typical example
        let input_files = ["input/input_1.txt", "input/input_2.txt"];
    
        let first = a[0];
        let second = a[1];
        // println!("{:?}", a + a);
        // a.push(3);
    }
    
  3. Buffer overflow? NEVER. Rust checks the bounds at both compile time and run time. You will get an OOB error.

Statements and Expressions

  • Statements are instructions that perform some action and **do not return a value. **
  • Expressions evaluate to a resulting value.
// function body
fn main() {
    let x = 5; // statement

    let y = {
        let x = 3; // statement
        x + 1      // expression
    }; // statement

    println!("The value of y is: {}", y); // statement
}

In the function body, this block is an expression.

{
    let x = 3; // statement
    x + 1      // expression
}

Comments

In each line, everything after a double reversed slash // can be used to mark comments.

 // I am a comment.
a  = 5; // I am also a comment.

Before a function definition, a triple reversed slash /// can be used to mark the description of a function.

/// I am the description of function `a_function()`.
fn a_function() {}

Control Flow

if

fn main() {
    let number = 6;

    let sign = if number < 0 {
        -1
    } else if number == 0 {
        0
    } else {
        1
    };
}

loops

loop

The loop keyword tells Rust to execute a block of code over and over again forever or until you explicitly tell it to stop.

fn main() {
    loop {
        println!("again!");
    }
}
while
fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    
    println!("LIFTOFF!!!");
}
for
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Rust provides a way to iterate over a collection

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

If you know exactly which elements you want to iterate, used Range:

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in (1..4).rev() {
        println!("the value is: {}", element);
    }
}

Testing

In any language, there is the need to test code.

In most languages, testing requires extra libraries:

  • Minitest in Ruby
  • Ounit in Ocaml
  • Junit in Java

Testing in Rust is a first-class citizen! The testing framework is built into cargo.

Unit testing

Unit testing is for local or private functions Put such tests in the same file as your code

  • Use assert! to test that something is true
  • -Use assert_eq! to test that two things that implement the PartialEq trait are equal.
    • E.g., integers, booleans, etc.
fn bad_add(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_bad_add () {
        assert_eq!(bad_add(1,3),3);
    }
}

Integration testing

Integration testing is for APIs and whole programs (This is how we grade your projects).

  • Create a tests directory
  • Create different files for testing major functionality
  • Files don’t need #[cfg(test)] or a special module
    • But they do still need #[test] around each function
  • Tests refer to code as if it were an external library
    • Declare it as an external library using extern crate
    • Include the functionality you want to test with use

src/lib.rs

pub fn add(a: i32, b: i32) -> {
    a + b
}

tests/test_add.rs

extern crate your_project_name // this will tell rust you have an external source
use your_project_name::add; // this will tell rust you will use add() function from the extern source

#[test]
pub fn test_add () {
    assert_eq!(add(1,2),3);
}

#[test] 
pub fn test_negative_add() {
    assert_eq!(add(1,-2),-1);
}

Please fill out this survey!