GLT23 workshop: Discovering rust with 25 examples
by meisterluk at GLT23 on 2023-04-14 14:05 for 2h50min in English
Introduction §
Welcome to Grazer Linuxtage 2023!
This is the website of a workshop “Discovering rust with 25 examples” at Grazer Linuxtage. When I started learning rust, many people said they want to study rust as well, but don't have the time to study another programming language. Even though it takes some time to get used to rust with its distinct features, rust also shares many features of other programming languages.
I think rust's concepts can be reduced to some introductory examples discoverable within three hours. I try to address programmers which are not yet familiar with rust and I expect everyone to come with a rust toolchain installation. The examples are really small and quick to solve. If you run into a problem, feel free to ask.
Examples §
-
Example 00: setup §
Reproduction steps:
- Download the .tar.gz or .zip archive accompanying this workshop
- Uncompress its content into a folder like glt23-rust-workshop on your filesystem. Thus, you should have an folder glt23-rust-workshop/example-01-strformat/.
- Open a terminal, switch to the folder glt23-rust-workshop, and run cargo new --bin example-00-setup
- In glt23-rust-workshop/example-00-setup/, run cargo run
Actual output:
Hello, world!Expected output:
Hello Grazer Linuxtage!Hint to achieve the expected output:
Open the text file src/main.rs and modify itWhat we learned:
- Our rust installation works
- We can create a new project with cargo new where --bin means that the project will generate a binary executable (for the commandline); not a library (to be used by other programmers)
- We can invoke the compiler and run the program with one command called cargo run
- The rust code can be found in a project's file path src/main.rs
- The file extension .rs is used for rust source files
Once, your program shows the expected output, proceed to the next example.
-
Example 01: projectdir §
Reproduction steps:
- Go to directory example-01-projectdir
- This project is similar to example-00-setup, but src/main.rs was modified and src/utility/mod.rs was added.
- Look at the file structure and answer the following questions:
- The existence of a .git folder indicates a git repository. Are there any folders in .git/objects besides .git/objects/info and .git/objects/pack?
- Let gitignore.io generate a gitignore file for rust. How does it differ from the ./gitignore file?
- Open the files Cargo.lock and Cargo.toml. Which one is meant to be modified manually?
- What is an edition? Which edition was your project created for?
- Run cargo add termcolor
- Open files Cargo.lock and Cargo.toml again. How did they change?
- Run cargo run and see the error message
- Open the text file src/main.rs and modify it to get the expected output below
- Read its output and determine which file was run. Confirm the file exists in your file structure.
Actual output:
error[E0425]: cannot find function `execute` in this scope --> src/main.rs:14:5 | 14 | execute(); // TODO adjust this line | ^^^^^^^ not found in this scope | help: consider importing this function | 1 | use crate::utility::execute; | For more information about this error, try `rustc --explain E0425`. error: could not compile `example-01-projectdir` (bin "example-01-projectdir") due to previous error
Expected output:
Hello, world!What we learned:
- cargo's projects come together with some files and even a git repository
- .gitignore only excludes target. This is the folder where output files are put (e.g. ELF binary). You can also consider excluding other tool-specific directories. It is interesting that Cargo.lock shall be committed for libraries, but not for executables.
- Cargo.lock stores the exact version number of dependencies like termcolor. A library should be reproducible. An executable has lower standards and should just run.
- An edition is a broader concept than a version. An edition can introduce small backwards incompatible changes over [e.g.] 3 years whereas releases happen every 6 weeks. The current version is 1.68 whereas the current edition is 2021.
- Error messages by the rust compiler are very helpful and often guide you to the solution.
-
Example 02: strformat §
Reproduction steps:
- Go to directory example-02-strformat
- Read the source code of src/main.rs
- Modify src/main.rs to get the expected output, but only edit placeholders in the strings (i.e. curly braces and their content). Look up the documentation to fix the output.
Actual output:
Holmes and Watson function(arg2=39, arg1=42) agent 7 left-padded message |42| debugging representation = message "hello world"Expected output:
Holmes and Watson function(arg1=39, arg2=42) agent 007 left-padded message |42 | debugging representation = "message \"hello world\""What we learned:
- The main routine of the program lives in a function called main. Functions are introduced with the keyword fn, parentheses follow after the name and curly braces wrap the function body.
- String formatting is done with curly-brace-placeholders (inspired by .NET)
- The order of arguments need not match the order of placeholders
- A debug representation can be generated which prints values in a way rust can use as input
-
Example 03: integers §
Reproduction steps:
- Go to directory example-03-integers
- Read the source code of src/main.rs
- Run cargo run and discover its error
- Run cargo run --release and look at its output
- Build an intuition how the source code generated the output (documentation on numeric types)
- Look at the release profile specification to guess why the error did not occur with --release
Actual output:
23 1666 100 16 10 2 127 127 127 false -128What we learned:
- Integers are written with digits and operators are similar to other programming languages (arithmetic +-*/ and logical &/!^ operators)
- multiplication and division have higher precedence over addition and subtraction (as in mathematics)
- underscores can be placed arbitrarily in between digits of an integer to make large integers more readable
- 0x refers to hexadecimal and 0b refers to binary
- 127u8 is an integer 127 with a type suffix u8. A type suffix enforces this integer to be of that type.
- X as Y allows to coerce value X from its type to type Y.
- If we cargo run, it compiles in debug mode. This mode includes integer overflow checks and few optimizations. If we supply --release, it compiles in release mode. This mode does not include overflow checks, but optimizes heavily. An integer overflow follows the semantics of twos complement wrapping.
-
Example 04: strings §
Reproduction steps:
- Go to directory example-04-strings
- Read the source code of src/main.rs
- Run cargo run
Actual output:
glt23 has started! topics: <SYSADMIN> <DEV> <DEVOPS> <COMMUNITY> <LEGAL> <BOOTH> <EXAM> glt23 features Lua python rust PL/pgSQL VerilogExpected output:
glt23 has started! topics: <SYSADMIN> <DEV> <DEVOPS> <COMMUNITY> <LEGAL> <BOOTH> <EXAM> → g → l → t → 2 → 3 glt23 features Lua python rust PL/pgSQL VerilogHint to achieve the expected output:
- Uncomment the loop and look at the error message
- There are 149,186 characters in Unicode. So one character does not fit into one byte.
What we learned:
- We learned how variables are declared (let keyword, optionally type after colon) and values are assigned
- Since rust 1.58, string interpolation is implemented. So println!("{x}") will insert variable x which requiring it as explicit argument
- In line 3, eprintln!(…) is used instead of println!(…) (recognize the leading “e”). The text is then written to stderr and not stdout.
- We learned how methods (like to_ascii_uppercase) are invoked upon values. They can be chained!
- A regular string has type &str. Another string type is String. The former is stored inside the executable (.data section) and cannot be modified. The latter is stored on the heap and can grow arbitrarily (e.g. with method push_str)
- Variables are immutable per default. We need to add the keyword mut to make a variable modifiable.
- A loop can be written with for ITEM in VALUE { DO_SOMETHING }. It reminds me of the for-loop of the python programming language.
-
Example 05: collections §
Reproduction steps:
- Get an overview which collections exist in rust in the std::collections documentation
- Go to directory example-05-collections
- Read the source code of src/main.rs
- Run cargo run. Compilation fails, because you need to tell the rust compiler the type of variable visited.
- Fix the problem by specifying a type after a colon after let mut visited. Its type should be a HashSet of u32 values.
- Remove your previous type annotation and uncomment the lines with push instead
- Read the description of with_capacity.
- Give one difference between Vec and tuples.
Actual output:
error[E0282]: type annotations needed for `HashSet<T>` --> src/main.rs:19:9 | 19 | let mut visited = HashSet::with_capacity(42); | ^^^^^^^^^^^ | help: consider giving `visited` an explicit type, where the type for type parameter `T` is specified | 19 | let mut visited: HashSet<T> = HashSet::with_capacity(42); | ++++++++++++ For more information about this error, try `rustc --explain E0282`. error: could not compile `example-05-collections` (bin "example-05-collections") due to previous error
Expected output:
debug representation = (1, 4) first value = 1 count = 2 first element: 2023-04-14 {5, 2, 3, 7}What we learned:
- A tuple can contain several hetergeneous values. But you cannot add values. A Vec is expandable, but homogeneous.
- vec![A, B, C] is a convenient shorthand notation to generate a vector with items A, B, and C
- The compiler cannot always deduce the type. If we add u32 values with the push method, it knows the collection must be generic over type u32. But if there is no such method call, it does not know and asks you to specify its type. let A: B = C is a variable declaration of variable A with type B assigning value C.
- We can control the initial size of a collection by invoking with_capacity instead of new
-
Example 06: controlflow §
Reproduction steps:
- Go to directory example-06-controlflow
- Run cargo run -- -i 3. Recognize that -- separates arguments for cargo (before --) with arguments for the executable (after --)
- Replace the number 3 with your own integer values. Observe the output to get an idea of the program's behavior.
- Replace the number 3 with your a non-digit string. Observe the output and determine which operation fails.
- Open the file src/main.rs and study the source code
- Answer the question: how can you access/read command line arguments with rust?
- Guess the answer: arg is of type String. arg.parse() parses the string into an integer of type i32. How does rust know which type to convert to (in this case)?
- Answer the question: which type does the variable stop_next have?
- Consider the if-statements in lines 7 and 10. Combine them into a single if statement using the Short-circuiting logical OR operator.
- Consider the increment (original line number 38) and decrement (original line number 46) in the last two loops. Replace them with the “Arithmetic addition and assignment” and “Arithmetic subtraction and assignment” operators.
- If you supply a negative number, the program prints too many lines. Make the behavior equivalent to supplying the number zero in this case.
- In practice, parsing the command line arguments is usually cumbersome. Crates like clap can be used to simplify this.
What we learned:
- Unlike the programming language C syntax, we don't need parentheses between the “if” or “while” keyword and “{”. rust uses else if and neither elif nor elseif.
- rust denotes logical operators like C:
||
for OR,&&
for AND, and!
for NOT. - You can get the command line arguments supplied to the current process by calling std::env::args().
- We declared iterations to be of type i32. In the line iterations = arg.parse().unwrap(), the right-hand side must therefore result in an i32. Therefore the call of unwrap() must produce a i32. Therefore parse() must produce something related to an i32. So the return type in line 30 depends on the declaration in line 22. Rust's type system is very advanced and type inference is very strong.
- Unlike the programming language C (and python and many others), the syntax does not distinguish between statements and expressions. Everything is an expression. Therefore loops, if-statements, function-declarations, and everything else is an expression which means that it returns a value which can be assigned to a variable.
- The expression if j == 0 { break } else if j == 1 { true } else { false } distinguishes three cases. The first case terminates the loop and would not return to the assignment of stop_next in this case. The other cases are booleans and thus stop_next has type bool.
-
Example 07: structenum §
Reproduction steps:
- Go to directory example-07-structenum
-
Learn the following facts:
- structures/records are introduced with the syntax struct X { MEMBERS } where X is the struct's name and MEMBERS is the set of entries in the struct
- Functions associated with struct X are introduced with the syntax impl X { FUNCTIONS }. Then these functions can be accessed with X::function_name()
- Functions are written with the syntax fn F() -> RETTYPE { IMPL } where F defines the function name, RETTYPE defines a return type, and IMPL define the function body. The final expression of the function is its return value
- If the first element in the parentheses of a function fn are &self, the function takes an instance of this struct as argument. Then the function becomes a method.
- With type A = B; a type alias can be defined. A type alias defines A and B to be the same type. Thus extending A means extending B.
- enumerables/enums are introduced with the syntax enum X { ALTERNATIVES } where X is the enum's name and ALTERNATIVES is the set of alternative cases the enum can take on.
- Open the file src/main.rs and study its source code. How do the facts apply to this source code?
- Run cargo run and observe the error message
- Add the line #[derive(Debug)] before the lines starting with struct or enum
- Run cargo run and read the warning messages
- Adjust the variable submission not to hold an alternative Submission::LightningTalk but Submission::Workshop. You can see by enum's definition that Workshop is parameterized. Use the variable glt23_rust as parameter.
Actual output:
error[E0277]: `Workshop` doesn't implement `Debug` --> src/main.rs:29:14 | 25 | #[derive(Debug)] | ----- in this derive macro expansion ... 29 | Workshop(Workshop), | ^^^^^^^^ `Workshop` cannot be formatted using `{:?}` | = help: the trait `Debug` is not implemented for `Workshop` = note: add `#[derive(Debug)]` to `Workshop` or manually `impl Debug for Workshop` = note: this error originates in the derive macro `Debug` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider annotating `Workshop` with `#[derive(Debug)]` | 1 | #[derive(Debug)] | For more information about this error, try `rustc --explain E0277`. error: could not compile `example-07-structenum` (bin "example-07-structenum") due to previous error
Expected output:
Compiling example-07-structenum v0.1.0 (glt23-rust-workshop/example-07-structenum) warning: fields `date`, `lead`, and `has_started` are never read --> src/main.rs:3:5 | 2 | struct Workshop { | -------- fields in this struct 3 | date: String, | ^^^^ 4 | lead: String, | ^^^^ 5 | attendees: u32, 6 | has_started: bool, | ^^^^^^^^^^^ | = note: `Workshop` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis = note: `#[warn(dead_code)]` on by default warning: variants `Booth`, `Workshop`, and `Lecture` are never constructed --> src/main.rs:29:5 | 27 | enum Submission { | ---------- variants in this enum 28 | LightningTalk, 29 | Booth(BoothIdentifier), | ^^^^^ 30 | Workshop(Workshop), | ^^^^^^^^ 31 | Lecture{ title: String, speaker: String }, | ^^^^^^^ | = note: `Submission` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis warning: `example-07-structenum` (bin "example-07-structenum") generated 2 warnings Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/example-07-structenum` Is the room full? true What do you plan to submit? Workshop(Workshop { date: "2023-04-13", lead: "Lukas", attendees: 4294967295, has_started: true })
What we learned:
- How struct and enum work syntactically
- Function with_default_parameters returns a Workshop instance as a return value. The function body only contains one expression. Thus it is its last expression and hence its return value. This value creates an instance of Workshop meaning that actual values to this structure are inserted.
- Since the function does not take an argument, but is inside impl Workshop, it is associated with type Workshop but does not use it. We call such function static methods. In practice, please implement the Default trait, not a custom function with_default_parameters).
- rust enums do not only consider different cases, but its cases can take arguments. They might take no argument (LightningTalk), they might take scalar arguments (Booth with some u32 value), might take another struct argument (Workshop), or might take custom arguments which are declare inside the enum (Lecture).
- #[derive(Debug)] is
magica procedural macro. This means it studies the structure of the enum and then generates methods which allow to print the enum using the {:?} placeholder. The generated method only exists during compilation and is not written to the file.
-
Example 08: match §
Reproduction steps:
- Go to directory example-08-match
- Run cargo run
- Study the source code of src/main.rs. Recognize that …
- match VAL { ALT1 => RETVAL1, ALT2 => RETVAL2, … } allows to pattern-match a specific case. If case ALT1 is parameterized, the parameters can be assigned by giving it a name in ALT1. You can then use it in RETVAL1 (of course similar for other values). One can also use _ as final ALT declaration to catch all remaining cases.
- All branches of match must return the same type. Thus RETVAL1, RETVAL2, … must be of the same type. Since format!(…) returns a String, but "lightning talk" is a &str, I turned "lightning talk" into a String by calling to_string
- if let ALT1 = VAL allows to pattern-match only one specific case. Be aware that currently no other condition (with a logical operator) can be added in this if-let syntax (in contrast to if syntax).
What we learned:
- We learned the semantics of match
- We learned the semantics of if let
-
Example 09: funccol §
Reproduction steps:
- Go to directory example-09-funccol
- Understand that the Iterator trait features many methods like
map
,reduce
, orzip
- Understand that calling the iter method on a collection returns something implementing the Iterator trait (that means you can use the aforementioned methods)
- Understand that these methods are very popular in functional programming because they emphasize that data shall be modified on-the-fly as a sequence of operations to apply. This is unlike imperative programming which teaches us to create temporary variables for intermediate results and modifying the stored state.
- Run cargo run
- Study the source code of src/main.rs
What we learned:
- enumerate() allows us to generate tuples (I, ITEM) where I is the incrementing number starting from 0 and ITEM is the element in sequence
- zip(OTHER_ITER) allows us to generate tuple (A, B) where A is the i-th element of the given iterator and B is the i-th element of OTHER_ITER
- skip(N) allows us to skip the first N elements
- step_by(N) only returns every N-th element
-
Example 10: error §
Reproduction steps:
- Go to directory example-10-error
- Understand that A..B defines a range (A, A+1, …, B-1). Understand that A..=B defines a range (A, A+1, …, B).
- Understand that |ARGS| { BODY } defines an anonymous function with an argument list ARGS and function implementation BODY (compare it with ruby).
- Read about the semantics of reduce. What does it do?
- Understand that we can use enums in practice to distinguish between a successful case and a failure case.
- Run cargo run
- Study the source code of src/main.rs
What we learned:
- Ranges allow us to quickly get a sequence of numbers
- reduce takes a collection (A, B, C, …) and a function F. It computes TMP1 = F(A, B), then TMP2 = F(TMP1, C), … until all elements are processed.
- When the function definition is very short (e.g. a + b), it is more convenient to define an anonymous function and not a regular one
- The result of a computation can be an enum where we distinguish between a successful case and a failure case. This can be used for error handling. In fact, this is how rust does error handling. The type is called Result (not ReturnValue), Ok (not Success), and Err (not Failure)
-
Example 11: trait §
Reproduction steps:
- Go to directory example-11-trait
- Run cargo run and read the error
- A trait is a protocol we define. Types can implement a trait by implementing the functions/methods defined by the trait.
- In general, trait NAME { FN_SIGS } defines a trait where NAME is an identifier (we don't use the name of capabilities as identifier like Java) and FN_SIGS is a set of function signatures that need to be implemented. Default implementations are also possible.
- Then impl NAME for TYPE { FNS } is used to implement the trait NAME for a specific TYPE by implementing the required functions in FNS
- The program fails because the trait implementation is missing. I commented the function body, but you need to provide the correct function signature wrapping it to get the example working.
Actual output:
error[E0046]: not all trait items implemented, missing: `censor` --> src/main.rs:19:1 | 2 | fn censor(&self, text: &str) -> String; | --------------------------------------- `censor` from trait ... 19 | impl Censorship for AustrianCensorship { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `censor` in implementation For more information about this error, try `rustc --explain E0046`.
Expected output:
It shows bad demeanor to say '*******', '*****', and '****' in AustriaHint to achieve the expected output:
The function signature is specified in the trait alreadyWhat we learned:
- Traits specify which methods must be implemented. This design is based on Haskell typeclasses.
- Methods are implemented for a specific type. This means rust uses a nominal type system unlike python or Go. Just by fulfilling a certain function signature does not mean we implement a trait.
-
Example 12: extensibility §
Reproduction steps:
- Go to directory example-12-extensibility
- Run cargo run
- Study the source code of src/main.rs
- Understand that we implemented the trait for &str. Hence wherever you have &str, you can use method increment_if_u32_string if the trait is in the current scope.
Actual output:
Ok("43") Err(ParseIntError { kind: InvalidDigit })
What we learned:
- We saw how to extend a builtin type with additional functionality in rust
-
Example 13: generics §
Reproduction steps:
- Go to directory example-13-generics
- Run cargo run to observe the error
- Look at line 8 and discover that trait LogPrint is only implemented for u32
- We want to make it generic over all types which implement std::fmt::Display
- Introduce a generic type A in line 8 and define a trait bound for std::fmt::Display. Therefore replace impl with impl<A: Display>. Furthermore replace u32 with our generic type A.
- Recognize that all three types in the main routine implement this trait.
Actual output:
warning: unused import: `std::fmt::Display` --> src/main.rs:1:5 | 1 | use std::fmt::Display; | ^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default error[E0689]: can't call method `print` on ambiguous numeric type `{float}` --> src/main.rs:19:27 | 19 | println!("{}", (45.2).print()); | ^^^^^ | help: you must specify a concrete type for this numeric value, like `f32` | 19 | println!("{}", (45.2_f32).print()); | ~~~~~~~~ error[E0599]: no method named `print` found for reference `&'static str` in the current scope --> src/main.rs:20:29 | 20 | println!("{}", "tiraen".print()); | ^^^^^ method not found in `&str` | = help: items from traits can only be used if the trait is implemented and in scope note: `LogPrint` defines an item `print`, perhaps you need to implement it --> src/main.rs:4:1 | 4 | trait LogPrint { | ^^^^^^^^^^^^^^ Some errors have detailed explanations: E0599, E0689. For more information about an error, try `rustc --explain E0599`. warning: `example-13-generics` (bin "example-13-generics") generated 1 warning error: could not compile `example-13-generics` (bin "example-13-generics") due to 2 previous errors; 1 warning emitted
Expected output:
[UNIX epoch + 1681432172] 3 [UNIX epoch + 1681432172] 45.2 [UNIX epoch + 1681432172] "tiraen"What we learned:
- Implementations of traits can be generic over a type.
- The generic type must be declared after impl.
- It can be constrained with a trait bound after the colon.
-
Example 14: monomorphization §
Reproduction steps:
- Go to directory example-14-monomorphization
- Run cargo run
- Study src/main.rs and determine which types are inserted instead of D
Hint to achieve the goal:
Lines 19 and 26 show the instantiations which types are usedWhat we learned:
- We can also be generic over structs (equivalently for enums). For this we put <D: Display> after the struct name
- Inside of the implementation self.duration is of type D. D will be substituted with each type given in the instantiation
- Lines 19 and 26 show the instantiations. D will be substituted with f64 (default floating point type) and &str
- Monomorphization describes that for each instantiation of the type variables, a separate version is written into the executable. So we don't have one abstract start implementation which works for any type in the executable, but we have two implementations in this example optimized for the two instantiated types. This is similar to C++ templates and unlike Java interfaces.
- In programming language design this is also called static dispatch, because at compile time we know which actual implementation (for a specific type) shall be used. Static refers in this case to compile time.
- The other concept (abstract implementation dispatching for the type at runtime) is represented in rust with trait objects, but they are not easy to handle. Thus I skipped it in this workshop.
-
Example 15: genericfunction §
Reproduction steps:
- Go to directory example-15-genericfunction
- Take a look at src/main.rs.
- generic1 and generic2 are almost the same. The both take two arguments and their implementation is the same. And both arguments need to implement the Display trait.
- Use the main function for experiments. Make a call of generic2 which does not work for generic1.
Hint to achieve the goal:
Who defines which actual type is used for type argument A or B?What we learned:
- If we use generics, we should think through who defines the actual type replacing the type arguments. This helps us to understand the errors the compiler throws because the compiler collects this information as it reads the source code.
- Furthermore if we write an API, it is crucial to think through how lose or strict the type specification shall be.
-
Example 16: frominto §
Reproduction steps:
- Go to directory example-16-frominto
- Run cargo run. Recognize that this example compiles and works fine.
- In this example, I would like to introduce the Into trait. Study the function into_example and recognize that we did not use type coercion with as, but some method into to convert a u16 to a u32.
- Look at the signature of trait Into and recognize that traits can be generic too.
- Recognize that we communicate source type u16 and target type u32 to the compiler. So because the target type shall be u32, the into method knows which conversion we want to perform.
- Do the following test: Can we swap the source and destination types u16 and u32? What might be a reason it fails?
- Now I built this on my own. I introduce a trait ConvertInto. It uses a so-called associated type called TargetType. An associated type allows us to introduce a type variable which can be used across the entire trait implementation, not just one function or method (Disclaimer: we only have one method here and do not reuse it).
- Self refers to the type of the &self variable.
- Self::TargetType refers to the type variable.
- The example convert_example shows how to use it. Recognize that we never specified TargetType explicitly, but rust infers its type from the call of convert.
- Extend MyType for another target type, you choose on your own.
What we learned:
- type inference allows us to specify the type of a variable and propagate it into distant places.
- The Into trait takes advantage of that provides a type coercion mechanism. Unlike the as keyword, it is extensible. The first time, I have seen from the serde crate that one can write/implement something like let doc: TomlDocument = source_code.parse(); let converted: HtmlOutput = doc.into(); I was amazed.
-
Example 17: ownership §
Reproduction steps:
- Go to directory example-17-ownership
- Run cargo run and read the error message
- The integer example is working fine. The vector example is broken. Can you guess what the difference between integers and vectors are from a memory management perspective?
- Study the three ownership rules of rust. Owners are variables.
- Who owns the value 42 after line 3? Who owns the value 42 after line 4?
- Who owns the vector after line 10? Who owns the vector after line 11?
- Add an ampersand before the assignment of x. If we write let y = &x; we assign a reference to value x, not the value itself.
- Recognize that line 14 does not print a reference, but a value. Rust implements auto-dereferencing, because you rarely want to print a memory address, but the value at this address.
- Replace y with *y in line 14. Recognize that you now dereferenced explicitly instead of automatically.
- There is nothing like auto-referencing. References are never created implicitly.
- Move means transferring ownership from one variable to another by assignment. Can you guess where a move happens?
Actual output:
error[E0382]: borrow of moved value: `x` --> src/main.rs:13:22 | 10 | let x = vec!["Grazer Linuxtage"]; | - move occurs because `x` has type `Vec<&str>`, which does not implement the `Copy` trait 11 | let y = x; | - value moved here 12 | 13 | println!("{:?}", x); | ^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider cloning the value if the performance cost is acceptable | 11 | let y = x.clone(); | ++++++++ For more information about this error, try `rustc --explain E0382`. error: could not compile `example-17-borrowowner` (bin "example-17-borrowowner") due to previous error
Expected output:
42 42 ["Grazer Linuxtage"] ["Grazer Linuxtage"]What we learned:
- An integer can be cheaply copied on the memory stack. Rust will just do it if you assign a integer of variable a to an integer of variable b. Copying a vector (which lives on the heap) is not cheap.
- After line 3 variable a is the owner of the first value 42 stored in memory. After line 4 variable b is the owner of the second value 42 stored in memory (there is a second value, because it was copied).
- After line 10 variable x is the owner of the vector stored in memory. After line 11 variable b is the owner of the second value 42 stored in memory (there is a second value, because it was copied).
- Because there are two values in lines 3 and 4, there is no movement operation happening. Because there is only one value in lines 10 and 11, a movement occurs. Ownership is transferred from owner x to owner y.
- You must not use a variable which was moved. Thus the error occured.
- If you take a reference to a value, ownership does not change.
- Scalar types like integers, floating point numbers, booleans, and characters.
- Auto-dereferencing is convenient. Sometimes it is better to be explicit to keep track of your types.
-
Example 18: borrowmut §
Reproduction steps:
- Go to directory example-18-borrowmut
- Run cargo run and take a look at src/main.rs
- You can always wrap a block of code inside curly braces. This introduces another lexical scope. This means variables declared inside the curly braces are not accessible from the outside. I am using this mechanism in the main routine to re-use the variable name events.
- One can use &events to take a shared reference (like the references in the previous example). One can use &mut events to take a mutable reference.
- There is no example here, you can only take an arbitrary number of shared references or exactly one mutable reference. Can you guess why? In rust, this principle is call “aliasing XOR mutation”.
- mut events means the variable events is mutable. &mut events means the value behind the reference events is mutable.
- print_events transfers ownership from the variable events (of line 26) to the variable events (of line 1). Try to call print_events twice. Does it compile? Why (not)?
- Can you call add_and_print_referenced_event twice in line 42? Does it compile? Why (not)?
- Try to explain each function call with everything you learned about ownership and borrowing.
- Specifically specify who the owner of the vector is when calling add_and_return_event.
Actual output:
["Grazer Linuxtage"] ["Grazer Linuxtage"] ["Grazer Linuxtage", "Chemnitzer Linuxtage"] ["Grazer Linuxtage", "Chemnitzer Linuxtage"] ["Grazer Linuxtage", "Chemnitzer Linuxtage", "FOSDEM"]What we learned:
- The owner of the vector of line 26 is events at line 26. Then the ownership is transferred to events of line 1. If you try to call it twice, you try to use events of line 26 again. But you cannot because it is not the owner anymore.
- Since references do not transfer ownership, events at line 5 in print_referenced_events never gets the ownership of the vector.
- The owner of the vector of line 36 is events at line 36. Then the ownership is transferred to add_and_print_events of line 9. It is now mutable and you can add items to it
- The owner of the vector of line 41 is events at line 41. Then the ownership is not transferred, because we only pass a mutable reference as parameter. Thus we can call it twice, but still modify it.
- The owner of the vector of line 46 is events at line 46. Then the ownership is transferred to events at line 19. After finishing the function, we also return events. Then more_events at line 47 is the new owner and can modify the vector with a push.
- Aliasing XOR mutation ensures that there are no two references writing on a value at the same time or one reference invalidates the value and the other is trying to write to it.
-
Example 19: borrowruntime §
Reproduction steps:
- Go to directory example-19-borrowruntime
- The ownership and borrow model is wonderful, but sometimes too limiting.
- Run cargo run and take a look at src/main.rs
- We escape these limitations by checking the ownership and borrowing concepts at runtime. We use the type RefCell for it.
- This allows us - for example - to implement interior mutability. This means we have a shared (i.e. read-only) reference, but it manages a value behind this reference that it can modify.
- In this example, we wrap our vector with a RefCell. The function add_and_print_referenced_event borrows ownership briefly at runtime to push an element. borrow_mut returns a mutable reference to its content. borrow returns a shared reference to its content.
Actual output:
["Grazer Linuxtage", "Chemnitzer Linuxtage"] ["Grazer Linuxtage", "Chemnitzer Linuxtage"]What we learned:
- RefCell allows us to check ownership and borrowing concepts at runtime.
-
Example 20: lifetime §
Reproduction steps:
- Go to directory example-20-lifetime
- Take a look at src/main.rs
- Define a lifetime. A lifetime is declared like a type argument. Add <'a> after Value in line 1.
- The lifetime can then be annotated to the reference in the struct. Add 'a after & in line 2.
- Run cargo run
- Sometimes rust just does not know how long values live. Declaring lifetimes means “I am telling the compiler here that this value will live at least during this time”. The compiler will then only check during compilation that the claims are fulfilled.
- In our case “during this time” means “during the lifetime of the struct”. But it might also mean “during the lifetime of this scope”
- Recognize that the compiler checks that the supplied string (long_live_the_string or shortliving_string) lives as long as the struct or longer.
Actual output:
this string lives as long as the program: 'Hello world' this string only lives as long as this scope: 'Hallo'What we learned:
- Lifetimes are declared like type arguments, but have a leading single-quote; like 'a
- Lifetimes are sometimes necessary, because the rust compiler cannot infer them (most of the time, the compiler infers it successfully with a mechanism called lifetime elision).
- Common occurences are references occuring in struct members (always required). If there are references in arguments and return values of a function signature (if are several references) and rust cannot infer the relationship between input and output argument lifetimes.
-
Example 21: lib §
Reproduction steps:
- Go to directory example-21-lib
- Run cargo build to compile/build the library (this is the step cargo run always does)
- Unlike the previous examples, this project is a library. It cannot be executed (there is no src/main.rs)
- Executables can be initialized with cargo new --bin NAME. Libraries can be initialized with cargo new --lib NAME. Crates can actually be both at the same time (libraries and executables).
- But I want you to understand that we can control nicely which values are exposed and accessible from the outside.
- Recognize the following properties:
- Open src/lib.rs. This library exposes the functions print_me and execute_print. By modifying line 4 to pub use sub::utils; we would expose the entire module. Thus the library user cannot call example_21_lib::print_me() but example_21_lib::execute::print_me()
- Open all .rs files and recognize the pub keywords.
- Open src/sub/mod.rs and remove the pub keyword. Run cargo build and observe how it fails.
- All functions and structs are private per default (only the current file can use them). You can use pub(crate) to make them visible within the current library/crate. You can use pub to make them visible outside. For the last part, the visibility declaration in lib.rs and mod.rs must correspond.
What we learned:
- A file src/lib.rs specifies which values are exposed as part of a library.
- We define a module sub by creating a folder and putting a mod.rs file in it.
- Visibility can be defined with the pub keyword.
-
Example 22: unittests §
Reproduction steps:
- Go to directory example-22-unittests
- This project was created only by running cargo new --lib example-22-unittests
- cargo provides an example unittest when creating a library (this is just arbitrary, test your functions in executable with unittests too!).
- Open src/lib.rs. Recognize that we have a function add and it is used in the unittest at line 11.
- Recognize that we can only access it, because we import the scope in line 7. It uses the super keyword to access the scope above the module <
- Recognize that we use #[cfg(test)] in line 5. Recognize that we annotate a unittest with #[test] in line 9.
- Run cargo test. Make the test fail on purpose and observe the output.
Actual output:
Finished test [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/lib.rs (target/debug/deps/example_22_unittests-85413785cff54cd1) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests example-22-unittests running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
What we learned:
- Unit tests are written in the same file and can be run with cargo test
-
Example 23: macro §
Reproduction steps:
- Go to directory example-23-macro
- Recognize that there are three kinds of macros in rust:
- Procedural macro like #[derive(Debug)] (can generate code for a type in the TokenStream for struct and enum) (slow, complex, and super-powerful)
- Attribute-like macro like #[test] from the previous example (can also generate code in the TokenStream but for arbitrary items and can only access this item) (slow, complex, and powerful)
- Function-like macros like println!("") recognizable by the exclamation mark. They are fast, use a custom syntax, and are limited in their capabilities.
- Run cargo run
- Study the file src/main.rs
- Use an integer instead of a string in the call. Does it work? Guess why not.
Actual output:
Hello WorldWhat we learned:
- Unlike C macros, rust macros works with tokens, not text.
- literal specifies that only a literal is accepted. A string is a literal. An integer is not. You can use expr to accept any expression. You can use type to accept any type and so on.
-
Example 24: unsafe §
Reproduction steps:
- Go to directory example-24-unsafe
- Run cargo run and observe the program to crash
- Understand that there are unsafe blocks in rust.
- Read the five rust unsafe superpowers
- Understand that rust gives you five superpowers, but unsafe does not mean “nothing is checked”. All the lexical checks and type system checks are done normally. Some memory checks are skipped and the programmer is responsible for doing it right.
- The example accesses a raw pointer. There is actually a raw pointer type in rust, we did not discuss. But is only used for interfacing with embedded devices or C libraries.
Actual output:
thread 'main' panicked at 'misaligned pointer dereference: address must be a multiple of 0x8 but is 0xd00dcafe', src/main.rs:7:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
What we learned:
- unsafe skips some memory checks. The programmer needs to check some properties themselves. For example we need to ensure that some memory address is actually accessible before dereferencing a raw pointer.
-
Example 25: ferris §
Reproduction steps:
- Go to directory example-25-ferris
- Run cargo run in a terminal
- Observe the output
- Modify the variable name to use other non-latin characters
Actual output:
error: Ferris cannot be used as an identifier --> src/main.rs:2:9 | 2 | let 🦀 = "🦀"; | ^^ help: try using their name instead: `ferris` 3 | println!("Hello, world! {}", 🦀); | ^^ error: could not compile `example-25-ferris` (bin "gltttest") due to previous error
Expected output:
Hello, world! 🦀Hint to achieve the expected output:
Open the text file src/main.rs and modify itWhat we learned:
- Ferris is the name of rust's mascot
- The rust compiler has a dedicated error message for the Unicode character U+1F980 CRAB
- Rust files must be valid UTF-8 files, but
Credits §
- workshop lead
-
@meisterluk <admin@lukas-prokop.at>
I write digitaltypesetting software in rust
I maintain the litua and bibparser crates (and collaborated on others)
I used to present rust at RustGraz
Hire me for work with rust!
- organization
- thanks a lot to the GLT23 team!
- artwork
-
Most artwork taken from rustacean.net. rust's mascot is called Ferris.
This little square Ferris emoji was created by Dzuk licensed under the terms of CC BY-NC-SA