Rust in 30 Minutes
This documentation is intended to give a very very very quick overview of the Rust programming language.
References
- Documenation
- RustDoc
- Book: The Rust Programming Language
- Book: Asynchronous Programming in Rust
- Book: Programming Rust – Fast, Safe, System Development
- Book: Rust by Example
- Slides: Rethinking Systems Programming
Table of contents
- Basic
- Statements and Expressions
- Data Structure
- Lambda
- Scope and Package
- Generic Type
- Traits
- Ownership
- Testing
- I/O
- Thread
- Foreign Function Interface
Basic
const MAX_POINTS: u32 = 100_000;
fn main() {
let x = 10;
println!("The value of x is: {}", x);
eprintln!("Application error");
}
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]; // [3, 3, 3, 3, 3]
// Criticism: You can not create an array that has mutable element and unmutable pointer,
// or an array that has unmutable element and mutable pointer.
// In a word, the `mut` modifier makes no sense at all when defining an array.
let v1 = vec![1, 2, 3];
process::exit(1);
Primitive types:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
single | f32 | |
double | f64 | |
bool | ||
Unicode | char |
Slice:
let mut s = String::from("hello world");
let slice : &str = &s[..];
--------
let a; // we can initialize `a` later
let a = [1, 2, 3, 4, 5];
let slice : &[i32] = &a[1..3];
Print:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
// or
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}
let origin = Point { x: 0, y: 0 };
println!("The origin is: {:?}", origin); // The origin is: Point { x: 0, y: 0 }
println!("The origin is: {:#?}", origin); // pretty-print
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}
Statements and Expressions
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
_ => println!("Too big! or Equal!"),
}
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
loop { /* do something */ break; }
while number != 0 { /* do something */ }
for element in (1..4).rev() { /* do something */ }
for (i, &item) in bytes.iter().enumerate() { /* do something */ }
let number = if condition { 5 } else { 6 };
if let Some(3) = some_u8_value { /* do something */ }
match x {
1..=5 => println!("one through five"),
6 | 7 => println!("six or seven"),
_ => println!("something else"),
// 4 | 5 | 6 if y => ...
// 4 | 5 | (6 if y) => ... // different from the former
}
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
},
}
// Using @ to bind to a value in a pattern while also testing it:
match msg {
Message::Hello { id: id_variable @ 3..=7 } => {
/* do something with id_variable */ }
Message::Hello { id: 10..=12 } => {
/* do something with id */ }
Message::Hello { id } => {},
}
Data Structure
struct User {
username: String,
active: bool,
}
let user1 = User {
email: String::from("someone@example.com"),
sign_in_count: 1,
};
// Criticism: `mut` modifier (see the aforementioned comments on array)
let user2 = User {
email: String::from("another@example.com"),
..user1
};
impl User {
// constructor
fn new(args: &[String]) -> User { /* ... */ }
// method
fn isActive(&self) -> bool {
self.active
}
}
// You can have **multiple** impl Blocks
Tuple:
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
let r = black.0;
Enumeration:
enum Option<T> {
Some(T),
None,
}
let absent_number: Option<i32> = None;
let some_string = Some("a string");
match x {
Some(i) => Some(i + 1),
_ => None,
}
Lambda
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
Closures is not of the generic type.
Closures can capture environment variables in three way, each of which corresponds to a ownership transfer way. Rust will infer which traits to use.
Fn
borrows values immutably. IfFn
is implemented,FnMut
andFnOnce
are also implemented.FnMut
borrows values mutably. IfFnMut
is implemented,FnOnce
is also implemented.FnOnce
takes the ownership. TheOnce
part of the name indicates that the closure can’t take ownership of the same variables more than once, so it can be constructed only once. All closures implementFnOnce
.
Use the move
keyword before the parameter list to force the closure to take the ownership.
Example: thunk (a kind of lazy evaluation)
impl<T> Cacher<T>
where T: Fn(u32) -> u32 {
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}
Scope and Package
- Modules let us organize code within a crate (see below) into groups for readability and easy reuse. Use
use std::io;
to bring a module or a function / struct / enum /… in a module into scope. Useuse ... as ...
to create alias anduse ...::*
to bring in everything in a module. - A crate is a binary or library.
- The crate root is a source file that the Rust compiler starts from and makes up the root module of your crate.
- A package is one or more crates that provide a set of functionality. A package contains a
Cargo.toml
file, which tells all the external packages.
Modules can be nested:
mod front_of_house {
mod hosting {
...
}
}
By default, all structs, enums, functions, and methods as well as modules are private. We need pub
modifier to declare them as public.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() { super::serve_order(); }
}
pub fn serve_order() {}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Sometimes, we may want to seperate the modules into different files:
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
We can edit the file Cargo.toml
to add a crate to the package, then run cargo update
to download and compile it:
- tool
cargo
: Rust’s package manager and build system - file
Cargo.toml
: configuration file used bycargo
[dependencies]
rand = "0.6.0"
iced = { path = "../iced" } // or local crate
Set up a workspace:
$ cargo new [name]
$ cargo new [name] --lib # library project
$ cd [name]
$ cargo run
$ cargo run --package [packet name]
Split your program into a main.rs
and a lib.rs
and move your program’s logic to lib.rs
.
Generic Type
Example:
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
Iterator
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
let v1: Vec<i32> = vec![1, 2, 3];
// use collect() to collect the values from iterators
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
Traits
As Generic Type
Traits can be a generic type other than what is like template
in C++.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
// trait grammar:
fn notify(item: impl Summary + Display) {}
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {}
fn returns_summarizable() -> impl Summary {}
// automatically generate the trait implementation
#[derive(Debug, Clone, Copy)]
pub enum Message {
IncrementPressed,
DecrementPressed,
}
Dynamic Dispatch
Inheritance has recently fallen out of favor as a programming design solution in many programming languages because it’s often at risk of sharing more code than necessary. For these reasons, Rust takes a different approach, using trait objects instead of inheritance. Let’s look at how trait objects enable polymorphism in Rust.
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw(); // dynamic dispatch!!!!
}
}
}
Here, dyn Draw
is called trait object. A trait object points to both an instance of a type implementing our specified trait as well as a table used to look up trait methods on that type at runtime. You can only make object-safe traits into trait objects. A trait is object safe if all the methods defined in the trait have the following properties. Otherwise, there is no way to infer the concrete type at compile-time.
- The return type isn’t
Self
. - There are no generic type parameters.
Ownership
Rule 1: use &
to represent borrowing (i.e., not having the ownership)
fn foo(s: &String)
,foo(&s)
: use reference semantics (also called borrowing)fn foo(s: &mut String)
,foo(&mut s)
: mutable reference semantics. You can have only one mutable reference to a particular piece of data in a particular scope so that Rust can prevent data races at compile time. A similar rule exists for combining mutable and immutable references.- by default:
- if the type is not of
Copy
trait, use the move semantics - use copy semantics
- if the type is not of
Rust alleges that it does not need GC to manage the memory. However, this comes at a very high cost; you must write some counter-intuitive code to elate the compiler. Although Rust argues that this coding style saves programming from some subtle bugs, but it does not worth the time spent on fighting with the compiler.
Use std::mem::drop
to drop the ownership (need to implement the Drop
trait, which is similar to the deconstructor):
drop(sth);
Next, we introduce std::cell::RefCell
. With references and Box<T>
(will be discussed soon), the borrowing rules’ invariants are enforced at compile time. With RefCell<T>
, these invariants are enforced at runtime. RefCell<T>
lets us have many immutable borrows (borrow() -> Ref
) or one mutable borrow (borrow_mut() -> RefMut
) at any point in time (use *
to dereference them). If the program violates this at runtime, the program will panic.
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut borrow = self.sent_messages.borrow_mut();
borrow.push(String::from(message));
}
}
Rule 2: Rust associates each reference or variable with a lifetime. The variable must outlives any references that reference to that variable. (Here, the term “variable” means a variable holding the ownership)
In function signatures, sometimes we need the lifetime annotation to specify the lifetime for a reference. If two or more references are annotated with the save lifetime annotation, the acutal lifetime scope is the overlaps with the scope of all annotated variables.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes. The compiler uses three rules to figure out what lifetimes references have when there aren’t explicit annotations (If the compiler gets to the end of the three rules and there are still references for which it can’t figure out lifetimes, the compiler will stop with an error):
- each parameter that is a reference gets its own lifetime parameter
- if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- if there are multiple input lifetime parameters, but one of them is
&self
or&mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters.
One special lifetime is 'static
, which means that this reference can live for the entire duration of the program.
Pointer
Using Box<T>
to Point to Data on the Heap. Let’s see an example where pointers may be helpful.
enum List {
Cons(i32, List), // error: recursive without indirection
Nil,
}
The type List
is a recursive type, where a value can have as part of itself another value of the same type. At compile time, Rust needs to know how much space a type takes up. As a recursive type size is unknown, the compiler will complain. We can fix it by Box
(also known as indirection).
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
Implementing the std::ops::Deref
trait allows you to customize the behavior of the dereference operator, *
. Also, you can use the DerefMut
trait to override the *
operator on mutable references. There are three cases:
- From
&T
to&U
whenT: Deref<Target=U>
- From
&mut T
to&mut U
whenT: DerefMut<Target=U>
- From
&mut T
to&U
whenT: Deref<Target=U>
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// suger:
// `*y` now acutally is `*(y.deref())`
Use Rc<T>
to Share Data. Similar to shared_ptr
in C++. Use Rc::strong_count()
to get the count of references. Use Arc<T>
in concurrent situation; the a
means “atomically”. There are som similarities between RefCell<T>/Rc<T>
and Mutex<T>/Arc<T>
.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
// --------------------------------
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
However, reference cycles may cause memory leaks. To fix this, use Rc::downgrade
to create weak pointer. Similar to weak_ptr
in C++.
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
let leaf = Rc::new(...);
let branch = Rc::new(...);
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
Testing
Run cargo test
.
Filename: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
panic!("Make this test fail");
}
#[test]
#[should_panic]
fn it_also_works() {
panic!("This test won't fail");
}
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
I/O
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
let f = File::open("hello.txt").unwrap();
let f = File::open("hello.txt").expect("Failed to open hello.txt");
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config { query, filename })
}
}
Thread
Principles: Do not communicate by sharing memory.
Rust uses the green-threading M:N model, which means the programming language provide their own special implementation of threads (known as green threads), and there are M green threads per N operating system threads. The green-threading M:N model requires a larger language runtime to manage threads. As such, the Rust standard library only provides an implementation of 1:1 threading.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
Channel
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
We need mutex to protect shared data:
use std::sync::Mutex;
let m = Mutex::new(5);
let mut num = m.lock().unwrap(); // fail if the thread holds the mutex panicked.
As you might suspect, Mutex<T>
is a smart pointer. More accurately, the call to lock returns a smart pointer called MutexGuard
, wrapped in a LockResult
that we handled with the call to unwrap. The MutexGuard
smart pointer implements Deref
to point at our inner data; the smart pointer also has a Drop
implementation that releases the lock automatically when a MutexGuard
goes out of scope.
There are two traits about concurrency: Send
and Sync
. Send
means its ownership can be transferred between threads. Sync
means it is safe to be referenced from multiple threads. However, implementing them manually is unsafe. They’re just useful for enforcing invariants related to concurrency.
Future
Wrap Future
into async/await
(in Rust 2018):
async fn learn_and_sing() {
// Wait until the song has been learned before singing it.
// We use `.await` here rather than `block_on` to prevent blocking the
// thread, which makes it possible to `dance` at the same time.
let song = learn_song().await;
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing();
let f2 = dance();
// `join!` is like `.await` but can wait for multiple futures concurrently.
// If we're temporarily blocked in the `learn_and_sing` future, the `dance`
// future will take over the current thread. If `dance` becomes blocked,
// `learn_and_sing` can take back over. If both futures are blocked, then
// `async_main` is blocked and will yield to the executor.
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
Foreign Function Interface
// define the ABI as "C"
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
export:
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}