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
- install/update
rustc
- cargo basics
- variables and mutability
- data types
- comments
- control flow
"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.
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.
"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:
- 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.
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.
- 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! */
}
- 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?
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]
- 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 is hard: https://en.wikipedia.org/wiki/Therac-25
-
No data race in rust (achieved by ownership)
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
- Used to represent a missing value:
String getValue(HashTable<String, String> t)
; - Use to represent an error:
void* malloc(size_t size)
- 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>
.
enum Result<T, E> {
Ok(T),
Err(E),
}
fn read(&mut fd: FileDescriptor, buf: &mut [u8]) -> Result<usize, std::io::Error>;
- 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:
- Reference counting + Non-Lexical Lifetimes
- Fast, safe and smart.
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).
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
$ git clone example.org/someproject
$ cd someproject
$ cargo build
I suggest you to write your rust code in VS code with rust-analyzer
extention.
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.
- Check the format of your code
$ cargo fmt
If you add some empty line in the main()
, cargo fmt
will clean it for you.
$ cargo clippy
if you define an unused variable, for example
fn main() {
let a = 1;
println!("Hello, world!");
}
cargo clippy
will complain.
cargo run
cargo run
- Building for Release
$ cargo build --release
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
#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}
- constants are defined using the
const
keyword. - You are not allowed to
mut
constants. - You must annotate the type of constants.
- Constants are valid for the entire time a program runs, within the scope they were declared in.
- constants cannot be set as the result of a functional call or any other value that could only be computed at runtime.
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
- a value needs a few modifications in the whole program
- 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.
A scalar type represents a single value. Rust has four primary scalar types:
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.
fn main() {
let x = 2.0; // f64, default
let y: f32 = 3.0; // f32
}
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;
}
- 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.
- 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 can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
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.
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.
-
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 number5
indicates the array contains five elements.let a = [3; 5];
The array named
a
will contain5
elements that will all be set to the value3
initially. This is the same as writinglet a = [3, 3, 3, 3, 3]
; but in a more concise way. -
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); }
-
Buffer overflow? NEVER. Rust checks the bounds at both compile time and run time. You will get an OOB error.
- 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
}
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() {}
fn main() {
let number = 6;
let sign = if number < 0 {
-1
} else if number == 0 {
0
} else {
1
};
}
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!");
}
}
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
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);
}
}
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 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 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);
}