- references
- https://doc.rust-lang.org/
- https://rust-unofficial.github.io/too-many-lists/
- https://www.reddit.com/r/rust/comments/wo46dz/does_using_string_instead_of_str_a_lot_results_in/
- https://www.reddit.com/r/rust/comments/cyymw2/rule_of_thumb_for_struct_data_types/
- https://www.reddit.com/r/rust/comments/4ltwov/shortlived_struct_string_or_a_str/
- https://stackoverflow.com/questions/57562632/why-is-impl-needed-when-passing-traits-as-function-parameters
- https://www.reddit.com/r/rust/comments/12nhpvz/how_can_a_parameter_type_t_be_not_long_living/
- https://chat.openai.com/
- https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ef5675b5d78490c1fb440c229cc5d129
- https://stackoverflow.com/questions/31949579/understanding-and-relationship-between-box-ref-and
- https://users.rust-lang.org/t/box-with-a-trait-object-requires-static-lifetime/35261
- https://stackoverflow.com/questions/24158114/what-are-the-differences-between-rusts-string-and-str
- https://chrismorgan.info/blog/rust-fizzbuzz/
- https://stackoverflow.com/questions/31012923/what-is-the-difference-between-copy-and-clone
- https://stackoverflow.com/questions/65434252/how-to-return-a-reference-to-a-value-from-hashmap-wrappered-in-arc-and-mutex-in
- https://www.reddit.com/r/rust/comments/17luh6c/how_can_i_avoid_cloning_everywhere/
- https://www.reddit.com/r/rust/comments/vy9zvw/the_docs_say_hashmap_is_send_sync_how_can_that_be/
- https://stackoverflow.com/questions/26469715/how-do-i-write-a-rust-unit-test-that-ensures-that-a-panic-has-occurred
- https://www.reddit.com/r/rust/comments/ui7ayd/why_does_rust_not_have_a_standard_async_runtime/
- https://rustjobs.dev/blog/difference-between-string-and-str-in-rust/
- https://users.rust-lang.org/t/whats-the-difference-between-string-and-str/10177/2
- http://xion.io/post/code/rust-patterns-ref.html
- https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md
- https://www.cs.brandeis.edu/~cs146a/rust/doc-02-21-2015/book/static-and-dynamic-dispatch.html
- https://dhghomon.github.io/easy_rust
- https://stackoverflow.com/questions/49377231/when-to-use-rc-vs-box
- https://stackoverflow.com/questions/31012923/what-is-the-difference-between-copy-and-clone
- https://stackoverflow.com/questions/31168589/how-to-force-a-move-of-a-type-which-implements-the-copy-trait
- https://users.rust-lang.org/t/behind-the-scenes-how-does-rust-move-structs/99229/2
- https://www.reddit.com/r/rust/comments/ykku69/does_the_compiler_optimize_moves/
- https://www.reddit.com/r/rust/comments/f6urwh/why_move_semantics_for_value_types/
- https://stackoverflow.com/questions/29490670/how-does-rust-provide-move-semantics
- https://stackoverflow.com/questions/59628211/what-happens-in-memory-when-ownership-is-transferred-out-of-a-box
- https://stackoverflow.com/questions/53465843/why-does-move-in-rust-not-actually-move
- https://stackoverflow.com/questions/30288782/what-are-move-semantics-in-rust
- https://www.reddit.com/r/rust/comments/f6urwh/why_move_semantics_for_value_types/
- https://www.reddit.com/r/rust/comments/rlzhy1/rust_is_creating_copy_instead_of_moving/
- https://www.reddit.com/r/rust/comments/qw7wxx/why_does_mean_both_copy_and_move_where_are_the/
- https://www.reddit.com/r/rust/comments/srtuhy/when_a_move_occurs_what_happens_behind_the_scenes/
- https://users.rust-lang.org/t/sync-but-not-send/21551/5
- https://www.reddit.com/r/rust/comments/iuespp/question_mark_operator_implicit_conversion_why/
- https://web.mit.edu/rust-lang_v1.25/
- https://nnethercote.github.io/2021/12/08/a-brutally-effective-hash-function-in-rust.html
- https://medium.com/@luishrsoares/exploring-hash-functions-in-rust-fowler-noll-vo-fnv-siphash-and-beyond-63183a4d7de
- https://stackoverflow.com/questions/55128808/when-is-it-appropriate-to-require-only-partialeq-and-not-eq
- https://doc.rust-lang.org/book/appendix-03-derivable-traits.html#partialeq-and-eq-for-equality-comparisons
- https://www.reddit.com/r/rust/comments/11gm19h/why_eq_partialeq_ord_and_partialord_especially/
- https://stackoverflow.com/questions/61293115/how-can-a-struct-be-unsized
- https://medium.com/tips-for-rust-developers/pin-276bed513fd1
- https://www.reddit.com/r/rust/comments/eo7u4o/futures_pinning_101/
- https://www.reddit.com/r/rust/comments/pbemse/pin_unpin_and_why_rust_needs_them_blogexplainer/
- https://www.reddit.com/r/rust/comments/f55ur9/fromstr_vs_fromstr_vs_fromstring_which_should_i/
- https://stackoverflow.com/questions/67385956/what-is-the-difference-between-the-fromstr-and-tryfromstring-traits
- https://stackoverflow.com/questions/71487308/is-there-any-real-difference-between-fromstr-and-tryfromstr
- https://levelup.gitconnected.com/rust-unit-structs-explained-4ad2307efa72
- https://stackoverflow.com/questions/67689613/what-is-a-real-world-example-of-using-a-unit-struct
- https://medium.com/@verbruggenjesse/rust-using-rustlings-part-6-structs-b1f3be7c2cbc
- https://www.reddit.com/r/rust/comments/ny44z6/how_would_you_further_organize_the_project/
- https://stackoverflow.com/questions/57756927/rust-modules-confusion-when-there-is-main-rs-and-lib-rs
- https://stackoverflow.com/questions/30177395/when-does-a-closure-implement-fn-fnmut-and-fnonce
- https://stackoverflow.com/questions/50135871/how-can-a-closure-using-the-move-keyword-create-a-fnmut-closure
- https://users.rust-lang.org/t/fnonce-closure-can-be-called-multiple-times/53790/6
- https://stackoverflow.com/questions/51698648/why-is-the-move-keyword-not-always-needed-even-when-the-closure-takes-ownership
- https://www.reddit.com/r/rust/comments/axf137/are_fat_pointers_and_smart_pointers_same_in_rust/
- https://stackoverflow.com/questions/57754901/what-is-a-fat-pointer
- https://docs.rs/thiserror/latest/thiserror/
- https://docs.rs/mockall/latest/mockall/
- https://github.com/asomers/mockall
- https://rocket.rs/v0.5/guide/
- https://api.rocket.rs/v0.5/rocket
- rwf2/Rocket#1736
- https://stackoverflow.com/questions/76965631/how-do-i-spawn-possibly-blocking-async-tasks-in-tokio
- rwf2/Rocket#53 (comment)
- https://docs.rs/figment/0.10.13/figment/
- https://www.reddit.com/r/rust/comments/lvtzri/confused_about_package_vs_crate_terminology/
- https://stackoverflow.com/questions/52024304/what-exactly-is-a-crate-in-the-cargo-ecosystem-and-what-is-the-mapping-to-what
- https://mmapped.blog/posts/03-rust-packages-crates-modules.html
- https://docs.rs/nutype/latest/nutype/
- https://medium.com/itir/dyn-traits-in-rust-in-2-hours-1a44130ac514
- https://www.reddit.com/r/rust/comments/8su7r3/i_dont_understand_the_purpose_of_dyn/
- https://www.reddit.com/r/rust/comments/130cxak/when_should_you_use_a_trait_as_a_type_in_rust/
- https://www.reddit.com/r/rust/comments/lhjooa/is_dyn_redundant/
- https://www.ncameron.org/blog/dyn-trait-and-impl-trait-in-rust/
- https://www.reddit.com/r/rust/comments/l5uih4/what_is_the_difference_between_clone_and_to_owned/
- https://stackoverflow.com/questions/66853369/erronous-mutable-borrow-e0502-when-trying-to-remove-and-insert-into-a-hashmap/
- https://earthly.dev/blog/rust-lifetimes-ownership-burrowing/
- https://anooppoommen.medium.com/lifetimes-in-rust-7f2331be998b
- https://www.reddit.com/r/rust/comments/18e3oq0/why_do_lifetimes_need_to_be_leaky/
- https://www.reddit.com/r/rust/comments/v1z6bx/what_is_a_cow/
- https://blog.logrocket.com/using-cow-rust-efficient-memory-utilization/
- https://rahul-thakoor.github.io/rust-raw-string-literals/
- https://www.shakacode.com/blog/thiserror-anyhow-or-how-i-handle-errors-in-rust-apps/
- https://stackoverflow.com/questions/69518853/not-using-async-in-rocket-0-5
- https://github.com/rust-lang-nursery/lazy-static.rs
- rwf2/Rocket#2714
- https://github.com/GREsau/okapi
- https://github.com/GREsau/schemars
- goals of this workshop
- understanding basics of rust
- memory management
- stack vs heap allocation
- multithreading
- safety:
Send
,Sync
- machinery:
async/await
, runtime providers
- safety:
- borrow checker
- reference types: immutable and mutable
- move vs borrow
- lifetimes
- standard structures
- struct, enum
- trait
- including most used:
Clone
,Copy
,Debug
,Display
,Deref
,Drop
,Eq
,PartialEq
,Hash
,From
,Into
- including most used:
Box
,Cow
,Rc
,Arc
- closure
- String vs
&str
- collections: array, slice, vector, hashmap
- useful syntax
- operators:
.
,?
- pattern matching
- operators:
- error handling
- testing & mocking (mockall)
- memory management
- introduction to rocket
- configuration
- defining endpoints
- dependency injection (state management)
- rust ecosystem overview
- cargo
- crates
- validator
- lazy-static
- nutype
- thiserror
- understanding basics of rust
- workshop task: implement endpoint for deleting customer
- statically typed language
- makes memory safety guarantees without needing a garbage collector
- by default allocate on stack
- accessing data in the heap is slower than accessing data on the stack
- you have to follow a pointer to get there
- pushing to the stack is faster than allocating on the heap
- allocator never has to search for a place to store new data
- that location is always at the top of the stack
- allocator never has to search for a place to store new data
- accessing data in the heap is slower than accessing data on the stack
- uses snake case
- raw pointer
- kinds (either can be cast to another without any restrictions)
- distinction there serves mostly as a lint
*mut T
- mutable raw pointer*const T
- immutable raw pointer
- just like pointers in C++
- unsafe
- Rust makes no effort to track what it points to
- may be null, may point to memory that now contains a value of a different type etc
- dereference only within an unsafe block
- kinds (either can be cast to another without any restrictions)
- fat pointers
- is used to refer dynamically sized types (DSTs)
- example: slices or trait objects
- contains
- the actual pointer to the piece of data
- and some additional information at runtime
- example: length for slices, pointer to vtable for trait objects
- is used to refer dynamically sized types (DSTs)
- smart pointer
- data structures that act like a pointer but also have additional information at compile-time
- example
- if the compiler should insert some code when the pointer goes out of scope
- ensure its data will always be valid UTF-8
- example
- usually implemented using structs that implement the
Deref
andDrop
traits- example: String, Vec, Box
- don't always own the data
- example:
MutexGuard
only lets you get a mutable reference to the type inside theMutex
whileMutex
has ownership
- example:
- vs fat pointers
- built-in pointers like
&T
and smart pointers likeBox<T>
are thin ifT
is statically sized, and fat ifT
is dynamically sized
- built-in pointers like
- data structures that act like a pointer but also have additional information at compile-time
- Rust’s basic pointer type
- may be on the stack or in the heap
- example: reference to an i32 is a single machine word holding the address of the i32
- are never null
- for pointer it's not always true
- example:
NullPointerException
- example:
- for pointer it's not always true
- does not affect ownership
- data is owned by some other variable
- two types
&T
- immutable, shared reference
- read but not modify its referent
x
has the typeT
=>&x
has the type&T
- in Rust terminology, we say that it borrows a reference to x
- are
Copy
- we can have many shared references
- no mutable references along shared references
- modifying the value they point to is forbidden
- immutable, shared reference
&mut T
- mutable, exclusive reference
- both read and modify
x
has the typeT
=>&mut x
has the type&mut T
- forbids any other references of any sort to that value active at the same time
- in particular: as long as there are shared references to a value, not even its owner can modify it
- are not
Copy
(makes no sense) - iterating provides a mut reference to each element
- mutable, exclusive reference
- enforces "multiple readers or single writer" rule at compile time
- way to prevent data races
- race condition happens when these three behaviors 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’s no mechanism being used to synchronize access to the data
- race condition happens when these three behaviors occur
- mistakes like dangling pointers, double frees, and pointer invalidation are ruled out
- example
explanation:
let mut map: HashMap<String, String> = HashMap::new(); map.insert("example".to_string(), "a".to_string()); let example_ref = map.get("example").unwrap(); // immutable borrow occurs here map.remove("example"); // compilation error: cannot borrow `map` as mutable because it is also borrowed as immutable println!("{a}");
example_ref
is a reference to the map contents, meaning the existence of v requires borrowing the map until v stops being used
- example
- can’t protect you from deadlock
- best protection is to keep critical sections small
- way to prevent data races
- syntax
*
operator- used to access the value pointed to by a reference
- often omitted, because "dot" operator automatically references or dereferences its left argument
- necessary only when we want to read or write the entire value that the reference points to
.
- implicitly dereferences its left operand
- can also implicitly borrow a reference to its left operand
- example:
HashMap
hash_map.insert("one", 1) // desugared into (&mut hashmap_wrapper).insert("one", 1);
- example:
Box<T>
- simplest way to allocate a value in the heap
Box::new(v)
allocates some heap space, moves the value v into it, and returns aBox
pointing to the heap space- if goes out of scope, the memory is freed immediately
- vs
RefCell<T>
Box
- borrowing rules’ invariants are enforced at compile timeRefCell
- invariants are enforced at runtime (program will panic and exit)Mutex<T>
is the thread-safe version ofRefCell<T>
Cow<'a, T>
- means "clone on write"
- is an enum that can either be
Cow::Borrowed
- allows you access borrowed reference as long as it lives
- makes clone if you attempt to mutate data
- invoking
to_mut
calls the reference’sto_owned
method to get its own copy of the referent - borrows a mutable reference to the newly owned value
- changes into a
Cow::Owned
- changes into a
- invoking
Cow::Owned
- invoking
into_owned
promotes the reference to an owned value and then returns it- moving ownership to the caller
- invoking
- common use case: return either a statically allocated string constant or a computed string
fn describe(error: &MyError) -> Cow<'static, str> { match error { MyError::OutOfMemory => "out of memory".into(), MyError::FileNotFound(ref path) => { format!("file not found: {}", path.display()).into() } } }
Rc<T>
- means "reference counter"
- non-mutable, non-atomic smart pointer
- call to
Rc::clone
only increments the reference count- doesn't make a deep copy of all the data
Rc<T>
value goes out of scope =>Drop
trait decreases the reference count automatically- when the count is then 0, and the
Rc
is cleaned up completely
- when the count is then 0, and the
Arc<T>
- atomically reference counted
- like
Rc<T>
but safe to use in concurrent situations
- CQRS
- command
- when you create / insert into a data structure, you move the data in (not reference &)
- when you run a command to change data, move the memory around (no reference &)
- query
- use references
- example:
HashMap
fn insert(&mut self, key: K, value: V) // command fn get(&self, key: &K) -> Option<&V> // query
- command
- permits references to references
- is a way to ensure that a reference is not used after the data it points to has been deallocated
- in short: uses to ensure all borrows are valid
- is a proof that no reference can possibly outlive the value it points to
- example: value cannot be dropped when its referents are still alive
fn create_struct<'a>() -> &'a str { let tmp = String::from("Hello, Rust!"); // data owned by the current function &tmp // compilation error: cannot return reference to temporary value } // tmp is dropped here
- example: reference stored in some data structure must enclose that of the data structure
struct Path<'a> { // specifies that the reference must live at least as long as the instance of the struct point_x: &'a i32, // struct cannot live longer than references it holds point_y: &'a i32, }
- example: value cannot be dropped when its referents are still alive
- every type in Rust has a lifetime
- variable's lifetime begins when it is created and ends when it is destroyed
- entirely compile-time
- at run time, a reference is nothing but an address
- function signatures with lifetimes rules
- any reference must have an annotated lifetime
- any reference being returned must have the same lifetime as an input or be static
- elision
- set of rules in Rust that allow the omission of explicit lifetime annotations in function signatures
- rules
- each elided lifetime in input position becomes a distinct lifetime parameter
fn print_strings(s1: &str, s2: &str) // elided fn print_strings<'a, 'b>(s1: &'a str, s2: &'b str) // expanded
- if there is exactly one input lifetime position (elided or not), that lifetime is assigned to all elided output lifetimes
fn as_pair(input1: &str) -> (&str, &str) // elided fn as_pair<'a>(input1: &'a str) -> (&'a str, &'a str) // expanded
- if there are multiple input lifetime positions, but one of them is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes
pub fn get<Q>(&self, k: &Q) -> Option<&V> // elided pub fn get<'a, 'q, Q>(&'a self, k: &'q Q) -> Option<&'a V> // expanded
- otherwise, it is an error to elide an output lifetime
fn get_str() -> &str; // ILLEGAL
- each elided lifetime in input position becomes a distinct lifetime parameter
- facilitates reasoning
- example: function with a signature
go(value: &str))
cannot stash its argument anywhere that will outlive the call- reason:
value
is a reference that is only valid in the method body
- reason:
- example: function with a signature
'static
- indicates that the data pointed to by the reference lives for the remaining lifetime of the running program
- rather limiting
struct S { r: &'static i32 // r can only refer to i32 values that will last for the lifetime of the program }
- any owned data always passes a 'static lifetime bound, but a reference to that owned data generally does not
- reason: owned data is self-contained and needn’t be dropped before any particular variable goes out of scope
'static
should really be renamed'unbounded
- example
fn test_bound<T: 'static>(t: T) {} fn test_ref<T>(t: &'static T) {} fn main() { let s1 = String::from("s1"); // not static test_bound(s1); // passes the 'static bound check test_ref(&s1); // borrowed value does not live long enough, we can spawn thread in test_ref }
- reason: owned data is self-contained and needn’t be dropped before any particular variable goes out of scope
- rather limiting
- use case: usually error values are always string literals that have the
'static
lifetime
- indicates that the data pointed to by the reference lives for the remaining lifetime of the running program
- compiler component that enforces ownership and borrowing rules
- not being perfect - does not accept all valid programs, but it does reject all invalid programs
- ownership = set of rules that govern how a Rust program manages memory
- 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
- scope = range within a program for which an item is valid
- owner is responsible for value deallocation
- Rust eliminates explicit memory deallocation
- rules
- borrowing = action of creating a reference
- rules
- each resource can only have one mutable reference or any number of immutable references at a time
- references must always be valid
- resource being referenced must remain in scope for the entire lifetime of the reference
- mutable reference cannot exist at the same time as any other reference, mutable or immutable
- rules
- type that is composed of other type
- three kinds
- named-field
struct Person { first_name: String, last_name: String, }
- tuple-like
struct Age(u32); impl Age { fn new(age: u32) -> Option<Self> { if age >= 0 { Some(Self(age)) } else { None } } }
- good for newtypes (structs with a single component)
- unit-like
- example:
std::fmt::Error
pub struct Error
- can serve as a marker type
- basis for stateless trait implementation
- occupies no memory, much like the unit type ()
- example:
- named-field
- does not support field mutability at the language level
- mutability is a property of the binding, not of the structure itself
- example
vs
struct Point { mut x: i32, // not compile y: i32, }
let mut point = Point { x: 0, y: 0 }; // ok point.x = 5;
- example
- mutability is a property of the binding, not of the structure itself
- contain data, but can also have logic
- if contains references, references’ lifetimes must be specified
- lifetimes on structs should only be used when the struct is a "view" or "cursor" that looks inside some other struct
- example:
&str
is easy for read-only function parameters, but it's a pain for structs- you should never put the
&str
type in a struct (just default toString
)
- you should never put the
- example:
- rule of thumb: "does the struct own this data?"
- yes =>
'static
(unborrowed) fields - no => you go with references
- shared ownership =>
Rc
/Weak
/Arc
- if it's possibly shared =>
Cow
- if it's possibly shared =>
- yes =>
- lifetimes on structs should only be used when the struct is a "view" or "cursor" that looks inside some other struct
impl
is used to tie logic to struct- Rust method must explicitly use
self
to refer to the value it was called on- similar to: Python (
self
), JavaScript (this
) - not similar to: Java (
this
is auto-generated by compiler) - can take
self
as value and as a reference- by value is used when the method transforms self into something else
- similar to: Python (
- example
struct Age(u32); impl Age { const ZERO: u8 = Age::new(0); // type-associated const fn new(age: u32) -> Option<Self> { // no "self", type-associated function - Age::new(5) if age >= 0 { Some(Self(age)) } else { None } } fn value(&self) -> u32 { // method invoked on instance - age.value() self.0 } }
- Rust method must explicitly use
- if contains references, references’ lifetimes must be specified
- memory
- Rust doesn’t make specific promises about how it will order a struct’s fields or elements in memory
- Rust does promise to store fields’ values directly in the struct’s block of memory
- Java would put values each in their own heap-allocated blocks and have struct fields point at them
- enums
- useful for quickly implementing tree-like data structures
- can have methods
- rust prohibits match expressions that do not cover all possible values
- memory
- tag + enough memory to hold all fields of the largest variant
- tag field is for Rust’s internal use
- tells which constructor created the value and therefore which fields it has.
- example
let circle = Shape::Circle(5.0); let rectangle = Shape::Rectangle(3.0, 4.0); let triangle = Shape::Triangle(1.0, 1.0, 1.0); Circle(5.0): +---------------------------+ | Variant Tag | +---------------------------+ | Associated Data (f64) | | | | | +---------------------------+ Rectangle(3.0, 4.0): +---------------------------+ | Variant Tag | +---------------------------+ | Associated Data (f64) | | Associated Data (f64) | | | +---------------------------+ Triangle(1.0, 1.0, 1.0): +---------------------------+ | Variant Tag | +---------------------------+ | Associated Data (f64) | | Associated Data (f64) | | Associated Data (f64) | +---------------------------+
- tag field is for Rust’s internal use
- tag + enough memory to hold all fields of the largest variant
- by default, struct and enum types are not
Copy
- all the fields of struct
Copy
<=> copy can be derived for struct
- all the fields of struct
- two categories
- recoverable
- example: file not found
- should implement the
std::error::Error
trait- can be derived:
#[derive(Error)]
- guaranteed that such error has human-readable and debuggable representations
- can be derived:
- part of
Result
- equivalent of
Either
- equivalent of try/catch in other languages
- equivalent of
- unrecoverable
- example: trying to access a location beyond the end of an array
- should never happen
- always symptoms of bugs
- like a
RuntimeException
in Java - named
panic
- macro
panic!()
triggers a panic directly - rule of thumb: "don't panic"
- macro
- doesn’t automatically spread from one thread to the threads that depend on it
- reported as an error
Result
in other threads
- reported as an error
- main thread => whole process exits (with a nonzero exit code)
- triggers stack unwinding
- walks back up the stack and cleans up the data from each function it encounters
- a lot of work
- catches stack unwinding:
std::panic::catch_unwind(function)
- allowing the thread to survive and continue running
- use case: mechanism used by Rust’s test harness to recover when an assertion fails in a test
- customizable
- compile with -C panic=abort => first panic in the program immediately aborts the process
- recoverable
- Rust’s take on interfaces
- approach inspired by Haskell’s typeclasses
- are not types in Rust
- are bounds on types
- two ways to use them in a type position
- dispatch = mechanism to determine which specific version is actually run
dyn Trait
~ dynamic dispatch- known as a trait object
- trait objects are types, and not traits
- type can only be known at runtime
- upsides: less code bloat (trait object is not specialised to each of the types)
- downsides
- slower virtual function calls
- inhibiting any chance of inlining and related optimisations
- known as a trait object
impl Trait
~ static dispatch- not something Go or Java have
- use case: generics
- means "any Struct that has an
impl Trait for Struct { ... }
" - performed using monomorphization
- process of turning generic code into specific code by filling in the concrete types that are used when compiled
- example
impl Foo for u8 { ... } impl Foo for String { ... } fn do_something<T: Foo>(x: T) { ... } // compiler will create a special version for both u8 and String, and then replace the call sites // fn some_function(foo: impl Trait) { ... } // equivalent to above, can be handy when one type for param
- can be used in three locations
- as an argument type
- example
pub fn notify(item: &(impl Summary + Display))
pub fn notify<T: Summary + Display>(item: &T)
- where clause
fn some_function<T, U>(t: &T) -> i32 where T: Summary + Display,
- example
- as a return type
- use case: iterators
fn women_vip<'a>(persons: &'a Vec<Person>) -> impl Iterator<Item = &'a Person> + 'a { // instead of Filter<Iter<'_, Person>, fn(&&'a Person) -> bool> persons.iter().filter(|p| p.is_woman()) }
- problem: concrete type needs to be known at compile time
- reason: in order to allocate the right amount of space on the stack
- trait types are unsized and don't have a fixed size known at compile time
- solution: box it
- use case: iterators
- as any type that implements given trait
- example:
impl<T: Display> ToString for T
- example:
- as an argument type
- upsides: allowing for inlining and hence usually higher performance
- downsides: code bloat due to many copies of the same function existing in the binary, one for each type
- used to add extensions methods to existing types (even built-in like
str
andbool
)- example
impl<W: Write> WriteHtml for W { ... } // extension trait for Write trait impl IsBlank for str { // adds is_blank() method for String and &str fn is_blank(&self) -> bool { self.trim().is_empty() } }
- example
- useful traits
- some of them can be automatically derived
- example:
#[derive(Copy, Clone, Debug, PartialEq)]
- example:
Clone
- deep copy: expensive, in both time and memory
- some types don’t make sense to copy:
Mutex
- problem: clone converts
&T
toT
- example: clone a
&str
-str
is unsized and is not even type that a function could return - solution:
ToOwned
can convert from&T
to another target type- generalizes
Clone
ToOwned
isClone
trait that doesn't have to return itself
- used to convert borrowed data (e.g., a reference) into an owned version
- example
impl ToOwned for str { type Owned = String; ... }
- generalizes
- example: clone a
Copy
- represents values that can be safely duplicated via
memcpy
: simply copying the bits in memory- example:
u8
- you cannot possibly be more efficient with a move
- under the hood it would probably at least entail a pointer copy
- it is already as expensive as a
u8
copy
- it is already as expensive as a
- under the hood it would probably at least entail a pointer copy
- you cannot possibly be more efficient with a move
- example:
- cannot be implemented, only derived
- affects how the compiler uses moves (automatic copies)
- just means not treating the old copy as uninitialized
- Copy` types are implicitly cloned whenever they're moved
- every
Copy
type is also required to beClone
Copy
is a special case ofClone
where the implementation is just "copy the bits"- reverse is not true
- example:
Vec<T>
,String
let s1 = String::from("hello"); let s2 = s1; // s1 was moved into s2, s1 cannot be used anymore
- example:
- every
move
is a property of the type, not the operationlet a = b
is always a move, doesn't matter whetherb
isCopy
or not- always does the same thing:
memcpy
- how to force a move of a type which implements the
Copy
trait?- question does not make sense - it is always move
- rust is pass by value
- reference is passed by value but is
copy
- reference is passed by value but is
- always does the same thing:
- similarly, a function call
f(b)
always movesb
- bytes of
b
really get copied into f's stack frame, at least in debug builds - if you use
&mut self
instead ofself
, you're still telling Rust to move, but you'll be moving the pointer instead- analogy: Linux OS
- self = physically moving a file from one disk area to another
- you copy n bytes, where n = size of file
- &self/&mut self = moving an inode pointer
- you copy m bytes where m = address (typically disk+sector)
- self = physically moving a file from one disk area to another
- analogy: Linux OS
- it's up to llvm to optimize out this copy in release builds
- example: https://play.rust-lang.org/ show LLVM IR (intermediate representation)
LLVM IR (in Debug) in "; playground::main" part contains
fn main() { let s = "Hello, World!".to_string(); let t = s; println!("{}", t); }
LLVM IR in Release completely elided the copies (realizing thatcall void @llvm.memcpy.p0.p0.i64(ptr align 8 %t, ptr align 8 %s, i64 24, i1 false), !dbg !2203
s
was unused) - if the object on the stack is large enough, Rust's compiler may choose to pass the object's pointer instead
- example: https://play.rust-lang.org/ show LLVM IR (intermediate representation)
- bytes of
- bitwise copy must represent a valid and independent duplicate of the original value
- problem: freeing resource
- example:
Drop
trait- double deallocation
- using
memcpy
would result in two instances pointing to the same memory
- using
- don't even have to alias heap data
- file handles: file being closed twice
- double deallocation
- example:
- problem: exclusive ownership of mutable references
- example:
Vec
Vec
looks like this:{ &mut data, length, capacity }
- copying it means both reference
&mut data
, which means we have aliased mutable data
- example:
- problem: exclusive ownership of a resource
- example: Mutex
let m = Mutex::new(42); thread::spawn(move || { m; }); // if it were moved with copy, we would have two mutexes on same resource
- passing a mutex by value (with copy) makes no sense
- example: Mutex
- use case: "plain old data" that are stored on the stack and doesn't contain any heap allocations
- however, it could be prudent to omit the
Copy
implementation, to avoid a breaking API change
- however, it could be prudent to omit the
- problem: freeing resource
- represents values that can be safely duplicated via
Debug
,Display
Debug
should format the output in a programmer-facing, debugging context- should be derived
- means printing with
{:?}
Display
is for user-facing output- must be manually implemented
- means printing with
{}
- automatically implement the
ToString
traitToString
should never be implemented butDisplay
instead
Deref
,DerefMut
- specify how dereferencing operators like
*
and.
behave- without it the compiler can only dereference
&
reference
- without it the compiler can only dereference
- example
struct MyBox<T>(T); impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } let x = 5; let y = MyBox(x); let deref_star: i32 = *y; // automatic deref with "*", behind the scenes: *(y.deref()) let deref_dot: i32 = y.0; // automatic deref with "." fn coercion<T>(value: &T) { } coercion(&y) // automatic deref with coercion
- specify how dereferencing operators like
Drop
- called automatically when an object goes out of scope
- used to free the resources
- example:
Box
,Vec
Eq
,PartialEq
,Hash
PartialEq
= corresponds to a partial equivalence relation- symmetric, transitive
Eq
= corresponds to equivalence relationPartialEq
+ reflexive (value is equal to itself)- marker trait: cannot be checked by the compiler
- example of
PartialEq
but notEq
: floating point- NaN != NaN
HashMap
requiresEq
- if you could use
PartialEq
, you would run the risk of black-holing certain values
- if you could use
assert_eq!
requiresPartialEq
- otherwise we could not check equality of floats
Hash
uses Siphash 1-3- Siphash is a cryptographic algorithm that protects hash-flooding denial-of-service attacks
From
andInto
- use case: performing error handling
- related to the
?
operator - resolves composition of distinct error types
- example: implicit conversion on the error value using the
From
traitpub fn fetch_data_from_network(url: &str) -> Result<String, NetworkError> { ... } pub fn read_file_contents(file_path: &str) -> Result<String, FileError> { ... } pub enum AppError { FileOperationFailed, NetworkOperationFailed, } impl From<FileError> for GatewayError { ... } impl From<NetworkError> for GatewayError { ... } pub foo() -> Result<(), GatewayError> { let file_contents = file_parser::read_file_contents("example.txt")?; // values go through the `From` trait to convert them into the return error type let network_data = network::fetch_data_from_network("https://example.com")?; ... } // converts specific errors to GatewayError
- example: implicit conversion on the error value using the
- related to the
- Rust provides
Into
implementation for types that have providedFrom
implementationInto
should be used, in cases whereFrom
cannot be implemented- example: gateway inputs that results in the same command
NewCustomerApiInput
,NewCustomerV2ApiInput
->NewCustomerCommand
- cannot be implemented using
From
: conflicting implementation forNewCustomerCommand
- example: gateway inputs that results in the same command
- conversions cannot fail
- use:
TryFrom
,TryInto
,FromStr
(kept historical reasons, but equivalent for newerTryFrom<&str>
)
- use:
- use case: performing error handling
Send
,Sync
Send
= can be moved to different threadSync
=T
isSync
if and only if&T
isSend
- other words: non-mut reference can be moved to different thread
- automatically derived traits (if a type is composed entirely of
Send
orSync
types) - thread safety story relies on these two built-in traits
- example:
HashMap
isSend
+Sync
- can be read concurrently if there is no mutable reference
- can be modified from any thread as long as there is exclusive mutable reference
- only one thread accessing it
- example:
- example of:
Send
, but notSync
mpsc::Receiver
- receiving end of an mpsc channel is used by only one thread at a time
- example of:
Sync
but notSend
MutexGuard
- not
Send
: requires alloc/dealloc happen on the same thread- sending it to other thread would violate the requirement
- is
Sync
: dropping a reference (&MutexGuard
) does nothing
- not
- similar case: thread-local allocator like
OpenSpan<Attached>
fromzipkin
- example of: neither
Send
norSync
- usually uses internal mutability in a way that isn’t thread-safe
- example:
std::rc::Rc<T>
- reason: race condition to reference count
- uses non-atomic operations to manage its reference count
- reason: race condition to reference count
- some of them can be automatically derived
- memory: fat pointer
- pointer to the value
- pointer to a table (vtable) corresponding to the specific implementation of
T
- vtable is essentially a struct of function pointers, pointing to the concrete piece of machine code for each method in the implementation
- orphan rule: when implementing a trait, either the trait or the type must be new in the current crate
- without the rule, two crates could implement the same trait for the same type, and Rust wouldn’t know which implementation to use
- example: can't
impl Write for u8
- both are defined in the standard library
Self
type represents the concrete type implementing that trait- example
pub trait Clone { fn clone(&self) -> Self; // type of `x.clone()` is the same as the type of `x` }
- example
- are functions that can capture the enclosing environment (values from the scope)
- two ways for closures to get data from enclosing scopes
- moves
let name = String::from("John"); // without move it will borrow the variable by reference (default behaviour) let print_name = move || { // used to indicate that the closure should take ownership of the variable println!("Hello, {}", name); }; println!("Hello, {}", name); // compilation error: value borrowed here after move
- borrowing
- default behavior for closures in Rust is to capture variables by reference
- moves
- usually do not have the same type as functions
- capturing => has its own type
- no two closures have exactly the same type
- ad hoc type created by the compiler, large enough to hold that data
- not capturing => identical to function pointers
fn()
- capturing => has its own type
- two ways for closures to get data from enclosing scopes
- automatically implement one, two, or all three of
Fn
traits- depending on how the closure’s body handles the values
move
- has no effect in whether a closure is
Fn
orFnMut
or not - causes the variables to be moved into the closure at creation time
- does not prevent the closure from being called more than once
- example
let s = String::from("test"); let f = move || { (); }; f(); f(); // is compiling
- usually, you don't have to annotate the
move
keyword to explicitly tell the compilerlet s = String::from("test"); let f = || { s; () }; s; // compilation error: value used here after move - move actually took place
- it might still be necessary because of lifetimes
- closure uses the value from the environment via references => compiler assumes that moving is not necessary
- example
fn foo(s: String) -> Box<dyn Fn()> { // s now lives in the stackframe of foo and the closure outlives that stackframe Box::new(|| { &s; () }) // compilation error: compiler doesn't make the closure a move closure }
- it might still be necessary because of lifetimes
- has no effect in whether a closure is
FnOnce
- all closures implement at least this trait
trait Fn<Args: Tuple>: FnMut<Args>
trait FnMut<Args: Tuple>: FnOnce<Args>
- all closures can be called
- if only one implemented trait => can be called only once
- represents closures that can be invoked at least once
pub trait Fn<Args> { // simplified type Output; fn call(self, args: Args) -> Self::Output; // "self" passed by value vs "&self" passed in Fn }
- if the closure code moves any value out of the captured variables the closure becomes
FnOnce
- example: consumes these values (value would no longer be in the environment)
let s = String::from("test"); let f = move || { s; }; f(); f(); // not compile: closure cannot be invoked more than once because it moves the variable `s` out of its environment
- example: consumes these values (value would no longer be in the environment)
- all closures implement at least this trait
FnMut
- don’t move out captured values
- might mutate the captured values
Fn
- don’t move out captured values
- don’t mutate captured values
- automatically implemented by all functions
- note that
fn
is not a trait- example: cannot be used in
where
clause
- example: cannot be used in
- note that
- represents types that can be called as if they were functions
- used primarily for working with closures
- for example:
HashMap
not implementsFn
- for example:
- Rust will infer types of a closure’s arguments and return type
- generalization of the switch statement
- comparing objects not just by value (or overloaded equality operator, etc.) but by structure
- example
let example_enum: MyEnum = ... match example_enum { MyEnum::VariantA(value) => ... MyEnum::VariantC { name, age } if age >= 18 => ... // match guard MyEnum::VariantC { name, age } => ... }
- shorthand for a match with one pattern
is equivalent of
if let pattern = expr { block1 } else { block2 }
match expr { pattern => { block1 } _ => { block2 } }
- binding patterns
- to service better ergonomics, patterns operate in different binding modes
- default binding mode starts in "move" mode which uses move semantics
- problem: move when we want borrow
- example
let query_params: Vec<(String, String)> = vec![("page".to_string(), "1".to_string())] for &(name, value) in &query_params { // compilation error: cannot move out of a shared reference - type of name and value is String println!("{}={}", name, value); }
- solution:
ref
patternfor &(ref name, ref value) in &query_params { ... }
- example
- problem: move when we want borrow
- references / mutable references set binding mode to
ref
/mut ref
- called match ergonomics: https://rust-lang.github.io/rfcs/2005-match-ergonomics.html
ref
indicates that you want a reference- annotates pattern bindings to make them borrow rather than move
- not part of the pattern
Foo(ref foo)
matches the same objects asFoo(foo)
- example: iteration
- by value
- consumes the collection
- example:
for (k, v) in map
- over shared reference
- produces references
- example:
for (k, v) in &map
- over mut reference
- produces mut references
- example:
for (k, v) in &mut map
- produces
(&K, &mut V)
pairs - there’s no way to get mut access to keys stored in a map
- entries are organized by their keys
- produces
- by value
- default binding mode starts in "move" mode which uses move semantics
- to service better ergonomics, patterns operate in different binding modes
|
can be used to combine several patterns in a single match arm
- UTF-8
- encodes a character as a sequence of one to four bytes
- indexing is not intuitive
str[idx]
would need to return byte
- indexing is not intuitive
- restrictions
- only the shortest encoding for any given code point is considered well-formed
- example: can’t spend four bytes encoding a code point that would fit in three
- only the shortest encoding for any given code point is considered well-formed
- char-by-char comparison does not always give the expected answers
- example: UTF-8 encoding
th\u{e9}
andthe\u{301}
are both valid Unicode representations for théth\u{e9}
=0xC3 0xA9
the\u{301}
=0xC3 0x81
- example: UTF-8 encoding
- encodes a character as a sequence of one to four bytes
- Rust has only one string type in the core language: string slice
str
- usually seen in its borrowed form
&str
- stored as a well-formed UTF-8 encoding (of Unicode characters)
- usually seen in its borrowed form
- for Java people:
String
===StringBuilder
&str
=== (immutable) string
String
- implemented as a wrapper around a
Vec<u8>
- ensures the vector’s contents are always well-formed UTF-8
- is provided by Rust’s standard library rather than coded into the core language
- lives on the heap and therefore is mutable and can alter its size and contents
- overallocates for efficiency
- it is very slow
- can't be created at compile-time => there must be a runtime function call to do that allocation
String::from("literal")
.to_string()
vs.to_owned()
to_owned()
does the same asto_string()
to_owned()
is part of theToOwned
trait
- vs
Box<str>
Box<str>
owns astr
(unlike the&str
)- runtime representation is the same as a
&str
- can be seen as a fixed-length
String
that cannot be resized- cannot resize
str
because it does not know its capacity - you'd need to reallocate every time you push to the string
- cannot resize
- implemented as a wrapper around a
&str
- vs
String
String
is an object,&str
is a pointer at a part of the object
- vs
&String
&str
is a reference directly into the backing storage of the String, while&String
is a reference to the "wrapper" object&str
can be used for substrings, i.e. they are slices&String
references always the whole string
- pronounced "stir" or "string slice"
- reference to a sequence of UTF-8 text
- owned by someone else: it "borrows" the text
- fat pointer like other slice references
- contains address of the actual data and its length (twice the length of a
usize
)
- contains address of the actual data and its length (twice the length of a
- immutable
- type
&mut str
does exist, but it is not very useful- slice cannot reallocate its referent
- almost any operation on UTF-8 can change its overall byte length
- only available operations:
make_ascii_uppercase
andmake_ascii_lowercase
- by definition: modify the text in place and affect only single-byte characters
- slice cannot reallocate its referent
- is very much like
&[T]
- raw syntax: allow to write the literal without requiring escapes
let json_data: &str = r#" { "name": "John Doe", "age": 30, "city": "New York", "is_student": false, "grades": [90, 85, 95] } "#;
- useful to be able to to have multiple different substrings of a
String
without having to copy- example
let string: String = "a string".to_string(); let substring1: &str = &string[1..3]; let substring2: &str = &string[2..4];
- example
&static str
- fastest one but also the less flexible
- value needs to be known at compile time
- literals evaluate to type
&'static str
- literals evaluate to type
- cannot be modified
- compiler copies it into the crate's read-only static space
- forever-valid reference
- vs
str
- fixed-length, stack or heap allocated string slice
- is an immutable sequence of UTF-8 bytes of dynamic length somewhere in memory
- we can’t create a variable of type
str
- explanation: all values of a type must use the same amount of memory
- example
let s1: str = "Hello there!"; let s2: str = "How's it going?"; // s1 needs 12 bytes of storage and s2 needs 15
- example
- always in its borrowed form:
&str
- explanation: all values of a type must use the same amount of memory
- formatting macros
format!
buildsStrings
println!
writes to the standard outputwriteln!
writes to a designated output stream- always borrow shared references
- never take ownership or mutate them
- example
let formatted_string = format!("Hello, {}! You are {} years old.", name, age); println!("{}", formatted_string); let mut buffer = Vec::new(); writeln!(buffer, formatted_string); // write a formatted string to the buffer
- keywords
async
- used to define of asynchronous functions.await
- blocks until a future is ready
- async runtime
- provides the infrastructure for running asynchronous tasks and managing concurrency
- example: tokio
- why it is not part of std?
- 3rd party crates can typically afford to move faster
- different runtimes for different use cases
- example: tokio is a great library for things like web servers, but not ideal for microcontrollers
- in embedded software some async runtime functions are actually provided by the environment
- example: macroquad game engine, which has async as a useful abstraction for doing animation frame timing, but does not require an async runtime
- green threading model was removed before 1.0
- example
async fn async_function() -> String { ... } // async definition #[tokio::main] // async runtime async fn main() { let s: String = async_function().await; // block }
std::future::Future
- approach to supporting asynchronous operations
- async function returns Future immediately
- trait
trait Future { type Output; // never waits, returns immediately // piñata model: whack it with a poll until a value falls out fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } enum Poll<T> { Ready(T), // once a future has returned `Poll::Ready` should be never be polled again Pending, }
- modification
- Rust uses moves to avoid deep-copying values
- example:
Vec<T>::push(item)
takes its argument by value- value is moved into the vector
- usually are not
Copy
- they can’t be, since they owns a dynamically allocated table
- array
- allocated on stack
- have a fixed length (known at compile time)
- arrays of types that implement the
Copy
trait themselves are alsoCopy
- allocated on stack
Vec<T>
T
needs to be known at compile time to know exactly how much memory on the heap will be needed to store each element- example:
Vec<dyn MyTrait>
will not compile, should be usedVec<Box<dyn MyTrait>>
orVec<&dyn MyTrait>
- example:
- memory
- three fields
- the length
- the capacity
- manages the capacity automatically allocating a larger buffer and moving the elements into it when more space is needed
- pointer to a heap allocation where the elements are stored
- created and owned by the
Vec<T>
- created and owned by the
- elements are stored in a contiguous, heap-allocated chunk of memory
- three fields
- compile time protection against modifications during traversal
- java digression:
ConcurrentModificationException
- example
let mut v = vec![10]; for (index, &val) in v.iter().enumerate() { // borrows a shared (non-mut) reference to the vector if val > 10 { v.remove(index); // can't borrow `v` as mutable } }
- solution: use build in functions
v.retain(|&val| val <= 10)
- java digression:
VecDeque<T>
- double-ended queue (deque)
HashMap<K, V>
- memory
- keys, values, and cached hash codes are stored in a single heap-allocated table
- manages the capacity automatically
- useful methods
- get or insert
for id in balances { let balance = bank.entry(id).or_insert(0); *count += 1; }
- modify value:
map.entry(key).and_modify(closure)
- modify or insert
balances.entry(id) .and_modify(|count| *count += 1) .or_insert(1);
- get or insert
BTreeMap
- maintains its keys in sorted order- std uses B-trees rather than balanced binary trees because B-trees are faster on modern hardware
- binary tree may use fewer comparisons per search than a B-tree, but searching a B-tree has better locality
- memory accesses are grouped together rather than scattered across the whole heap
- makes CPU cache misses rarer
- std uses B-trees rather than balanced binary trees because B-trees are faster on modern hardware
- memory
- Slice
- is a region of an array or vector
- notation:
&[T]
- example
fn do_something(n: &[String]) { // either a vector or an array do_something(&a); // works on arrays do_something(&v); // works on vectors
- many methods are defined on slices
- example: sort, reverse
- memory: two-word value (fat not-owning pointer)
- pointer to the slice’s first element
- number of elements in the slice
- other:
LinkedList<T>
,BinaryHeap<T>
,HashSet<T>
,BTreeSet<T>
- shadowing of let
fn main() { let mut count = 5; let count = count + 1; // 6 let count = count * 2; // 12 let mut count = count * 3; // 36 count += 1; // 37 }
- return types
- return without a value is shorthand for return
()
- if the last expression isn’t followed by a semicolon, its value is the function’s return value
!
- expressions that don’t finishfn diverging_function() -> ! { loop { // This loop never exits, representing a function that diverges. } }
main()
can also returnResult
- return without a value is shorthand for return
?
operator- can only be applied to the types Result<T, E> and Option
- unwraps valid values or returns erroneous values
- return type needs to be compatible with the value the
?
is used on - example
is equivalent to
let output = File::create(filename)?
let output = match File::create(filename) { Ok(f) => f, Err(err) => return Err(err) };
- return type needs to be compatible with the value the
- auto converting
&String => &str
&Vec<i32> => &[i32]
&Box<Chessboard> => &Chessboard
- other type conversion requires an explicit cast in Rust
- destructuring
let Track { album, track_number, title, .. } = song;
- struct update syntax
let user2 = User { email: String::from("[email protected]"), ..user1 // move, user1 will no be available if some fields were moved and are not copy };
- convention
#[cfg(test)] // include this module only when testing mod tests { #[test] // tests are ordinary functions marked with the `#[test]` fn first_test() { assert!(true) } }
cargo build --release
skips the testing code- cargo compiles our test code only if we actively run the tests with
cargo test
- cargo compiles our test code only if we actively run the tests with
- assertions
assert!
,assert_eq!
,assert_ne!
- macros from the Rust standard library
- panics, which causes the test to fail
- when the main thread sees that a test thread has died, the test is marked as failed
- integration tests
- entirely external to your library
- use your library in the same way any other code would
.rs
files that live in atests
directory alongsidesrc
- requires
src/lib.rs
file- only library crates expose functions that other crates can use
- binary crate with only
src/main.rs
file => can’t create integration tests- binary crates are meant to be run on their own
- don’t need to annotate any code in
tests/integration_test.rs
with#[cfg(test)]
- cargo compiles files in this directory only when we run
cargo test
- each file in the
tests
directory is compiled as its own separate crate- files in subdirectories don’t get compiled as separate crates or have sections in the test output
└── tests ├── common │ └── mod.rs // tells Rust not to treat the common module as an integration test file └── integration_test.rs
- files in subdirectories don’t get compiled as separate crates or have sections in the test output
- cargo compiles files in this directory only when we run
- entirely external to your library
- run tests from a file
- cargo test --test file_name
- parallelization
cargo test -- --test-threads=4
- first
--
is used to separate the arguments passed to cargo itself from the arguments passed to the test binaries
- first
- testing panic cases
- example
#[test] #[allow(unconditional_panic)] #[should_panic(expected = "expected panic message")] fn test_panicking_function_with_message() { panic!("expected panic message"); }
unconditional_panic
allows the code to use the panic! macro without triggering a warning about an unconditional panic
- not recommended
- when error is expected we should use
Result
- usually panic should be considered a failure
- when error is expected we should use
- Rust standard library itself includes tests that validate panic behavior
#[test] #[should_panic] fn test_panic_macro() { panic!("This is a panic message"); } #[test] #[should_panic] fn test_unreachable_macro() { unreachable!("This code is unreachable"); }
- example
- package vs crate vs module
- crate
rustc
compiles one crate at a time- form the atomic compilation unit of the Rust compiler
- output artifact of the compiler
- two forms
- binary crate
- programs you can compile to an executable
- have a function
main
that defines what happens when the executable runs - example: command-line program, server
- library crate
- doesn't have a main function
- don’t compile to an executable
- define functionality intended to be shared with multiple projects
src/main.rs
andsrc/lib.rs
are called crate rootsmain.rs
is binary- handles running the program
lib.rs
is library- handles all the logic
- contents of either of these two files form a module named
crate
at the root of the crate’s module structure - usual workflow: binary as a thin wrapper around the library
- example
fn main() { library::main(); // binary crate becomes a user of the library crate }
- example
- binary crate
- imports
- example
use crate::common::wrapper; // crate refers to src/lib.rs use std::mem::swap; fn main() { wrapper(swap(&mut a, &mut b)); }
- example
- package
cargo build
builds your whole package- packages are aggregates of crates that share a single Cargo.toml file
- example: lib crate and bin crate
- artifact managed by Cargo
- Cargo book uses the term crate as an alias for package
- generally the main artifact of a package is a library crate and since it is identified with the package name it is customary to treat package and crate as synonyms
- module
- unit of code organization
- Rust’s namespaces
- container for functions, types, and nested modules
- defined using two equivalent approaches
- modules in their own file
- example
mod customer { }
- example
- modules in their own directory with a
mod.rs
- example
- file
customer.rs
mod.rs
pub mod customer;
- file
- example
- example: when Rust sees
mod customer;
, it checks for bothcustomer.rs
andcustomer/mod.rs
- if neither file exists, or both exist, that’s an error
- modules in their own file
use
= bring symbols (such as functions, structs, enums, and modules) into scope- analogy: filesystem’s directory tree
- we use a path in the same way we use a path when navigating a filesystem
use
and apath
in a scope is similar to creating a symbolic link in the filesystem
- example
use std::mem::swap; fn main() { swap(&mut a, &mut b); }
- analogy: filesystem’s directory tree
- crate
- is package manager & build tool
- vs sbt
- sbt = treat configuration as a code
- cargo = treat configuration as data
- vs sbt
- is command-line tool
- useful commands
cargo add
= add dependencies to a Cargo.tomlcargo build
= produces the executable or library specified in your project's configuration- has two main profiles
dev
- uses when you runcargo build
release
- uses when you runcargo build --release
- compile it with optimizations
- command will create an executable in
target/release
instead oftarget/debug
- stores downloaded build dependencies in the Cargo home
- default:
$HOME/.cargo/
- default:
- has two main profiles
cargo run
= builds and then runs project- entry point to be used is
main.rs
- entry point to be used is
cargo test
= executes the tests in your projectcargo check
= check project for errors and warnings without producing executablecargo clean
= remove generated artifactscargo tree
= tree-like representation of dependencies- including transitive dependencies (dependencies of dependencies)
- useful flags
--invert
= invert the tree and display the packages that depend on the given package--duplicates
= show only dependencies which come in multiple versions- useful in investigations if the package that depends on the duplicate with the older version can be updated to the newer version so that only one instance is built.
--edges
= dependency kinds to display- example: all, build, dev, etc
cargo update
= update dependencies as recorded in the local lock filecargo install
= install binaries from crates.iocrates.io
= community’s site for open source crates- installs the specified binary crate globally on your system
- typically,
$HOME/.cargo/bin
- typically,
- used for installing command-line tools or utilities
cargo-edit
package provides additional Cargo commands for managing dependenciescargo add <dependency-name>
= add a new dependency to Cargo.tomlcargo rm <dependency-name>
= remove a dependency
- useful commands
Cargo.toml
- configuration file
- resides at the root of project
- contains metadata about project
- example: dependencies, build settings, etc
- TOML (Tom's Obvious Minimal Language)
- data serialization language
- organized into key-value pairs
- loose interpretation of version specifications
- Cargo looks for the most recent version of the image crate that is considered compatible with version
- reason: allowing Cargo to use any compatible version is a much more practical default
- otherwise it would lead to situations where projects couldn't use multiple libraries with slightly different versions of shared dependencies
- example:
1.2.3 := >=1.2.3, <2.0.0
- example
[package] name = "my_project" version = "0.1.0" edition = "2021" // rust edition represents different releases of the Rust programming language [dependencies] uuid = { version = "1.6.1", features = ["v4"] } // "1.6.1" is shortcut for "^1.6.1" [build-dependencies] // used only during the build process (e.g., build scripts) [dev-dependencies] // used only for testing and development
- Rust editions
- example
- 2015 edition was the first stable release
- 2018 edition changed
async
andawait
into keywords, streamlined the module system, and introduced various other language changes- incompatible with the 2015 edition
- used to evolve without breaking existing code
- are backward-compatible
- code written in earlier editions should still compile and work in newer editions
- are backward-compatible
- programs can freely mix crates written in different editions
- crate’s edition only affects how its source code is construed
- edition distinctions are gone by the time the code has been compiled
- there’s no pressure to update old crates just to continue to participate in the modern Rust ecosystem
- example: fine for a 2015 edition crate to depend on a 2018 edition crate
- crate’s edition only affects how its source code is construed
- example
- Rust editions
- configuration file
Cargo.lock
- example
[[package]] name = "uuid" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", ]
- records the exact version of every crate it used
- generated during first time build
- builds consult this file and continue to use the same versions
- Cargo should not upgrade to the latest library versions every time we build
- version numbers are deliberately flexible
- Cargo should not upgrade to the latest library versions every time we build
- should commit?
- project is an executable => commit
- everyone will consistently get the same versions
- library => don't make much sense to commit
- downstream users will have
Cargo.lock
files that contain version information for their entire dependency graph- library’s
Cargo.lock
file will be ignored
- library’s
- downstream users will have
- project is an executable => commit
- example
- is a web framework for Rust
- provides routing, pre-processing of requests, and post-processing of responses
- fully asynchronous, powered by tokio
- every request is handled by an asynchronous task which internally calls one or more request handlers
- tasks are multiplexed on a configurable number of worker threads
- runtime can switch between tasks in a single worker thread iff (if and only if) an
await
point is reached- context switching is cooperative (switches occur when a task explicitly yields control to the scheduler)
- not preemptive (switches are done independently of the tasks' cooperation)
- if an
await
point is not reached, no task switching can occur- important that
await
points occur periodically
- important that
- context switching is cooperative (switches occur when a task explicitly yields control to the scheduler)
- runtime can switch between tasks in a single worker thread iff (if and only if) an
#[launch]
- generates a main function that launches a returned
Rocket<Build>
- automatically initializes an
async
runtime and launches the function’s returned instance - example
#[launch] // compiled to #[rocket::main] fn rocket() -> _ { // compiled to async fn main() -> Result<(), rocket::Error> rocket::build() // compiled to rocket().launch().await; }
- generates a main function that launches a returned
- endpoint
- example
- get
#[get("/customers/<id>")] async fn get_customer(id: String) -> Option<String> { ... } // if None - an error of 404 - Not Found is returned to the client
- typed validation for param is implemented via the
FromParam
trait- return type of method
FromParam::from_param(param: &'a str) -> Result<Self, Self::Error>;
corresponds to param input of endpointasync fn get_customer(id: Result<CustomerId, String>) -> ... // corresponds to FromParam::from_param -> Result<CustomerId, String>
- return type of method
- typed validation for param is implemented via the
- post
#[post("/hello", data = "<data>")] // data = "<param>", where param is an argument in the handler means that a handler expects body data async fn create_customer( data: Json<CustomerApiInput> // data needs to implement `FromData<'_>`, Json<_> implements it ) -> Result<Json<CustomerApiOutput>, Custom<Json<ErrorApiOutput>>> { ... }
- adding routes
rocket::build().mount(base_path, routes![get_customer, create_customer, ...]); // route needs to be mounted
- when a non-async handler is run, it will be as if Rocket used
spawn_blocking()
Custom<R>(pub Status, pub R)
- creates a response with the given status code and underlying responder- example
let response: Custom<String> = status::Custom(Status::BadRequest, "error");
- example
- serialization
- Rocket re-exports serde's
Serialize
andDeserialize
traits and derive macros fromrocket::serde
- using the re-exported derive macros requires annotating structures with
#[serde(crate = "rocket::serde")]
- due to Rust's limited support for derive macro re-exports
- Rocket re-exports serde's
- state
- used for dependency injection
- managed on a per-type basis
- at most one value of a given type
- handlers can concurrently access managed state
- add
&State<T>
type to any request handler, whereT
is the type of the value passed into manage- example
#[get("/customers/<customer_id>")] pub async fn get_customer( customer_id: String, service: &rocket::State<Arc<CustomerService>>, ) -> Option<Json<CustomerApiOutput>>
- example
- Rocket automatically parallelizes your application
- values you store in managed state implement
Send + Sync
- add
- Rocket instance needs to be started with initial value of the state
- use
manage
to add state to Rocket instance- example:
rocket::build().manage(state)
- example:
- if
&State<T>
for aT
is not managed => Rocket will refuse to start application- example:
error: launching with unmanaged T state
- example:
- use
- configuration:
Rocket.toml
- profiles
debug
andrelease
- profiles for the respective Rust compilation profile
default
- profile with fallback values for all profiles
- defining most configuration
global
- profile with overrides for all profiles
- to override entries use env variable:
ROCKET_{PARAM}
- example:
ROCKET_PORT=9092
- example:
- based on Figment
- figment is a library for declaring and combining configuration sources and extracting typed values from the combined sources
- useful parameters
workers
- sets the number of threads used for parallel task executionmax_blocking
- sets an upper limit of threads to execute potentially blocking, synchronous tasksrocket::tokio::task::spawn_blocking(FnOnce)
- execute the computation in its own thread
- example: long computations
- profiles
- testing
- example
let client = Client::tracked(rocket).unwrap(); // construct a Client using the Rocket instance let req = client.get("/"); // Construct requests using the Client instance let response = req.dispatch(); // Dispatch the request to retrieve the response
- blocking testing API is easier to use and should be preferred
- async testing API:
rocket::local::asynchronous
- async testing API:
- example
- useful macros
uri!()
creates a type-safe, URL safe URI- example
uri!("http://localhost:8000")
uri!("/api", person: name = "Mike", age = 28);
~"api/person/Mike?age=28"
- from endpoint
#[get("/person/<name>?<age>")] fn person(name: &str, age: Option<u8>) { } uri!("api/", person("Bob", Some(28))); // api/person/Bob?age=28
- example
#[async_trait]
- support for
async fn
in trait impls and declarations
- support for
- OpenAPI document generation for Rust/Rocket projects
- crate does not contain any code for the creation of OpenAPI
- only the structure and code to merge two OpenAPI
- can be reused to create OpenAPI support in other web framework
- rocket-okapi
- contains all the code for generating the OpenAPI file and serve it once created
- generate documentation while setting up the server
- never has outdated documentation
- usually executed using macro's like:
openapi_get_routes![...]
JsonSchema
implementation is handled bySchemars
- easiest way to generate a JSON schema for your types is to
#[derive(JsonSchema)]
- library is compatible with Serde
- generated schema should match how serde_json would serialize/deserialize to/from JSON
- example:
#[serde(rename_all = "camelCase")]
- example:
- generated schema should match how serde_json would serialize/deserialize to/from JSON
- crate provides with the schemas for all the different structures and enums
- objects for which the JsonSchema trait is not implemented => enable a feature flag in
Schemars
- example: uuid
schemars = { version = "0.8", features = [ "uuid1" ] }
- example: uuid
- objects for which the JsonSchema trait is not implemented => enable a feature flag in
- Okapi does not implement any schemas directly, this is all handled by
Schemars
- easiest way to generate a JSON schema for your types is to
- uses a combination of Rust Doc comments and programming logic
- example: endpoint
/// # Get all users /// /// Returns all users in the system. #[openapi(tag = "Users")] #[get("/user")] fn get_all_users() -> ... {
- example: dto
#[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] struct User { /// A unique user identifier. user_id: u64, /// The current username of the user. username: String, #[schemars(example = "example_email")] email: Option<String>, }
- example: endpoint
- simplify struct validation
- example
#[derive(Debug, Validate, Deserialize)] struct PersonApiInput { #[validate(custom = "validate_name")] name: String } fn validate_name(name: &str) -> Result<(), ValidationError> { ... } if let Err(_) = person_api_input.validate() { return "invalid input"; };
- enables to have statics initialized at runtime
- in most of the cases can be replaced with
std::sync::OnceLock
- example approach
fn hashmap() -> &'static HashMap<u32, &'static str> { static HASHMAP: OnceLock<HashMap<u32, &str>> = OnceLock::new(); HASHMAP.get_or_init(|| { let mut m = HashMap::new(); m.insert(0, "foo"); m.insert(1, "bar"); m.insert(2, "baz"); m }) }
- apart from cases that u need value not function
- example approach
- allows adding extra constraints like sanitization and validation to the regular newtype pattern
- example
fn sanitize_username(username: String) -> String { username.trim().to_lowercase() } fn validate_username(username: &str) -> bool { !username.is_empty() && username.len() <= 20 } #[nutype( sanitize(sanitize_username), validate(not_empty, len_char_max = 20), )] pub struct Username(String);
- simplifies the implementation of the Error trait
- the same thing as if you had written an implementation of
std::error::Error
by hand Display
impl is generated for your error if you provide#[error("...")]
- support a shorthand for interpolating fields
#[error("{var}")]
=>write!("{}", self.var)
#[error("{0}")]
=>write!("{}", self.var)
- support a shorthand for interpolating fields
From
impl is generated for each variant containing a#[from]
attribute
- provides tools to create mock versions of almost any trait or struct
- annotate with
#[automock]
- instantiate the mock struct with its
new
ordefault
method - record expectations
- annotate with