Module 5 Exercises — The Rust Core for Embedded Work

Back to the Course 2 syllabus. Read first: Module 5 lessons (Rust Book 3–10, 13, 18, 19 and the Embedded Rust Book’s Static Guarantees remain available as optional deep-dives).

Work in the labs repo’s course2/ folder — rust/host for everything that runs natively, rust/qemu for the one no_std exercise that boots in QEMU, c/host for the C halves — and record everything in m5/notes.md. Everything in this module runs on the Mac: cargo build/cargo run/cargo test for the host crate, cargo run --bin ex-5-5 --target thumbv7m-none-eabi for the QEMU crate, and clang with sanitizers for the C comparisons. Exercise 5.5 may additionally be flashed to the NUCLEO through rust/mcu as an optional last step. Predicted cells are filled in before building; observed cells at the machine.

Exercises

Exercise 5.1 — The same module, twice. Write a bounded single-producer/single-consumer byte ring buffer of capacity 64 in C (c/host/src/ex-5-1/ring.c + ring.h + a main.c driver) and in Rust (rust/host/src/bin/ex-5-1.rs, with the buffer as a struct Ring<const N: usize> and push/pop/len/is_full methods). The C API uses the conventions K&R left you — a pointer-plus-length init, int return codes, an uint8_t *out out-parameter; the Rust API uses lessons §2–§3 — push(&mut self, u8) -> Result<(), u8> that hands a full buffer’s byte back, pop(&mut self) -> Option<u8>, and no out-parameters. Drive both with the same sequence: fill, overfill by one, drain, pop from empty, interleave. Then complete the table by inspection of your own two sources:

Property C version — where is it enforced? Rust version — where is it enforced?
Buffer cannot be used before init
Capacity cannot be exceeded
Pop from empty is detectable
Caller cannot alias the buffer while a method mutates it
Index cannot exceed the array
Forgetting to check the return of push

In notes.md: one paragraph on which rows moved from “convention/comment” to “compile error”, and which stayed a run-time check in both.

Exercise 5.2 — A Result-based driver API. Model a fake I²C temperature sensor as a struct wrapping a simulated bus (rust/host/src/bin/ex-5-2.rs): the bus is a type you write whose write_read fails on a scripted schedule (NACK on the third transaction, timeout on the fifth). Define enum Error { Nack, Timeout, BadConfig(u16), OutOfRange(i16) }, implement From<BusError> for Error, and write configure(&mut self, cfg: Config) -> Result<(), Error>, read_raw(&mut self) -> Result<i16, Error>, and read_celsius(&mut self) -> Result<f32, Error> so that every bus failure propagates with ? and every driver-level failure (BadConfig, OutOfRange) is produced by your own checks. Match on the error at the top level and print a distinct line per variant. Then write the C equivalent (c/host/src/ex-5-2/) with an enum drv_err return code and the same scripted bus, and fill in:

Failure path Rust: how the caller learns of it C: how the caller learns of it What happens if the caller ignores it
NACK on transaction 3
Timeout on transaction 5
Configuration rejected
Reading out of range

Deliverable: both sources, the output transcript of each, and a notes.md paragraph on what #[must_use] and ? did that C’s if (rc != 0) discipline requires by hand.

Exercise 5.3 — Overflow semantics, predict-verify. In rust/host/src/bin/ex-5-3.rs, evaluate each expression below under cargo run (debug) and cargo run --release, and once more in release with overflow-checks = true set in a custom profile. Predict every cell before running — including which cells are a panic, and which panic in both profiles:

Expression Debug Release Release + overflow-checks wrapping_* / checked_* / saturating_* equivalent
250u8 + 10
i16::MIN - 1
1u32 << 32
i32::MIN / -1
7u8 / 0 (divisor from a runtime variable)
(300u16) as u8
(-1i8) as u8 and (200u8) as i8
1e10f32 as i32, f32::NAN as i32
u8::try_from(300u16)

