Unit 1.1 - Introduction
Exercise 1.1.1: Setup Your Installation
In this file you'll find instructions on how to install the tools we'll use during the course.
All of these tools are available for Linux, macOS and Windows users. We'll need the tools to write and compile our Rust code, and allow for remote mentoring. Important: these instructions are to be followed at home, before the start of the first tutorial. If you have any problems with installation, contact the lecturers! We won't be addressing installation problems during the first tutorial.
Rust and Cargo
First we'll need rustc
, the standard Rust compiler.
rustc
is generally not invoked directly, but through cargo
, the Rust package manager.
rustup
takes care of installing rustc
and cargo
.
This part is easy: go to https://www.rust-lang.org/learn/get-started and follow the instructions. Please make sure you're installing the latest default toolchain. Once done, run Alternatively, some linux distributions have rust packaged.
rustc -V && cargo -V
The output should be something like this:
rustc 1.67.1 (d5a82bbd2 2023-02-07)
cargo 1.67.1 (8ecd4f20a 2023-01-10)
Using Rustup, you can install Rust toolchains and components. More info:
Rustfmt and Clippy
To avoid discussions, Rust provides its own formatting tool, Rustfmt. We'll also be using Clippy, a collection of lints to analyze your code, that catches common mistakes for you. You'll notice that Rusts Clippy can be a very helpful companion. Both Rustfmt and Clippy are installed by Rustup by default.
To run Rustfmt on your project, execute:
cargo fmt
To run clippy:
cargo clippy
More info:
Visual Studio Code
During the course, we will use Visual Studio Code (vscode) to write code in. Of course, you're free to use your favorite editor, but if you encounter problems, you can't rely on support from us. Also, we'll use vscode to allow for remote collaboration and mentoring during tutorial sessions.
You can find the installation instructions here: https://code.visualstudio.com/.
We will install some plugins as well. The first one is Rust-Analyzer. Installation instructions can be found here https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer. Rust-Analyzer provides a lot of help during development and in indispensable when getting started with Rust.
The last plugin we'll use is CodeLLDB. This plugin enables debugging Rust code from within vscode. You can find instructions here: https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb.
More info:
Git
We will use Git as version control tool. If you haven't installed Git already, you can find instructions here: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git. If you're new to Git, you'll also appreciate GitHubs intro to Git https://docs.github.com/en/get-started/using-git/about-git and the Git intro with vscode, which you can find here: https://www.youtube.com/watch?v=i_23KUAEtUM.
More info: https://www.youtube.com/playlist?list=PLg7s6cbtAD15G8lNyoaYDuKZSKyJrgwB-
Course code
Now that everything is installed, you can clone the source code repository. The repository can be found here: https://gitlab.ethz.ch/sis/courses/rust/teach-rs.
Instructions on cloning the repository can be found here: https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls
Trying it out
Now that you've got the code on your machine, navigate to it using your favorite terminal and run:
cd exercises/1-course-introduction/1-introduction/1-setup-your-installation
cargo run
This command may take a while to run the first time, as Cargo will first fetch the crate index from the registry.
It will compile and run the intro
package, which you can find in exercises/1-course-introduction/1-introduction/1-setup-your-installation
.
If everything goes well, you should see some output:
Compiling intro v0.1.0 ([REDACTED]/exercises/1-course-introduction/1-introduction/1-setup-your-installation)
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/intro`
🦀 Hello, world! 🦀
You've successfully compiled and run your first Rust project!
If Rust-Analyzer is set up correctly, you can also click the '▶️ Run'-button that is shown in exercises/1-course-introduction/1-introduction/1-setup-your-installation/src/main.rs
.
With CodeLLDB installed correctly, you can also start a debug session by clicking 'Debug', right next to the '▶️ Run'-button.
Play a little with setting breakpoints by clicking on a line number, making a red circle appear and stepping over/into/out of functions using the controls.
You can view variable values by hovering over them while execution is paused, or by expanding the 'Local' view under 'Variables' in the left panel during a debug session.
Unit 2.1 - Basic Syntax
Exercise 2.1.1: Basic Syntax
Open exercises/2-foundations-of-rust/1-basic-syntax/1-basic-syntax
in your editor. This folder contains a number of exercises with which you can practise basic Rust syntax.
While inside the exercises/2-foundations-of-rust/1-basic-syntax/1-basic-syntax
folder, to get started, run:
cargo run --bin 01
This will try to compile exercise 1. Try and get the example to run, and continue on with the next exercise by replacing the number of the exercise in the cargo run command.
Some exercises contain unit tests. To run the test in src/bin/01.rs
, run
cargo test --bin 01
Make sure all tests pass!
Unit 2.2 - Ownership and References
Exercise 2.2.1: Move Semantics
This exercise is adapted from the move semantics exercise from Rustlings
While inside the exercises/2-foundations-of-rust/2-ownership-and-references/1-move-semantics
folder, to get started, run:
cargo run --bin 01
This will try to compile exercise 1. Try and get the example to run, and continue on with the next exercise by replacing the number of the exercise in the cargo run command.
Some exercises contain unit tests. To run the test in src/bin/01.rs
, run
cargo test --bin 01
Make sure all tests pass!
01.rs
should compile as is, but you'll have to make sure the others compile as well. For some exercises, instructions are included as doc comments at the top of the file. Make sure to adhere to them.
Exercise 2.2.2: Borrowing
Fix the two examples in the exercises/2-foundations-of-rust/2-ownership-and-references/2-borrowing
crate! Don't forget you
can run individual binaries by using cargo run --bin 01
in that directory!
Make sure to follow the instructions that are in the comments!
Unit 2.3 - Advanced Syntax
Exercise 2.3.1: Error propagation
Follow the instructions in the comments of exercises/2-foundations-of-rust/3-advanced-syntax/1-error-propagation/src/main.rs
!
Exercise 2.3.2: Error handling
Follow the instructions in the comments of exercises/2-foundations-of-rust/3-advanced-syntax/2-error-handling/src/main.rs
!
Exercise 2.3.3: Slices
Follow the instructions in the comments of exercises/2-foundations-of-rust/3-advanced-syntax/3-slices/src/main.rs
!
Don't take too much time on the extra assignment, instead come back later once
you've done the rest of the excercises.
Exercise 2.3.4: Ring Buffer
This is a bonus exercise! Follow the instructions in the comments of
exercises/2-foundations-of-rust/3-advanced-syntax/4-ring-buffer/src/main.rs
!
Exercise 2.3.5: Boxed Data
Follow the instructions in the comments of exercises/2-foundations-of-rust/3-advanced-syntax/5-boxed-data/src/main.rs
!
Unit 2.4 - Traits and Generics
Exercise 2.4.1: Local Storage Vec
In this exercise, we'll create a type called LocalStorageVec
, which is generic list of items that resides either on the stack or the heap, depending on its size. If its size is small enough for items to be put on the stack, the LocalStorageVec
buffer is backed by an array. LocalStorageVec
is not only generic over the type (T
) of items in the list, but also by the size (N
) of this stack-located array using a relatively new feature called "const generics". Once the LocalStorageVec
contains more items than fit in the array, a heap based Vec
is allocated as space for the items to reside in.
Within this exercise, the objectives are annotated with a number of stars (⭐), indicating the difficulty. You are likely not to be able to finish all exercises during the tutorial session
Questions
- When is such a data structure more efficient than a standard
Vec
? - What are the downsides, compared to just using a
Vec
?
Open the exercises/2-foundations-of-rust/4-traits-and-generics/1-local-storage-vec
crate. It contains a src/lib.rs
file, meaning this crate is a library. lib.rs
contains a number of tests, which can be run by calling cargo test
. Don't worry if they don't pass or even compile right now: it's your job to fix that in this exercise. Most of the tests are commented out right now, to enable a step-by-step approach. Before you begin, have a look at the code and the comments in there, they contain various helpful clues.
2.4.1.A Defining the type ⭐
Currently, the LocalStorageVec
enum
is incomplete. Give it two variants: Stack
and Heap
. Stack
contains two named fields, buf
and len
. buf
will be the array with a capacity to hold N
items of type T
; len
is a field of type usize
that will denote the amount of items actually stored. The Heap
variant has an unnamed field containing a Vec<T>
. If you've defined the LocalStorageVec
variants correctly, running cargo test
should output something like
running 1 test
test test::it_compiles ... ignored, This test is just to validate the definition of `LocalStorageVec`. If it compiles, all is OK
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
This test does (and should) not run, but is just there for checking your variant definition.
Hint 1
You may be able to reverse-engineer the `LocalStorageVec` definition using the code of the `it_compiles` test case.Hint 2 (If you got stuck, but try to resist me for a while)
Below definition works. Read the code comments and make sure you understand what's going on.
#![allow(unused)] fn main() { // Define an enum `LocalStorageVec` that is generic over // type `T` and a constant `N` of type `usize` pub enum LocalStorageVec<T, const N: usize> { // Define a struct-like variant called `Stack` containing two named fields: // - `buf` is an array with elements of `T` of size `N` // - `len` is a field of type `usize` Stack { buf: [T; N], len: usize }, // Define a tuple-like variant called `Heap`, containing a single field // of type `Vec<T>`, which is a heap-based growable, contiguous list of `T` Heap(Vec<T>), } }
2.4.1.B impl
-ing From<Vec<T>>
⭐
Uncomment the test it_from_vecs
, and add an implementation for From<Vec<T>>
to LocalStorageVec<T>
. To do so, copy the following code in your lib.rs
file and replace the todo!
macro invocation with your code that creates a heap-based LocalStorageVec
containing the passed Vec<T>
.
#![allow(unused)] fn main() { impl<T, const N: usize> From<Vec<T>> for LocalStorageVec<T, N> { fn from(v: Vec<T>) -> Self { todo!("Implement me"); } } }
Question
- How would you pronounce the first line of the code you just copied in English?*
Run cargo test
to validate your implementation.
2.4.1.C impl LocalStorageVec
⭐⭐
To make the LocalStorageVec
more useful, we'll add more methods to it.
Create an impl
-block for LocalStorageVec
.
Don't forget to declare and provide the generic parameters.
For now, to make implementations easier, we will add a bound T
, requiring that it implements Copy
and Default
.
First off, uncomment the test called it_constructs
.
Make it compile and pass by creating a associated function called new
on LocalStorageVec
that creates a new, empty LocalStorageVec
instance without heap allocation.
The next methods we'll implement are len
, push
, pop
, insert
, remove
and clear
:
len
returns the length of theLocalStorageVec
push
appends an item to the end of theLocalStorageVec
and increments its length. Possibly moves the contents to the heap if they no longer fit on the stack.pop
removes an item from the end of theLocalStorageVec
, optionally returns it and decrements its length. If the length is 0,pop
returnsNone
insert
inserts an item at the given index and increments the length of theLocalStorageVec
remove
removes an item at the given index and returns it.clear
resets the length of theLocalStorageVec
to 0.
Uncomment the corresponding test cases and make them compile and pass. Be sure to have a look at the methods provided for slices [T]
and Vec<T>
Specifically, [T]::copy_within
and Vec::extend_from_slice
can be of use.
2.4.1.E Iterator
and IntoIterator
⭐⭐
Our LocalStorageVec
can be used in the real world now, but we still shouldn't be satisfied. There are various traits in the standard library that we can implement for our LocalStorageVec
that would make users of our crate happy.
First off, we will implement the IntoIterator
and Iterator
traits. Go ahead and uncomment the it_iters
test case. Let's define a new type:
#![allow(unused)] fn main() { pub struct LocalStorageVecIter<T, const N: usize> { vec: LocalStorageVec<T, N>, counter: usize, } }
This is the type we'll implement the Iterator
trait on. You'll need to specify the item this Iterator
implementation yields, as well as an implementation for Iterator::next
, which yields the next item. You'll be able to make this easier by bounding T
to Default
when implementing the Iterator
trait, as then you can use the std::mem::take
function to take an item from the LocalStorageVec
and replace it with the default value for T
.
Take a look at the list of methods under the 'provided methods' section. In there, lots of useful methods that come free with the implementation of the Iterator
trait are defined, and implemented in terms of the next
method. Knowing in the back of your head what methods there are, greatly helps in improving your efficiency in programming with Rust. Which of the provided methods can you override in order to make the implementation of LocalStorageVecIter
more efficient, given that we can access the fields and methods of LocalStorageVec
?
Now to instantiate a LocalStorageVecIter
, implement the [IntoIter
] trait for it, in such a way that calling into_iter
yields a LocalStorageVecIter
.
2.4.1.F Index
⭐⭐
To allow users of the LocalStorageVec
to read items or slices from its buffer, we can implement the Index
trait. This trait is generic over the type of the item used for indexing. In order to make our LocalStorageVec
versatile, we should implement:
Index<usize>
, allowing us to get a single item by callingvec[1]
;Index<RangeTo<usize>>
, allowing us to get the firstn
items (excluding itemn
) by callingvec[..n]
;Index<RangeFrom<usize>>
, allowing us to get the lastn
items by callingvec[n..]
;Index<Range<usize>>
, allowing us to get the items betweenn
andm
items (excluding itemm
) by callingvec[n..m]
;
Each of these implementations can be implemented in terms of the as_ref
implementation, as slices [T]
all support indexing by the previous types. That is, [T]
also implements Index
for those types. Uncomment the it_indexes
test case and run cargo test
in order to validate your implementation.
2.4.1.G Removing bounds ⭐⭐
When we implemented the borrowing Iterator
, we saw that it's possible to define methods in separate impl
blocks with different type bounds. Some of the functionality you wrote used the assumption that T
is both Copy
and Default
. However, this means that each of those methods are only defined for LocalStorageVec
s containing items of type T
that in fact do implement Copy
and Default
, which is not ideal. How many methods can you rewrite having one or both of these bounds removed?
2.4.1.H Borrowing Iterator
⭐⭐⭐
We've already got an iterator for LocalStorageVec
, though it has the limitation that in order to construct it, the LocalStorageVec
needs to be consumed. What if we only want to iterate over the items, and not consume them? We will need another iterator type, one that contains an immutable reference to the LocalStorageVec
and that will thus need a lifetime annotation. Add a method called iter
to LocalStorageVec
that takes a shared &self
reference, and instantiates the borrowing iterator. Implement the Iterator
trait with the appropriate Item
reference type for your borrowing iterator. To validate your code, uncomment and run the it_borrowing_iters
test case.
Note that this time, the test won't compile if you require the items of LocalStorageVec
be Copy
! That means you'll have to define LocalStorageVec::iter
in a new impl
block that does not put this bound on T
:
#![allow(unused)] fn main() { impl<T: Default + Copy, const N: usize> LocalStorageVec<T, N> { // Methods you've implemented so far } impl<T: const N: usize> LocalStorageVec<T, N> { pub fn iter(&self) -> /* TODO */ } }
Defining methods in separate impl
blocks means some methods are not available for certain instances of the generic type. In our case, the new
method is only available for LocalStorageVec
s containing items of type T
that implement both Copy
and Default
, but iter
is available for all LocalStorageVec
s.
2.4.1.I Generic Index
⭐⭐⭐⭐
You've probably duplicated a lot of code in exercise 2.4.1.F. We can reduce the boilerplate by defining an empty trait:
#![allow(unused)] fn main() { trait LocalStorageVecIndex {} }
First, implement this trait for usize
, RangeTo<usize>
, RangeFrom<usize>
, and Range<usize>
.
Next, replace the multiple implementations of Index
with a single implementation. In English:
"For each type T
, I
and constant N
of type usize
,
implement Index<I>
for LocalStorageVec<T, N>
,
where I
implements LocalStorageVecIndex
and [T]
implements Index<I>
"
If you've done this correctly, it_indexes
should again compile and pass.
2.4.1.J Deref
and DerefMut
⭐⭐⭐⭐
The next trait that makes our LocalStorageVec
more flexible in use are Deref
and DerefMut
that utilize the 'deref coercion' feature of Rust to allow types to be treated as if they were some type they look like.
That would allow us to use any method that is defined on [T]
by calling them on a LocalStorageVec
.
Before continuing, read the section 'Treating a Type Like a Reference by Implementing the Deref Trait' from The Rust Programming Language (TRPL).
Don't confuse deref coercion with any kind of inheritance! Using Deref
and DerefMut
for inheritance is frowned upon in Rust.
Below, an implementation of Deref
and DerefMut
is provided in terms of the AsRef
and AsMut
implementations. Notice the specific way in which as_ref
and as_mut
are called.
#![allow(unused)] fn main() { impl<T, const N: usize> Deref for LocalStorageVec<T, N> { type Target = [T]; fn deref(&self) -> &Self::Target { <Self as AsRef<[T]>>::as_ref(self) } } impl<T, const N: usize> DerefMut for LocalStorageVec<T, N> { fn deref_mut(&mut self) -> &mut Self::Target { <Self as AsMut<[T]>>::as_mut(self) } } }
Question
- Replacing the implementation of
deref
withself.as_ref()
results in a stack overflow when running an unoptimized version. Why? (Hint: deref coercion)
Unit 2.5 - Closures and Dynamic dispatch
Exercise 2.5.1: Config Reader
In this exercise, you'll work with dynamic dispatch to deserialize with serde_json
or serde_yaml
, depending on the file extension. The starter code is in exercises/2-foundations-of-rust/5-closures-and-dynamic-dispatch/1-config-reader
. Fix the todo's in there.
To run the program, you'll need to pass the file to deserialize to the binary, not to Cargo. To do this, run
cargo run -- <FILE_PATH>
Deserializing both config.json
and config.yml
should result in the Config
being printed correctly.
Unit 2.6 - Interior mutability
There are no exercises for this unit
Unit 3.1 - Introduction to Multitasking
There are no exercises for this unit
Unit 3.2 - Parallel Multitasking
Exercise 3.2.1: TF-IDF
Follow the instructions in the comments of exercises/3-multitasking/2-parallel-multitasking/1-tf-idf/src/main.rs
!
Exercise 3.2.2: Mutex
The basic mutex performs a spin-loop while waiting to take the lock. That is terribly inefficient. Luckily, your operating system is able to wait until the lock becomes available, and will just put the thread to sleep in the meantime.
This functionality is exposed in the atomic_wait crate. The section on implementing a mutex from "Rust Atomics and Locks" explains how to use it.
- change the
AtomicBool
for aAtomicU32
- implement
lock
. Be careful about spurious wakes: afterwait
returns, you must stil check the condition - implement unlocking (
Drop for MutexGuard<T>
usingwake_one
.
The linked chapter goes on to further optimize the mutex. This is technically out of scope for this course, but we won't stop you if you try (and will still try to help if you get stuck)!
Unit 3.3 - Asynchronous Multitasking
Exercise 3.3.1: Async Channels
Channels are a very useful way to communicate between threads and async
tasks. They allow for decoupling your application into many tasks. You'll see how that can come in nicely in exercise E.2. In this exercise, you'll implement two variants: a oneshot channel and a multi-producer-single-consumer (MPSC) channel. If you're up for a challenge, you can write a broadcast channel as well.
3.3.1.A MPSC channel ⭐⭐
A multi-producer-single-consumer (MPSC) channel is a channel that allows for multiple Sender
s to send many messages to a single Receiver
.
Open exercises/3-multitasking/3-asynchronous-multitasking/1-async-channels
in your editor. You'll find the scaffolding code there. For part A, you'll work in src/mpsc.rs
. Fix the todo!
s in that file in order to make the test pass. To test, run:
cargo test -- mpsc
If your tests are stuck, probably either your implementation does not use the Waker
correctly, or it returns Poll::Pending
where it shouldn't.
3.3.1.B Oneshot channel ⭐⭐⭐
A oneshot is a channel that allows for one Sender
to send exactly one message to a single Receiver
.
For part B, you'll work in src/broadcast.rs
. This time, you'll have to do more yourself. Intended behavior:
Receiver
implementsFuture
. It returnsPoll::Ready(Ok(T))
ifinner.data
isSome(T)
,Poll::Pending
ifinner.data
isNone
, andPoll::Ready(Err(Error::SenderDropped))
if theSender
was dropped.Receiver::poll
replacesinner.waker
with the one from theContext
.Sender
consumesself
on send, allowing the it to be used no more than once. Sending setsinner.data
toSome(T)
. It returnsErr(Error::ReceiverDropped(T))
if theReceiver
was dropped before sending.Sender::send
wakesinner.waker
after putting the data ininner.data
- Once the
Sender
is dropped, it marks itself dropped withinner
- Once the
Receiver
is dropped, it marks itself dropped withinner
- Upon succesfully sending the message, the consumed
Sender
is not marked as dropped. Insteadstd::mem::forget
is used to avoid running the destructor.
To test, run:
cargo test -- broadcast
3.3.1.C Broadcast channel (bonus) ⭐⭐⭐⭐
A Broadcast channel is a channel that supports multiple senders and receivers. Each message that is sent by any of the senders, is received by every receiver. Therefore, the implemenentation has to hold on to messages until they have been sent to every receiver that has not yet been dropped. This furthermore implies that the message shoud be cloned upon broadcasting.
For this bonus exercise, we provide no scaffolding. Take your inspiration from the mpsc
and oneshot
modules, and implement a broadcast
module yourself.
Exercise 3.3.2: Async Chat
In this exercise, you'll write a simple chat server and client based on Tokio. Open exercises/3-multitasking/3-asynchronous-multitasking/2-async-chat
in your editor. The project contains a lib.rs
file, in which a type Message
resides. This Message
defines the data the chat server and clients use to communicate.
3.3.2.A Server ⭐⭐⭐
The chat server, which resides in src/bin/server.rs
listens for incoming TCP connections on port 8000, and spawns two tasks (futures):
handle_incoming
: reads lines coming in from the TCP connection. It reads the username the client provides, and broadcasts incomingMessages
, possibly after some modification.handle_outgoing
: sends messages that were broadcasted by thehandle_incoming
tasks to the client over TCP.
Both handle_incoming
and handle_outgoing
contain a number to todo
s. Fix them.
To start the server, run
cargo run --bin server
3.3.2.B Client ⭐⭐
The chat client, residing in src/bin/client.rs
contains some todo's as well. Fix them to allow for registration and sending Message
s to the server.
To start the client, run
cargo run --bin client
If everything works well, you should be able to run multiple clients and see messages sent from each client in every other.
Unit 4.1 - Scientific Computing with Rust
Exercise 4.1.1: PyO3
Write a custom python extension using PyO3.
Python is a convenient and popular language, but it is not fast. By writing complex logic in faster languages, you can get the best of both worlds. PyO3 makes it extremely easy to write and distribute python extensions written in Rust.
PyO3 and SIMD
PyO3 makes it easy to write python extensions in rust. The code for this exercise is a skeleton, taken from the PyO3 documentation.
you should be able to run this example like so from the repository root:
folkertdev@folkertdev ~/t/teach-rs (mod-g)> cargo build -p pyo3-simd
Compiling pyo3-simd v0.1.0 (/home/folkertdev/tg/teach-rs/exercises/G/4-pyo3)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
folkertdev@folkertdev ~/t/teach-rs (mod-g)> cp target/debug/libpointwise_simd.so pointwise_simd.so
folkertdev@folkertdev ~/t/teach-rs (mod-g)> python3
Python 3.8.5 (default, May 27 2021, 13:30:53)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pointwise_simd
>>> dir(pointwise_simd)
['__all__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sum_as_string']
>>> pointwise_simd.sum_as_string(4,5)
'9'
>>>
Our goal is to implement pointwise addition of two python lists of floats in rust using SIMD instructions.
- hook up the
pointwise_sum
pyfunction, it should callpointwise_sum_simd
. It is easiest to useVec<f64>
in the interface. - next run
cargo test -p pyo3-simd
. This should compile, but the test fails. Use the given simd functions and pointer offsets to implement thepointwise_sum_simd
correctly. - verify that this works from python.
If that succeeded: congrats, you can now write arbitrary python extensions, and speed up python code. Rust and PyO3 make this really straightforward.
Unit 5.1 - Rust for Web
Exercise 5.1.1: Pastebin
This exercise is about writing a simple pastebin web server. Like the quizzer app, you will need to set up the project yourself. This webserver will be powered by axum
.
- Data is kept in memory. Bonus if you use a database or
sqlite
, but first make the app function properly without. - Expose a route to which a POST request can be sent, that accepts some plain text, and stores it along with a freshly generated UUID. The UUID is sent in the response. You can use the
uuid
crate to generate UUIDs. - Expose a route to which a GET request can be sent, that accepts a UUID and returns the plain text corresponding to the UUID, or a 404 error if it doesn't exist.
- Expose a route to which a DELETE request can be sent, that accepts a UUID and deletes the plain text corresonding to that UUID.
Unit 6.1 - Embedded wasm apps
Exercise 6.1.1: Web Assembly animation
Embed a wasm animation into a static web page.
We will use wasm-bindgen to interface with javascript.
Setup
cargo install wasm-pack
or get it here: https://rustwasm.github.io/wasm-pack/installer/.
Project skeleton
wasm-animation/
├── Cargo.toml
├── description.md
├── src
│ ├── lib.rs
│ └── animation.rs
└── static
└── index.html
At the beginning, the project contains a rust wasm library exporting a function
draw_play_button
which does nothing but to pop up an alert box.
The source tree also contains src/animation.rs
, but this file is not used
by lib.rs
at the beginning.
To compile the project (here, without using npm), use
wasm-pack build --target web --release --no-pack --no-typescript -d public/pkg
The file static/index.html
serves as the entry point, but currently
does not yet load the wasm bundle. Instead it displays "Click to start"
on a <canvas>
element.
If the user clicks on the canvas, the elapsed time after
loading the page will be displayed in an animation loop.
The animation loop is implemented by using
requestAnimationFrame().
Note: You cannot directly open index.html
in your web browser due
to CORS
limitations. Instead, you can set up a quick development
environment using either Python's built-in HTTP server:
python -m http.server -d static/
or by using miniserve:
cargo install miniserve
miniserve static --index "static/index.html" -p 8080
Step 1: Integrate the wasm bundle into index.html
-
At the beginning of
<script type="module">
, addimport init, { draw_play_button } from "./pkg/wasm_animation.js";
-
Call
await init();
as the first statement ofasync function run() {
, -
Call
draw_play_button();
as the 2nd statement, -
Symlink or copy
static/index.html
topublic/
, - Compile / test the result in the browser. A alert popup should appear when loading the page.
Step 2: Replace the alert popup by actual logic
Here is javascript code which draws a play button on the canvas element:
function drawPlayButton() {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
const cx = canvas.width/2
const cy = canvas.height/2;
const r = 0.4*Math.min(canvas.width, canvas.height);
ctx.lineWidth = 0.2 * r;
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "black";
ctx.beginPath();
const r_triangle = 0.7 * r;
const sin_60 = Math.sqrt(3)/2;
const cos_60 = 0.5;
ctx.moveTo(cx-r_triangle*cos_60, cy+r_triangle*sin_60);
ctx.lineTo(cx-r_triangle*cos_60, cy-r_triangle*sin_60);
ctx.lineTo(cx+r_triangle, cy);
ctx.closePath();
ctx.fill();
}
Translate it to rust, replacing alert("TODO");
.
-
Remove
alert("TODO");
and theextern "C"
block, -
Start with
let document = web_sys::window().unwrap().document().unwrap(); let canvas = document.get_element_by_id("canvas").unwrap(); let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().unwrap(); let ctx = canvas .get_context("2d") .unwrap() .unwrap() .dyn_into::<web_sys::CanvasRenderingContext2d>() .unwrap();
-
Translation involves:
ctx.fillStyle = ...
->ctx.set_fill_style(&JsValue::from_str(...))
,ctx.lineWidth = ...
->ctx.set_line_width(...)
,canvas.width
->canvas.width() as f64
(same forheight
),ctx.camelCase(...)
->ctx.snake_case(...)
,ctx.arc
needs an.unwrap()
.
-
In
static/index.html
: Replace the calls todraw_initial_text()
withdraw_play_button();
and remove its definition, - Compile / test it in the browser.
Step 3: Replace the js "animation" by the prepared rust animation
-
Make the
animation
module available tolib
, by inserting
at the top of// wasm independent logic: mod animation;
src/lib.rs
, -
Wrap the
animation::Animation
struct in a new structAnimation
inlib.rs
- exporting it withwasm_bindgen
:
Here, we also persist a#[wasm_bindgen] pub struct Animation { animation: animation::Animation, context: web_sys::CanvasRenderingContext2d, }
web_sys::CanvasRenderingContext2d
instance, so we do not have to fetch it all the time from the DOM. -
Add exported functionality to
Animation
:#[wasm_bindgen] impl Animation { pub fn new() -> Result<Animation, JsValue> { // Initialize context and animation Ok(Self { animation, context }) } /// Advance one time step and draw pub fn step(&mut self, dt: f64) { // Call self.animation.step and self.render } /// Render the current state of the animation to the canvas pub fn render(&self) { // - Clear the canvas // - Draw a bounding box (canvas width / height) // - Draw all balls in `self.animation.balls` } }
-
In
index.html
, addAnimation
to theimport
statement (inside the curly braces), -
Call
const animation = Animation.new();
andanimation.render();
directly afterawait init();
, -
Remove
function animation()
, -
Replace the call
animation(t)
withanimation.step(t - state.t0)
, - Test it in the browser.
Step 4 (challenge): Translate the rest of the js code to rust
Goal: the <script>
element in index.html
should look like:
<script type="module">
import init from "./pkg/wasm_demo.js";
init();
</script>
and all the initializing logic should go to
#[wasm_bindgen(start)]
fn run() {
// init logic
}
Hint: Have a look at this example.
Unit 6.2 - Full Stack web Apps
Exercise 6.2.1: Full Stack web App using dioxus
This exercise is about writing a full stack web application.
We refer to the Rust Full Stack Workshop.
Be aware that the API of dioxus changed slightly in the mean time.
Unit 6.3 - wasm containers
Exercise 6.3.1: Web Assembly container
Demo / Preview: wasm container
This is a quick demo of a web container (source: wasmedge.org). At the moment support in container engines is experimental but already can be tried out locally.
Build locally
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release
The result can be directly tested in a wasm runtime, e.g. wasmedge.
Build the container
In the following podman
is representative for docker
or buildah
depending on your setup:
podman build --annotation "module.wasm.image/variant=compat-smart" -t wasm-server .
Run the container
This needs crun
built with wasm support.
podman run --rm -it --annotation module.wasm.image/variant=compat-smart wasm-server
k8s
If you have access to a cluster with the crun
container runtime,
the following will declare a runtime class which can be used by pods / deployments:
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
scheduling:
nodeSelector:
runtime: crun
handler: crun
After it is applied to the cluster, the following should work:
kubectl apply -f deployment.yml
Unit 7.1 - CLI
Exercise 7.1.1: Build a command line interface with clap
Write a command line interface to for the prepared "gumpiball" project. It can run a simulation of moving 2d balls confined in a box (the balls dont interact with each other). It either animates the simulation in a window or saves the trajectories of balls to a svg file:
Use clap to add a command line
interface to src/main.rs
.
The interface should be able to do the following:
-
Show the usage:
gumpiball --help
You will get this for free when using clap,
-
Run an animation with
n
(default: 20) balls in a window with dimensions<width> x <height>
(defaults:400.0
and400.0
):gumpiball ui [-n <n>] [--width <width>] [--height <height>] [--paused]
For parsed
width
,height
,n
,paused
(boolean flag if omitted defaults tofalse
), this corresponds tolet animation = crate::animation::Animation::new(width, height, n); crate::ui::run(animation, paused)?;
-
Load an animation setup from a json file, run it in memory and save the trajectory to a svg file:
gumpiball trace -o <out> <config> [--dt <timestep>] [-n <n_snapshots>]
For parsed
out
andconfig
(json file),dt
,n
, this corresponds tolet animation = crate::animation::Animation::load_from_json(&std::path::PathBuf::from(config))?; let trajectory = crate::animation::Trajectory::track(animation, 0., n as f64 * dt, dt); crate::graphics::svg::save_to_svg(&trajectory, &std::path::PathBuf::from(out))?;