= rust == resources === one of the first slides about rust http://venge.net/graydon/talks/intro-talk-2.pdf === RustConf effects talk https://www.youtube.com/watch?v=MTnIexTt9Dk&t=924s blog post: https://blog.yoshuawuyts.com/extending-rusts-effect-system/#stage-ii-effect-generic-bounds-impls-and-types effects: • try? • const generic • generic associated types (GAT) • async/.await === C++ examples by Matt Godbolt to convince people of rust https://www.youtube.com/watch?v=nLSm3Haxz0I https://www.collabora.com/news-and-blog/blog/2025/05/06/matt-godbolt-sold-me-on-rust-by-showing-me-c-plus-plus/ API: void sendOrder(const char *symbol, bool buy, int quantity, double price); implementation effort #1: #include using Price = double; using Quantity = int; void sendOrder(const char *symbol, bool buy, int quantity, double price) { std::cout << symbol << " " << buy << " " << quantity << " " << price << std::endl; } int main(void) { sendOrder("GOOG", false, Quantity(100), Price(1000.00)); // Correct sendOrder("GOOG", false, Price(1000.00), Quantity(100)); // Wrong } → Both clang 19 and gcc 14 will take that and not complain → even with -std=c++23 -Wall -Wextra -Wpedantic → these are just type aliases (“using”). Let us make it more strict by defining explicit constructors. #include class Price { public: explicit Price(double price) : m_price(price) {}; double m_price; }; class Quantity { public: explicit Quantity(unsigned int quantity) : m_quantity(quantity) {}; unsigned int m_quantity; }; void sendOrder(const char *symbol, bool buy, Quantity quantity, Price price) { std::cout << symbol << " " << buy << " " << quantity.m_quantity << " " << price.m_price << std::endl; } int main(void) { sendOrder("GOOG", false, Quantity(100), Price(1000.00)); // Correct sendOrder("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong } → static_assert from C++11 is required → it fixes the problem #include #include class Price { public: explicit Price(double price) : m_price(price) {}; double m_price; }; class Quantity { public: template explicit Quantity(T quantity) : m_quantity(quantity) { static_assert(std::is_unsigned(), "Please use only unsigned types"); } unsigned int m_quantity; }; void sendOrder(const char *symbol, bool buy, Quantity quantity, Price price) { std::cout << symbol << " " << buy << " " << quantity.m_quantity << " " << price.m_price << std::endl; } int main(void) { sendOrder("GOOG", false, Quantity(100u), Price(1000.00)); // Correct sendOrder("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong } → all problems solved? → assume the value is provided as a string (from the GUI) … static_cast can be used, right? sendOrder("GOOG", false, Quantity(static_cast(atoi("-100"))), Price(1000.00)); // Compiles just fine, value is e.g. 4294967196 == explanation What does the lifetime bound "'x: 'y" mean? How can I remember it? Pronounce it as ‘outlives’ 'a: 'b … means “'a outlives 'b” What is the difference between crates/packages/workspaces? package → one or more crates (described in Cargo.toml) crate → smallest compilation unit (e.g. everything in src/) workspace → collection of one or more packages Which memory safety guarantees does the rust compile provide you? The compiler guarantees (except unsafe{}) that Rust programs do not • Access memory out-of-bounds • Access memory after it has been freed • Execute the "wrong" code • Have data races What is soundness in programming language theory? Soundness is the property of an API to avoid Undefined Behavior when called from safe code. Unsound code is a library that can be used by safe code to cause undefined behavior. How can I get the Some(…) value or some default value of same type? let x = Some(1); x.unwrap_or(42) Shall I implemented From<…> or Into<…>? From What is a poisoned mutex? If a thread (holding the lock) panics, the mutex content might be in an invalid state. Poisoned means the state of the content might be invalid. Panicing in this situation is a programming bug on its own. How to deal with a poisoned mutex? • Overwriting/not using the state anymore is a good approach. • .unwrap() might even be valid, if you can prove that panicing cannot happen. • otherwise ….unwrap_or_else(std::sync::PoisonError::into_inner) is a common pattern. in case of Err, it takes the inner value of PoisonError which is the value formerly wrapped by the Mutex Why shall I use atomic data types (std::sync::atomic)? • because Mutex is too expensive (→ lock-free is preferred) • you only need to secure access to values in basic data types (AtomicBool, AtomicU8, AtomicU32, …) • the operation is covered by the API for atomic values (e.g. read, write, read-modify-write) Atomic data types are platform features provided in a platform-agnostic manner What is a blanket implementation? A trait implementation `A` for any type `T` which implements trait `B` - in this sense, a trait implementation for another general type also called "conditional trait implementation" e.g. "impl ToString for T" where A=ToString B=Display When does rust elide lifetimes? general lifetime elision rules: #1 compiler assigns a lifetime parameter to each parameter that’s a reference #2 if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters #3 if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters Even here because of #3 … pub fn lexer(&self) -> Lexer { … } elides the lifetime of Lexer<'l> using the lifetime of &self == How-To === cargo and compilation cargo +nightly build -z timings cargo bloat --release --crate #![no_std] --target=riscv32i-unknown-none-elf === VSCode and compilation { "rust-analyzer.cargo.features": ["my-feature-1", "my-feature-2"] } … can be used to add features to rust-analyzer locally (via "Preferences: Open Workspace Settings (JSON)") === compile with MIRI rustup toolchain install nightly --component miri cargo +nightly miri run # run MIRI at runtime cargo +nightly miri test cargo clean set-env MIRIFLAGS "-Zmiri-disable-isolation" cargo +nightly miri run == reference of crates of interest TODO == reference snippets for reuse === custom Error types use std::error; use std::fmt; #[derive(Debug)] enum LibError { FileExists(String), } impl fmt::Display for LibError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::FileExists(filepath) => write!(f, "file '{}' already exists, delete before running this program", filepath) } } } // implementation of suggested traits for custom Error types impl std::error::Error for LibError {} unsafe impl Send for LibError {} unsafe impl Sync for LibError {} // optionally, I put the definitions above in a error.rs file and use the following definitions as well pub type LibResult = Result; impl From for LibError { fn from(err: io::Error) -> Self { LibError::IOError(err.to_string()) } } === blanket implementation equivalence impl<'s> TryInto for StrArg { type Error = errors::Errors; fn try_into(self) -> Result { match self.0.parse::() { Ok(int) => Ok(int), Err(_) => Err(errors::Errors::ArgValueError(self.1, format!("cannot be converted into an integer: '{}'", self.0))), } } } … is already provided by a blanket implementation if you implement … impl<'s> TryFrom for u64 { type Error = errors::Errors; fn try_from(value: StrArg) -> Result { match value.0.parse::() { Ok(int) => Ok(int), Err(_) => Err(errors::Errors::ArgValueError(value.1, format!("cannot be converted into an integer: '{}'", value.0))), } } } === encode u16 array as string vice versa use hex; fn u16arr_to_string(input: &[u16]) -> String { let mut s = String::new(); for i in 0..input.len() { let split = input[i].to_be_bytes(); s.push_str(&hex::encode_upper(&split)); } s } fn string_to_u16arr(input: &str) -> Vec { let mut tmp = vec![0u8; input.len() / 2]; if let Err(e) = hex::decode_to_slice(input, &mut tmp) { panic!("decoding hex error: {}", e); } let mut arr = Vec::new(); for i in 0..(tmp.len() / 2) { let mut data = [0u8; 2]; data.copy_from_slice(&tmp[2*i..2*i+2]); arr.push(u16::from_be_bytes(data)); } arr } === pseudo-hash an array use std::ops::BitXorAssign; fn array_hash(arr: &[T]) -> T where T: BitXorAssign + Copy + std::ops::Add { let mut result = arr[0]; for i in 1..arr.len() { result ^= arr[i] + arr[i - 1]; } result } === popular rust design patterns ==== builder pattern TODO ==== typestate pattern TODO ==== static dispatch for dependency injection pattern TODO ==== sans-io pattern TODO ==== parse-don't-validate pattern TODO == reference snippets for understanding === ownership model i32 satisfies the Copy trait (rationale: it is cheap to do so). So if you assign an i32 to another value, it gets copied. let m: i32 = 5; let mut n: i32 = 3; n = m; // copy n += 1; // only increment value of `n` assert_eq!(m, 4); String does *not* satisfy the Copy trait (rationale: it might be expensive). So if you assign a String to another value, both point to the same data. NOTE: "+" is not an operation of String. So we use "push_str" instead (appends a string). let mut foo: String = String::from("foo"); let mut bar: String = foo; foo.push_str("bar"); assert_eq!(&foo, "foobar"); // compiler: warning: unused variable: `bar` If we actually use `bar`, we get an error: let mut foo: String = String::from("foo"); let mut bar: String = foo; foo.push_str("bar"); bar.push_str("baz"); assert_eq!(&foo, "foobar"); // compiler: error[E0382]: borrow of moved value: `foo` Because only one variable owns the value. First, it is `foo`, in line "ownership transfer" to `bar` happens. Since `bar` has ownership now, `foo.push_str` is disallowed since `push_str` is defined in a way which requires ownership. Can we temporarily pass ownership to `bar` and then get it back? Sure. let mut foo: String = String::from("foo"); { let bar: &mut String = &mut foo; bar.push_str("bar"); } foo.push_str("baz"); assert_eq!(&foo, "foobarbaz"); So, we don't use an assignment for a String, but create a *mutable reference* to `foo` with `&mut foo`. A mutable reference provides temporary ownership. Then we use it to modify the string. Upon `}`, the scope ends and the mutable reference is destroyed. This means ownership goes back to `foo`. So we can run `push_str("baz")` afterwards. NOTE: we cannot use `foo` to modify the string inside of `{…}`. let mut foo: String = String::from("foo"); { let mut bar: String = foo; bar.push_str("baz"); } foo.push_str("bar"); // compiler: error[E0382]: borrow of moved value: `foo` If we would actually assign the string, nothing would happen *temporarily*. Ownership is transferred from `foo` to `bar` and with `}`, the ownership is destroyed. === modularization (https://stackoverflow.com/q/57756927) mod module; … module.rs or module/mod.rs required mod self::module; … same (more explicit way in lib.rs) pub mod module; … module.rs or module/mod.rs required and is accessible if this module is imported extern crate mycrate; … linking step required to find this crate use mycrate::func;  … import func identifier from dependency mycrate use mycrate::{self, func}; … import func identifier from dependency mycrate and import mycrate as well use super::func;  … import func identifier from one level up in hierarchy pub use mycrate::func; … re-export other crate's functionality as part of your own crate foo::bar() … bar function of foo (which depends on known modules and crates) ::foo::bar() … bar function looked up by absolute path in current crate === unsafe via https://doc.rust-lang.org/nomicon/what-unsafe-does.html superpowers: 1. Calling a function marked unsafe, non-Rust external function, or a compiler intrinsic (a function whose implementation is handled specially by the compiler) 2. Dereferencing a C-style pointer 3. Accessing a mutable global variable 4. Using inline assembly instructions 5. Accessing a field of a union type … rust still borrow-checks or verifies lifetimes. Implementor needs to guarantee: * no dereferencing of dangling or unaligned pointers * don't break: A reference cannot outlive its referent. A mutable reference cannot be aliased. * no data races * don't execute code compiled with target features that the current thread of execution does not support * don't produce invalid values Rust allows in safe & unsafe rust: • deadlock • race conditions • leak memory • fail to call destructor • integer overflow • abort the program • … === rust ref matching Some(10) AND match Some(ref member) => member has type "&i32" Some(10) AND match Some(member) => member has type "i32" Some(10) AND match Some(&member) => does not compile Some(Vec) AND match Some(ref member) => member has type "&Vec" Some(Vec) AND match Some(member) => member has type "Vec" Some(Vec) AND match Some(&member) => does not compile for item in vec![3] { let _: u32 = item; } => compiles for item in vec![3].iter() { let _: u32 = item; } => does not compile for item in vec![3].iter() { let _: &u32 = item; } => compiles for &item in vec![3].iter() { let _: u32 = item; } => compiles for ref item in vec![3].iter() { let _: &&u32 = item; } => compiles "if let" does not allow "ref" keyword. ref/ref mut -> "I want to borrow, not move", there is a value but I only need a reference move -> "I want to move" (used per default if no "&" is discovered in the match-argument) "&" -> "I expect a reference here", does actual referencing/dereferencing "&Foo(foo)" does not match enum X { Foo(u32) } match enum_value { X::Foo(int) => { let _: u32 = int; } } => compiles match enum_value { &X::Foo(int) => { let _: u32 = int; } } => does not compile, enum_value is not a reference match &enum_value { &X::Foo(int) => { let _: u32 = int; } } => compiles match enum_value { X::Foo(&int) => { let _: u32 = int; } } => does not compile, integer in Foo is not a reference match enum_value { X::Foo(ref int) => { let _: &u32 = int; } } => compiles match enum_value { ref X::Foo(int) => { let _: &u32 = int; } } => does not compile, invalid syntax enum X { Foo(String) } match enum_value { X::Foo(text) => { let _: String = text; } } => compiles match enum_value { &X::Foo(text) => { let _: String = text; } } => does not compile, enum_value is not a reference match &enum_value { &X::Foo(text) => { let _: String = text; } } => does not compile, cannot move out of shared reference match enum_value { X::Foo(&text) => { let _: String = text; } } => does not compile, mismatched type (unexpected reference String in "&text") match enum_value { X::Foo(ref text) => { let _: &str = text; } } => compiles match enum_value { ref X::Foo(text) => { let _: &str = text; } } => does not compile, invalid syntax • The iterator returned by into_iter may yield any of T, &T or &mut T, depending on the context (enables a for loop). • The iterator returned by iter will yield &T, by convention. • The iterator returned by iter_mut will yield &mut T, by convention. === traits "impl Trait" as return type only works if the return value always has the same type due to technical compiler constraints === lifetimes via https://doc.rust-lang.org/stable/book/ch10-03-lifetime-syntax.html and https://doc.rust-lang.org/rust-by-example/scope/lifetime/explicit.html ==== understanding expressions "fn f(x: &impl Trait)" equals "fn f(x: &T)" equals "fn f(x: &T) where T: Trait" fn f(x: &'a) pronounce it as "x lives at least as long as 'a" fn('a, 'a) -> 'a pronounce it as "the lifetime of the returned reference is the same as the smaller of the lifetimes of the references passed in" struct St<'a> { x: &'a i32 } pronounce it as "'a means an instance of St can’t outlive the reference it holds in its part field" T: Trait + 'a "Type T must implement `Trait` and all references in T must outlive 'a (meaning A)" "impl Debug + 'static": the type implementing Debug does not contain any non-static references (meaning B) … this is satisfied by any owned data (e.g. i32) … is *not* satisfied by any reference to owned data (e.g. &i32) because its lifetime is limited by some function and thus not 'static ==== examples fn longest<'c, 'a: 'c, 'b: 'c>(x: &'a str, y: &'b str) -> &'c str { if x.len() > y.len() { x } else { y } } … is equivalent to "fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { … }" fn longest<'a, 'b, 'c: 'a + 'b>(x: &'a str, y: &'b str) -> &'c str { if x.len() > y.len() { x } else { y } } … is NOT equivalent! struct NamedBorrowed<'a> { x: &'a i32, y: &'a i32, } … means the references "x" and "y" must outlive the struct instance impl<'s> { fn construct() -> Self {} } … what is the lifetime of Self? IMHO: every reference in Self must be 'static impl<'s> { fn construct<'a>(&'a arg) -> Self {} } … what is the lifetime of Self? IMHO: Self implicitly carries the lifetime 's. There is no connection of 'a and 's. This will always fail unless bound "'a: 's" is specified. impl<'s> { fn construct(&'s arg) -> Self {} } … what is the lifetime of Self? IMHO: This is equivalent to the previous case with bound "'a: 's". // coerces 'static into a shorter lifetime static NUM: i32 = 18; fn coerce_static<'a>(_: &'a i32) -> &'a i32 { &NUM } ==== higher-ranked lifetime trait bounds via https://doc.rust-lang.org/reference/trait-bounds.html#higher-ranked-trait-bounds fn call_on_ref_zero<'a, F>(f: F) where F: Fn(&'a i32) { let zero = 0; f(&zero); } … does not compile … why? I think because 'a is declared for the function boundary. So when calling the function, 'a is decided to be a lifetime which exists *as long as the function or longer*. But 'a is defined through the lifetime of `0`. So it is the lifetime of some local variable inside `call_on_ref_zero` and therefore a subset of the function's duration. Therefore a "`zero` does not live long enough" error is reported. So a special lifetime declaration is defined where the lifetime is *not* declared as part of the function, but on the trait bound for `Fn` (thus 'a is not connected to `call_on_ref_zero` but `F`): fn call_on_ref_zero(f: F) where F: for <'a> Fn(&'a i32) { let zero = 0; f(&zero); } … this trait bound is pronounced as "F implements the Fn trait for any lifetime 'a such that it takes a reference to an 32-bit signed integer with lifetime 'a and returns nothing" === C++ smart pointer equivalents in rust ==== single-threaded unique_ptr … ownership transfer shared_ptr … Rc weak_ptr … rc::Weak auto_ptr (deprecated since C++11) … Rc new … Box Cells enable interior mutability (allow mutability even if the object (Cell is part of) is declared immutable): Cell:: implemented by swapping/copying, circumvents aliasing/borrowing; use only for small objects, requires Copy RefCell:: implemented with runtime checks, mutable & read-only borrowing checked at runtime, use for any objects, requires only Sized OnceCell:: can only be written once ==== multi-threaded unique_ptr … ownership transfer shared_ptr … Arc weak_ptr … rc::Weak auto_ptr (deprecated since C++11) … Arc new … Box sync::RwLock:: implemented with runtime checks, mutable & read-only borrowing checked at runtime, use for any objects, requires only Sized sync::OnceLock:: can only be written once === Atomic data types // basic example use std::sync::atomic::{AtomicU64, Ordering}; let atval = AtomicU64::new(42); assert_eq!(atval.fetch_add(10, Ordering::Related), 53); // Ordering variants ordering only matters if you have at least two variables whose load/store operations must get synchronized. Simple counter? no memory ordering required - use Relaxed! Relaxed:: no guarantees when stores/loads of values are visible Release:: commonly used for a store operation (all prev instr are ordered before load with Acquire happens) Acquire:: commonly used for a read operation (all subseq instr are ordered after store with Release) AcqRel:: for loads, Acquire is used. for writes, Release is used. SeqCst:: like Acquire/Release/AcqRel combined and additionally all threads see [all sequentially consistent] operations in the same order === API naming conventions https://rust-lang.github.io/api-guidelines/naming.html#ad-hoc-conversions-follow-as_-to_-into_-conventions-c-conv ============= ========== ================================== Prefix Cost Ownership ------------- ---------- ---------------------------------- as_ Free borrowed -> borrowed to_ Expensive borrowed -> borrowed borrowed -> owned (non-Copy types) owned -> owned (Copy types) into_ Variable owned -> owned (non-Copy types) ============= ========== ==================================