Then write the same battery in C (c/host/src/ex-5-3/) using uint8_t/int16_t/uint32_t/int32_t, compile at -O0 and -O2, and run under -fsanitize=undefined. Add two columns to your notes.md copy of the table — C at −O2 and C sanitizer verdict — and mark each row as defined-and-same-in-both, defined-in-Rust-only, or UB-in-C. Close with the timer-delta idiom: write elapsed for a wrapping 32-bit tick counter in both languages such that neither the sanitizer nor the debug build objects.

Exercise 5.4 — Generics vs. trait objects, in the disassembly. Define trait Sensor { fn read(&mut self) -> i16; } and two implementors with deliberately different bodies (one returns a constant, one steps a counter). Write fn sum_n<S: Sensor>(s: &mut S, n: usize) -> i32 and fn sum_n_dyn(s: &mut dyn Sensor, n: usize) -> i32 with identical bodies, call each with both implementors, and mark all four call paths #[inline(never)] so they survive as symbols. Build --release and read cargo objdump --bin ex-5-4 --release -- -d --no-show-raw-insn:

Question Predicted Observed
How many copies of sum_n exist in the binary?
How many copies of sum_n_dyn?
Is the read call in sum_n direct, indirect, or gone (inlined)?
Same question for sum_n_dyn
Which is larger by cargo size, and by roughly what fraction? qualitative only

Deliverable: the annotated listings and a notes.md decision rule — one sentence each — for when a driver API should be generic and when it should take &dyn.

Exercise 5.5 — A no_std ring buffer in QEMU. Port Exercise 5.1’s Rust ring buffer into rust/qemu/src/bin/ex-5-5.rs as a #![no_std] #![no_main] binary: the buffer is a static or a stack local (record which and why), capacity is a const generic, and the driver loop pushes a known pattern and pops it back, reporting mismatches over semihosting with hprintln!. Then break it on purpose three ways, each as a separate build: (a) index past the array with a computed index, (b) unwrap() a pop() from empty, (c) overflow a u8 counter in a debug build. For each, predict what QEMU shows — the panic message text, which #[panic_handler] ran, whether execution halted or QEMU exited — and record it:

Deliberate fault Predicted behavior in QEMU Observed Which lesson §6 setting decided it
Out-of-bounds index
unwrap on None
u8 overflow (debug)

Swap panic-halt for a panic handler you write yourself that prints the PanicInfo location via semihosting, and confirm the file/line reported. Optional rung: build the same source for rust/mcu with panic-probe and run it on the NUCLEO through probe-rs, recording the RTT output.

Exercise 5.6 — dot_q15 three ways. In rust/host/src/bin/ex-5-6.rs, implement the Module 0 kernel as (1) an indexed for i in 0..n loop over two slices, (2) the iter().zip().map().sum() chain from lessons §10, and (3) a chunks_exact(4) version that accumulates four products per iteration. Verify all three agree on random inputs against a plain i64 reference. Build --release, mark each #[inline(never)], and read the three loop bodies:

Version Bounds check inside the loop? (predicted / observed) Vectorized? (predicted / observed) Loads per iteration (observed)
Indexed loop … / … … / …
Iterator chain … / … … / …
chunks_exact(4) … / … … / …

Cross-compile the same file’s kernel functions for the Cortex-M4 (cargo build -p mcu --target thumbv7em-none-eabihf --release with the kernels moved into a library module the mcu crate can also use) and repeat the reading for Thumb-2. In notes.md: which version you would ship on each target and why the answer is not “the fastest one”.

Exercise 5.7 — Clippy- and Miri-clean. Take the sources of Exercises 5.1, 5.2, and 5.6 and add, at the top of each binary, #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] and #![warn(clippy::pedantic)]. Run cargo clippy --all-targets -- -D warnings and triage every diagnostic: fix it, or allow it locally with #[allow(...)] and a one-line justification comment. Then add unit tests for the ring buffer and the driver (#[cfg(test)] mod tests) and run them under cargo miri test (nightly only: rustup +nightly component add miri, then cargo +nightly miri test). Complete the triage log:

Lint / Miri report Where Fix or justified allow? Why

Deliverable: the clean cargo clippy and cargo miri test transcripts, the triage log, and the closing notes.md paragraph of the module — the list of categories of defect the Rust compiler or its lints caught in code you had believed correct after writing the C version first, and the one category that neither language caught for you.