Skip to content

Instantly share code, notes, and snippets.

@Ortham
Last active March 3, 2022 13:28
Show Gist options
  • Save Ortham/c83db97d9ceca18509d251e3f5145f74 to your computer and use it in GitHub Desktop.
Save Ortham/c83db97d9ceca18509d251e3f5145f74 to your computer and use it in GitHub Desktop.
An intro to basic Rust syntax and features
// Adjust linter settings to make output less noisy.
#![allow(unused_variables, dead_code)]
/*
The Rust programming language
=============================
Website: https://www.rust-lang.org/
## Learning
The official website has links to lots of great learning resources.
https://cheats.rs/ may also be useful, and
https://github.com/ctjhoa/rust-learning is an unofficial list of many more
resources.
## What's interesting about Rust?
It sits alongside C and C++ in terms of performance, efficiency and suitability
for bare-metal software development, but:
- has a much richer type system, which allows much more expressive programming,
making code easier to reason about and reducing the opportunity for bugs to
occur. A lot has been learned from other languages in the decades since C and
C++ came about.
- enforces memory safety, eliminating a whole class of bugs (which are
responsible for ~ 70% of CVEs) and also making thread safety much easier to
achieve (data races are prevented, other issues like deadlocks aren't).
- has a strong focus on correctness, and a compiler that leads you through
issues with really helpful error messages, which leads to a feeling of
"if it compiles, it works".
- has far better tooling for working with libraries, so code re-use is much
easier.
- this one's a big vague, but a lot of the language and standard library is just
very well designed, with lots of symmetries and orthogonal features fitting
together like a jigsaw. There's also relatively little 'magic' or stuff that's
special-cased by the language.
- it's got a very open development model, and a welcoming and helpful community.
In particular, I like how signficant changes go through an RFC process:
https://rust-lang.github.io/rfcs/
Vs. the usual GC'ed languages (Java, JavaScript/TypeScript, Go, Python) it:
- can be much faster (esp. vs. Python), but more importantly more consistently
and predictably fast, and with much less memory usage
- can be much more helpful in achieving correctness
- prevents data races without extra overhead (it's a "zero-cost abstraction")
- can add significant development overhead when working with some data
structures, e.g. graphs and hierarchical GUIs.
Now, on to the language stuff. You can run all this code in the Rust Playground
at <https://play.rust-lang.org/>.
*/
// This is how you declare a function that takes no args and returns nothing.
fn main() {
// Mandatory hello world example.
// println is a macro, you can tell because it's invoked with a !.
/*
Macros in Rust allow metaprogramming using pattern matching or by
operating on some Rust code - they're not like C preprocessor macros
where there's no understanding of the code they're operating on, so
they're a lot safer. Writing them is a bit complicated, but using them
is easy.
*/
println!("Hello, world!");
immutability();
primitive_types();
functions();
control_flow();
ownership();
borrowing();
slices();
structs();
enums();
methods();
pattern_matching();
traits();
attributes();
generics();
lifetime_annotations();
closures();
iterators();
error_handling();
safe_concurrency();
efficiencies();
modules();
unsafe_rust();
}
fn immutability() {
// Variables are immutable by default. Also notice that Rust has type
// inference.
let number = 1;
/* This fails because number is immutable:
number = 2;
rustc prints the following very helpful error message:
error[E0384]: cannot assign twice to immutable variable `number`
--> src/main.rs:72:5
|
71 | let number = 1;
| ------
| |
| first assignment to `number`
| help: make this binding mutable: `mut number`
72 | number = 2;
| ^^^^^^^^^^ cannot assign twice to immutable variable
*/
}
fn primitive_types() {
// This is a signed 8-bit integer.
let num1: i8 = 4;
// There are also i16, i32, i64 and i128.
// The unsigned equivalents start with u instead of i.
// There's also isize/usize for representing pointer sizes (e.g. 32-bit on
// a 32-bit architecture, and 64-bit on a 64-bit architecture).
// As you'd expect, there are also IEE-754 floats.
let float32: f32 = 2.0;
let float64: f64 = 2.0;
// Here's a little bit of Rust's correctness in action: it doesn't
// implicitly convert between types, so this won't work:
// let result = 1u8 + 2u16;
// This might seem pedantic, but implicit number conversion has caused me
// bugs in C++.
// This works because types and variables exist in separate namespaces.
let bool: bool = true;
// A char holds a 'Unicode Scalar Value'.
let character: char = '😻';
// Here's a tuple.
let tup = (4, 2.0, true, '😻');
// You can destructure tuples, this is part of more general support for
// pattern matching.
let (u, f, b, c) = tup;
// This is the 'unit' type. It's like 'void' in other languages.
let unit: () = ();
// As you can see, it's looks like an empty tuple, a nice bit of design
// efficiency there.
// There are fixed-length arrays:
let array: [i32; 5] = [1, 2, 3, 4, 5];
// There are also slices, references and raw pointers, but they make more
// sense after covering ownership.
}
fn functions() {
// Here's a function that takes an unsigned 32-bit int and adds one to it.
fn plus_one(x: i32) -> i32 {
return x + 1;
}
// Function arguments and the return type need to explicitly give their
// types, this is by design to help API stability. The exception is that
// -> () can be omitted.
// Rust is expression-based, and semicolons turn expressions into
// statements. The above could have been written as:
fn plus_one_expression(x: i32) -> i32 {
x + 1
}
}
fn control_flow() {
// if is an expression
let condition = true;
let number = if condition { 1 } else { 0 };
// you don't need parentheses in if expressions.
// loops loop until you break them. They're also expressions!
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
// There are also while loops, which I'm not writing an example for because
// they're boring.
// for loops should look familiar:
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {}", element);
}
}
fn ownership() {
/* There are three simple rules to ownership:
1. Each value has a variable that is its owner.
2. A value can only have one owner.
3. When the owner goes out of scope, the value will be dropped.
The last point is essentially RAII, which should be familiar to C++ people.
Java people may see some similarity to try-with-resources. The difference is
that Rust does this for everything, everywhere, at a language level.
'dropped' hasn't been defined, but in general it means something like
"run the type's drop function (like a destructor in C++) then free the
type's memory". Not all types have drop functions, but they can be used to
free the type's resources (e.g. memory on the heap that the type points to,
or a database connection that the type holds).
*/
// Let's create a new scope to help demonstrate ownership.
{
// This is a string that is allocated on the heap. I'm going to gloss
// over "Hello world"'s type for now, just think that String::from()
// creates a copy on the heap and internally holds a pointer to that
// memory location.
let str = String::from("Hello world");
// str is only valid within this scope.
}
// str is no longer valid, and the memory allocated by its value has now
// been freed. RAII in action!
// The second rule above prevents double-free bugs, e.g.
{
let str = String::from("Hello world");
let str2 = str;
// Ownership of the value has moved from str to str2, and str is no
// longer valid. This is move semantics. Attempting to use str causes an
// error, e.g.
/*
println!("{}", str)
error[E0382]: borrow of moved value: `str`
--> src/main.rs:250:24
|
246 | let str = String::from("Hello world");
| --- move occurs because `str` has type `std::string::String`, which does not implement the `Copy` trait
247 | let str2 = str;
| --- value moved here
...
250 | println!("{}", str)
| ^^^ value borrowed here after move
The error message mentions the Copy trait: implementing this on a type
indicates to the compiler that it's safe to memcpy them. It's not safe
to memcpy a String because then two values would point to the same heap
memory location, and you'd end up with a double-free bug when they both
went out of scope and tried to free the same memory.
*/
let a = 1;
let b = a;
println!("Integers are copyable: {}, {}", a, b);
// What if you do want to copy a string? You use the clone method:
let str3 = str2.clone();
// This results in there being two heap locations that have separate
// copies of "Hello world", and str3 points to one, while str2 points to
// the other. It is an example of Rust making memory allocations
// obvious, which really helps when optimising code.
}
// Ownership also applies to function arguments and return types, e.g.
fn transfer_ownership() -> String {
// The return value is first owned by a, but then ownership is
// transferred out of the function.
let a = String::from("hello");
a
}
fn take_ownership(input: String) {}
let str = transfer_ownership();
take_ownership(str);
// take_ownership(str);
// The second call is a compile-time error, because ownership of the value
// was transferred into the function in the first call, so str is no longer
// valid.
}
fn borrowing() {
// Ownership on its own is not enough, you need to be able to pass values
// around multiple places at the same time. That's where borrowing comes in.
let str = String::from("Hello world");
let str_reference = &str;
// str owns the value, str_reference is an immutable reference to the value.
// Under the hood, references are essentially pointers, but with added
// semantics.
/* There are a couple of rules for references that the compiler enforces:
1. At any given time, you can have either one mutable reference or any
number of immutable references.
2. References must always be valid.
*/
// A second immutable reference.
let str_ref2 = &str;
// Can't do this:
// let str_mut_ref = &mut str;
// But can do this:
{
let mut str_mut = String::from("Hello world");
let str_mut_ref = &mut str_mut;
}
// This demonstrates reference validity:
let result = {
let str = String::from("Hello world");
/* This expression can't evaluate to the value of &str because str only
exists within this scope.
&str
error[E0597]: `str` does not live long enough
--> src/main.rs:331:9
|
327 | let result = {
| ------ borrow later stored here
...
331 | &str
| ^^^^ borrowed value does not live long enough
332 | };
| - `str` dropped here while still borrowed
*/
};
fn borrow(string: &String) {}
// This is fine, because ownership remains with str.
borrow(&str);
borrow(&str);
// Note that you need to explicitly pass in a reference, unlike C++, which
// makes it easier to spot how a value is used.
/*
While the ownership and borrowing rules can be overly restrictive in some
situations, and make implementing some data structures more difficult (e.g.
graphs), it's hard to overstate how much of an impact they make on being
able to confidently write efficient and correct code. E.g. in C++ I very
rarely pass references around (except as function arguments) because it's so
easy to accidentally invalidate a reference, and it's hard to know a
reference's lifetime over time (you might know when you write that code, but
how can you avoid changing the lifetime when making other changes in the
future?), so I end up copying data all over the place.
*/
}
fn slices() {
/*
A slice is a dynamically-sized view into a contiguous sequence, they're
represented as a pointer (to the start of the view) and a length (of the
view). It's a primitive type.
String slices are another primitive type that are essentially just slices
where the bytes must form valid UTF-8 text.
It's rare to operate on slices themselves, you usually work with references
to them.
*/
let str: &str = "Hello world";
// Earlier I glossed over the type of "Hello world". It's an immmutable
// reference to a string slice. Because "Hello world" is a string literal,
// it gets hardcoded into the compiled binary, so the reference points to
// the memory location of that hardcoded data when the binary is loaded into
// memory. In this case the slice covers the full length of the string, but
// it can be a subsection of it:
let slice = &str[..5];
// ..5 an example of the RangeTo operator in action.
println!("Sliced string: {}", slice);
// Note that because slices are UTF-8, their byte and character counts may
// not be equal:
let sparkle_heart = "\u{1F496}";
assert_eq!("💖", sparkle_heart);
assert_eq!(4, sparkle_heart.len());
assert_eq!(1, sparkle_heart.chars().count());
assert_eq!(2, "y̆".chars().count());
assert_eq!("SS", "ß".to_uppercase());
// Attempting to slice a string in the middle of a UTF-8 byte sequence would
// produce an invalid string, so this panics instead:
// &sparkle_heart[0..2]; //Panics
// Here's an array again.
let a = [1, 2, 3];
// Here's a slice of that array:
let a_slice: &[u8] = &a[0..2];
// We've used a Range again to say we want the first two elements.
println!("Sliced array: {:?}", a_slice);
// It's not really important here, but the :? is to indicate that we want
// the debug output, which will typically provide detail about the data
// structure that the normal display output doesn't.
}
fn structs() {
// We've already seen one struct: String.
// Structs are product types, like classes and structs in many other
// languages.
// Fields are private by default.
struct User {
name: String,
email: String,
active: bool,
}
// You can construct structs using the following syntax.
let user1 = User {
name: String::from("Oliver"),
email: String::from("oliver@example.com"),
active: true,
};
// There's also shorthand for using an existing variable of the same name:
let name = String::from("Oliver");
let user2 = User {
name,
email: String::from("oliver@example.com"),
active: true,
};
// You can also use update syntax, which moves the values in the given
// object for any undefined fields:
let user1_disabled = User {
active: false,
..user1
};
// You use the usual dot notation to access fields.
println!("User name is {}", user1_disabled.name);
// Struct fields don't need to be named.
struct RGB(u8, u8, u8);
let black = RGB(0, 0, 0);
// You use indices to access fields in this case.
println!("Red value of black in RGB is {}", black.0);
// Structs don't even need to have any fields!
struct Void;
}
fn enums() {
// Here's an enum:
enum IpAddrKind {
V4,
V6,
}
let v4 = IpAddrKind::V4;
// You can also assign values to enum variants:
enum Colour {
Red = 0x100,
Green = 0x0F0,
Blue = 0x00F,
}
// You can also store values inside enum variants.
// Fields are private by default.
enum Example {
NoValue,
AnonymousStruct { r: i8, g: i8, b: i8 },
SingleValue(String),
MultipleValues(i8, i8, i8),
}
}
fn methods() {
// You can implement methods on structs that you've defined:
struct User {
name: String,
}
// You can have multiple impl blocks for a type, and methods are private by
// default, the pub keyword makes them public.
impl User {
// Rust doesn't have constructors, new() is just a normal function.
// This would be more idiomatic if it took a String so that if the
// caller had a string this function wouldn't need to create a new copy.
pub fn new(name: &str) -> User {
// Here we're using the Into trait implementation to convert from a
// &str to String, using type inference to know the target type.
// It's the reciprocal of String::from(), it happens to be
// a little shorter at the expense of clarity.
User { name: name.into() }
}
// name() must be called on an existing object, which it immutably
// borrows. Methods can also borrow mutably using &mut self, and take
// ownership using self.
pub fn name(&self) -> &str {
// This is doing something a little fancy called a type coercion,
// which are only allowed in certain circumstances. In this case,
// String implements the Deref<str> trait, so &String can
// implicitly be converted to &str when returning from a function.
&self.name
}
// Rust doesn't have function overloading.
fn set_name(&mut self, name: &str) {
self.name = name.into();
}
}
let mut user1 = User::new("Oliver");
println!("User name is {}", user1.name());
user1.set_name("Alice");
println!("User name is now {}", user1.name());
// You can also implement methods on enums:
enum YesNo {
Yes,
No,
}
impl YesNo {
fn is_yes(&self) -> bool {
match self {
YesNo::Yes => true,
YesNo::No => false
}
}
}
let yes = YesNo::Yes;
println!("Is yes? {}", yes.is_yes());
}
fn pattern_matching() {
// Rust has powerful pattern matching, and you've already seen it in action.
let a = 1;
// Assignment uses pattern matching: a is a pattern! That's why tuple
// destructuring works:
let (a, b) = (1, 2);
// In assignments, pattern matching must be irrefutable, i.e. you can't
// assign to a pattern that might not always match whats on the rhs.
// Patterns can look deep inside values:
struct S {
id: [u8; 16],
}
let s = S {
id: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF],
};
let (
S {
id: [_, second, ..],
},
b,
) = (s, 2);
println!("Second byte of ID is {}", second);
// Here we've matched the second byte in an array in a struct in a tuple,
// and the second element of the tuple. The _ is used to say that you don't
// care what the value of the first element is, and the .. is used to say
// that there are other elements but you don't care about them either.
// Here's an example of fallible pattern matching using a match expression,
// which is like switch on steroids:
enum Colour {
Red,
Green,
Blue,
}
let colour = (Some(Colour::Red), 5);
match colour {
// The part before => is the pattern to match, the part after =>
// evaluates to the value of the match expression if the pattern
// matches.
(Some(Colour::Red), 5) => println!("deep matching"),
(Some(Colour::Red), x) if x < 5 => println!("conditions"),
// _ means "I don't care what value this is", it'll match anything.
// Matches are exhaustive, so you either need to explicitly handle every
// case, or use _ to handle those you don't explicitly handle.
(_, 5) | (_, 70) => println!("don't care about colour"),
_ => println!("matching must be exhaustive"),
}
}
fn traits() {
// A trait is a collection of methods that a type can implement.
// They're like Java interfaces or C++ pure abstract classes, except that
// a trait can be implemented on an existing type, a type's definition does
// not include the traits that are implemented on it. In that respect
// they're more like Go's interfaces.
// Traits support static dispatch (where the types are known at compile time
// and the compiler generates a copy of each trait method for each type that
// uses it, like C++ templates).
// Traits also support dynamic dispatch, where the type is only known at
// runtime and a vtable is used to look up the implementation, like C++
// virtual member functions or Java interfaces.
// Here's a trait.
trait Numberwang {
fn is_numberwang(&self) -> bool;
fn check_numberwang(&self) {
if self.is_numberwang() {
println!("That's numberwang!");
} else {
println!("That's not numberwang...");
}
}
}
/*
There is one main restriction on implementing traits: you cannot implement
traits defined outside the current library on types defined outside the
current library.
*/
// I've defined Numberwang and can implement it on the i16 type.
impl Numberwang for i16 {
fn is_numberwang(&self) -> bool {
match self {
4 | 12 | 47 | 54 | 496 => true,
_ => false,
}
}
}
println!("Is 1 numberwang?");
1.check_numberwang();
println!("Is 4 numberwang?");
4.check_numberwang();
// I can also implement my trait on my type.
struct Number(i16);
impl Numberwang for Number {
fn is_numberwang(&self) -> bool {
self.0.is_numberwang()
}
}
println!("Is Number(4) numberwang?");
Number(4).check_numberwang();
// And I can implement a standard library trait on my type:
impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
println!("I can print Number(4) as {}", Number(4));
// Many operators are implemented as standard library traits, and can be
// overloaded by implementing the relevant trait on your type:
impl std::ops::Add for Number {
type Output = Self;
fn add(self, other: Self) -> Self {
Self(self.0 + other.0)
}
}
println!("Number(4) + Number(5) = {}", Number(4) + Number(5));
// All those are examples of static dispatch, here's dynamic dispatch:
fn check_numberwang(value: &dyn Numberwang) {
value.check_numberwang();
}
println!("Checking Number(4) for numberwang using dynamic dispatch...");
check_numberwang(&Number(4));
println!("Checking 4 for numberwang using dynamic dispatch...");
check_numberwang(&4i16);
// We can also store heterogeneous collections using these 'trait objects'.
// We need to store references because the size of 'dyn std::fmt::Display'
// is not necessarily known at compile time or may be different for
// different types (and arrays must be contiguous), but the size of a
// reference is known and the same for all types of reference.
// We could have also used Box, which is a struct that owns a heap pointer.
let values: [&dyn std::fmt::Display; 4] = [&Number(4), &4i16, &"Hello world", &true];
for value in values {
println!("Value in heterogeneous collection: {}", value);
}
}
fn attributes() {
// Attributes can be used to indicate something about what they're attached
// to.
#[deprecated(since = "1.0", note = "use new_function() instead")]
fn old_function() -> bool {
false
}
old_function();
#[must_use]
fn return_important_value() -> u8 {
42
}
return_important_value();
#[derive(Debug)]
struct Person {
name: String
}
let person = Person { name: "Oliver".into() };
println!("Person is {:?}", person);
#[test]
fn true_should_be_equal_to_true() {
assert!(true == true);
}
}
fn generics() {
/* Idiomatic Rust uses the Option enum to indicate when a value may or may
not exist. It's a standard library type defined as:
Option<T> {
None,
Some(T)
}
T here is a generic data type. An option can either be None, which holds no
value, or Some, which holds a value of type T.
Generics can be used in function, struct and enum definitions.
*/
// Here's three examples of generics: in the function, the array, and the
// return type.
fn get_index<T>(slice: &[T], index: usize) -> Option<&T> {
if slice.len() <= index {
None
} else {
Some(&slice[index])
}
}
let array = [0, 1, 2];
let result = get_index(&array, 1);
println!("Result is {:?}", result);
}
fn lifetime_annotations() {
// Every reference has a lifetime, and the Rust compiler keeps track of
// these to ensure that references stay valid. The compiler can usually
// figure out how lifetimes relate to each other, but sometimes it's
// ambiguous, e.g.
/*
fn return_first_arg(arg1: &str, arg2: &str) -> &str {
arg1
}
error[E0106]: missing lifetime specifier
--> src/main.rs:349:52
|
349 | fn return_first_arg(arg1: &str, arg2: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `arg1` or `arg2`
help: consider introducing a named lifetime parameter
|
349 | fn return_first_arg<'a>(arg1: &'a str, arg2: &'a str) -> &'a str {
| ^^^^ ^^^^^^^ ^^^^^^^ ^^^
The compiler could figure out that the return value lives for at most as
long as arg1 by looking in the function body, but to help maintain API
stability it requires you to annotate the lifetimes involved.
*/
// The 'a is a lifetime annotation, (they take the form '<label>), which
// tells the compiler that the return type must have the same lifetime as
// the first argument.
fn return_first_arg<'a>(arg1: &'a str, arg2: &str) -> &'a str {
arg1
}
// Lifetime annotations are a form of generic type parameter, and can be
// used in functions, structs and enums like other generics.
}
fn closures() {
// This is a closure in Rust:
let add_to_itself = |x| x + x;
println!("4 + 4 is {}", add_to_itself(4));
// Closures can capture their environment:
let value_to_add = 10;
let add_value = |x| x + value_to_add;
println!("4 + {} is {}", value_to_add, add_value(4));
}
fn iterators() {
// Iterators in Rust are a standard library trait.
let array = [1, 2, 3, 4];
// They're lazy, so this doesn't do anything.
let iter = array.iter();
let sum: u32 = array
.iter()
.zip(array.iter().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
println!("Result of array iteration and sum is {}", sum);
// For loops are sugar over iterators, and the item after 'in' can be
// anything that implements the IntoIterator trait.
// A Vec is a standard library type representing a resizeable array. Here
// we're using _ to tell the compiler to infer the type of the Vec's
// elements.
let vec: Vec<_> = array.to_vec();
for element in vec {
println!("Vec element: {}", element);
}
}
fn error_handling() {
// A panic is used to represent unrecoverable errors, and (generally)
// crashes the program, and may unwind the stack or just abort. panics
// provide error messages and backtraces.
/* If an error may be recoverable, idiomatic Rust uses the standard library
Result type, which is an enum:
Result<T, E> {
Ok(T),
Err(E)
}
Using it means that success or failure are represented in the type system.
*/
let result = u8::try_from(60_000u16);
assert!(result.is_err());
println!("Error for int conversion is: {:?}", result);
// BTW, here I'm shadowing the earlier result with a new declaration, it's
// not reassigning the same variable (result is immutable).
let result = u8::try_from(60u16);
assert!(result.is_ok());
// If I want to get the value of a result, I can pattern match, handling the
// error case. if let is one way to handle a fallible assignment. I could
// also use match, or map the result, or several other things depending on
// what I want to do.
if let Ok(i) = result {
println!("Successfully converted types for {}", i);
}
// Here's a function that might error.
fn fallible(num: u16) -> Result<i8, ()> {
match i8::try_from(num) {
Err(_) => Err(()),
Ok(i) => Ok(i),
}
}
// Here's a function that maps the error instead.
fn fallible_map(num: u16) -> Result<i8, ()> {
i8::try_from(num).map_err(|e| ())
}
// Here's a function that uses the Try operator to propagate the error up
// to the caller.
fn fallible_try(num: u16) -> Result<i8, std::num::TryFromIntError> {
let i = i8::try_from(num)?;
/* That is equivalent to
let i = match i8::try_from(num) {
Err(e) => return Err(From::from(e)),
Ok(i) => i
};
The From::from means that the Try operator can convert between different
error types, if the target type implements From for the original type.
*/
Ok(i)
}
// If you are sure that a Result cannot hold an error, you can use .unwrap()
// to get the value inside it without handling the error case - if the
// Result does hold an error, unwrap() will panic.
let result = u8::try_from(60u16).unwrap();
// It's bad practice to use unwrap() outside of tests: if you've got code
// that really cannot hold an error due to some invariant, expect() is like
// unwrap but allows you to provide an error message, which can help
// identify what expectation was violated.
let result = u8::try_from(60u16).expect("60 should fit inside a u8");
}
fn safe_concurrency() {
use std::sync::{Arc, Mutex};
use std::thread::{sleep, spawn};
use std::time::Duration;
// Here's an example of thread-based concurrency.
let other_thread = spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
sleep(Duration::from_millis(1));
}
// Block this thread waiting for the spawned thread to complete.
other_thread.join().expect("join to succeed");
// Threads interact with ownership rules. Here we have to move v into the
// closure to ensure that it doesn't get dropped (or mutated) before the
// thread is done with it.
let v = vec![1, 2, 3];
let other_thread = spawn(move || {
println!("Here's a vector: {:?}", v);
});
other_thread.join().expect("join to succeed");
/* Rust's freedom from data races is achieved using a combination of its
ownership rules and the Send and Sync traits. The traits are both
'marker traits', they don't actually do anything themselves, but they're
recognised by the compiler to have certain semantics.
A type that implements Send may have its ownership transferred from one
thread to another. Most types are send, but an example of one that isn't is
Rc<T>, the reference-counted smart pointer. It uses non-atomic reference
counting, so if it could have its ownership transferred, two threads might
update the reference count at the same time. Arc<T> is the atomic,
thread-safe equivalent.
A type that implements Sync is safe to reference from multiple threads. An
example of a type that implements Sync is Mutex<T>, which (as the name
suggests) is a mutex that owns the data (T) that it protects. The data is
only accessible through methods that first check that no other references
exist before handing out a new reference, so it's always safe to reference
the mutex itself.
(The design of Mutex is a great example of how Rust's ownership semantics
can be used to design APIs that prevent incorrect usage.)
*/
// Here's Arc and Mutex in action. Both are needed because we want two
// threads to reference the same mutex, which needs to live at least as long
// as there is at least one reference to it - which is what
// reference-counted pointers are about.
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
// Spawn 10 threads that each add 1 to the counter.
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = spawn(move || {
let mut num = counter.lock().expect("mutex lock to succeed");
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("join to succeed");
}
println!(
"Result: {}",
*counter.lock().expect("mutex lock to succeed")
);
// Rust's standard library also has channels:
let (tx1, rx) = std::sync::mpsc::channel();
let tx2 = tx1.clone();
spawn(move || tx1.send(1));
spawn(move || tx2.send(2));
for recieved in rx {
println!("{}", recieved);
}
// However, the standard library channels have some problems that are solved
// in the crossbeam library.
// You can also use scoped threads (e.g. as provided by the crossbeam
// library). These are also in the process of being added to the standard
// library.
}
fn efficiencies() {
/* An enum with one variant that has no data and a second variant that has a
value where the all-zero bit pattern isn't valid will only use the same
amount of space as the value.
E.g. Option<&T> is the same size as &T, because references can't be null.
*/
assert_eq!(
std::mem::size_of::<Option<&str>>(),
std::mem::size_of::<&str>()
);
// BTW, the ugly ::<> syntax is unfortunately necessary to avoid parsing
// ambiguity (learning from C++), and is known as the turbofish.
// https://github.com/varkor/rust/blob/master/src/test/ui/bastion-of-the-turbofish.rs
//Structs are only as large as the data they hold:
struct Number(i8);
assert_eq!(std::mem::size_of::<Number>(), std::mem::size_of::<i8>());
// This means that zero-sized types (ZSTs) completely disappear at compile
// time, making them useful for encoding invariants with no runtime cost.
// E.g. you could define a hashset as a hashmap with ZST values.
// Iterators are generally as efficient as hand-written loops.
}
fn modules() {
use std::path::Path;
mod mylibrary {
pub mod paths {
use std::io;
use std::path::Path;
pub fn delete(path: &Path) -> Result<(), io::Error> {
Ok(())
}
}
}
assert!(mylibrary::paths::delete(Path::new("/tmp/foo/bar.txt")).is_ok());
}
fn unsafe_rust() {
/*
Unsafe operations are those that can potentially violate the memory-safety
guarantees of Rust's static semantics.
The following language level features cannot be used in the safe subset of
Rust:
- Dereferencing a raw pointer.
- Reading or writing a mutable or external static variable.
- Accessing a field of a union, other than to assign to it.
- Calling an unsafe function (including an intrinsic or foreign function).
- Implementing an unsafe trait.
*/
let sparkle_heart = vec![240, 159, 146, 150];
// The byte sequence above is valid UTF-8 and because it's hardcoded it
// always will be. However, the compiler doesn't know that so it's not safe
// for code to interpret the bytes as a String without first checking
// they're valid UTF-8.
let string = unsafe {
String::from_utf8_unchecked(sparkle_heart)
};
assert_eq!("💖", string);
// Unsafe is also required when building FFI (Foreign Function Interface)
// due to the use of pointers.
// Don't run this code on windows, because it doesn't have setmntent or
// getmntent.
#[cfg(not(windows))]
{
let path = std::ffi::CString::new("/proc/self/mounts").unwrap();
let open_type = std::ffi::CString::new("r").unwrap();
unsafe {
// Here we're using libc, which is an external library
let mounts_file = libc::setmntent(path.as_ptr(), open_type.as_ptr());
if mounts_file.is_null() {
panic!("Couldn't open mount file");
}
while let Some(entry) = libc::getmntent(mounts_file).as_ref() {
let mount_point = std::ffi::CStr::from_ptr(entry.mnt_dir);
println!("Found mount point at {:?}", mount_point);
}
libc::endmntent(mounts_file);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment