>(())
});
let mut sum = 0;
while let Some(id) = receiver.recv().await {
sum += id;
}
producer.await??;
assert_eq!(sum, 6);
Ok(())
}
```
[View the tested source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/20_async.rs)
The macro builds a current-thread Tokio runtime. A producer task sends four values into a bounded channel. The consumer drains it, and the producer's sender is dropped at completion. The join handle reports whether the task failed and contains the producer's own result. In `await??`, the first `?` checks the task result; the second checks the producer's result.
## Limit queued work
A channel with capacity two creates backpressure: a send can wait for space. This bounds queued messages, not the size of each message or the total number of tasks elsewhere in the program. A production design must bound those separately.
Tokio schedules cooperatively. A long computation or blocking filesystem call inside a task can occupy the runtime thread and prevent other tasks from progressing. Async syntax alone does not make a function nonblocking. Use an appropriate async API, a dedicated worker, or `spawn_blocking` with a deliberate concurrency limit. A started blocking task generally cannot be aborted just by aborting its async handle.
## Check what cancellation leaves behind
Selecting between two futures drops a losing future in common patterns. Ask what progress it may have made and whether retrying loses or duplicates work. Tokio's `mpsc::Receiver::recv` has documented cancellation behavior useful in select loops. Other operations, such as reading an exact number of bytes, may have consumed partial input before cancellation.
A timeout is not rollback. Dropping a `JoinHandle` detaches the task; call `abort` when appropriate and await the handle to observe completion. Abortion takes effect when the task yields control, and destructors are part of cleanup. If cleanup needs async work, write a shutdown step that awaits it. Ordinary `Drop` cannot await.
## Exercise
Write a deterministic timeout test using Tokio's paused test clock. Make a receiver wait on an empty channel while a sender still exists; advance time and assert a timeout. Then drop the sender and assert that `recv` returns `None`. Use the repository's async tests as a reference after trying it.
Solution and acceptance check
Use `#[tokio::test(start_paused = true)]` with a current-thread test runtime and `tokio::time::timeout`. Paused time lets the runtime advance to the next timer when idle, avoiding fragile wall-clock sleeps. Retaining a sender prevents “channel closed” from winning first. Once the final sender is dropped, the receiver terminates. These are different outcomes and should have separate assertions.
Sources: [Tokio channels](https://tokio.rs/tokio/tutorial/channels), [shutdown](https://tokio.rs/tokio/topics/shutdown), [JoinHandle](https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html), and Alice Ryhl's [blocking explanation](https://ryhl.io/blog/async-what-is-blocking/).
---
# Pin and async traits
Learn what pinning protects and how to return futures through a trait object.
Canonical: https://rust.robertdevore.com/course/21-pin/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## What Pin keeps in place
Some values become address-sensitive, including futures that may contain references into their own suspended state. `Pin` constrains how the pointee accessed through pointer `P` may be moved. It does not mean the pointer handle itself cannot move, and it does not automatically allocate anything.
For `T: Unpin`, moving the value does not violate a pinning invariant, so many pin restrictions become irrelevant. For a `!Unpin` value, safe APIs prevent operations that would violate the pin contract. The contract also involves destruction and storage validity; it is stronger than “do not call mem::swap right now”.
## Use safe constructors first
`pin!` pins a value in local storage. `Box::pin` owns a pinned allocation whose handle can move. Most async application code can use these or runtime helpers without implementing unsafe projection. Accessing a pinned struct's fields requires knowing which fields are structurally pinned. Do not use `get_unchecked_mut` just to bypass a compiler error.
### Run the example
```sh
cargo run --locked --example 21_pin
```
```rust
use std::{future::Future, pin::Pin};
trait Lookup {
fn get(&self) -> Pin + '_>>;
}
struct Fixed(usize);
impl Lookup for Fixed {
fn get(&self) -> Pin + '_>> {
Box::pin(async move { self.0 })
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let lookup = Fixed(42);
let service: &dyn Lookup = &lookup;
assert_eq!(service.get().await, 42);
}
```
[View the tested source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/21_pin.rs)
The interface returns a future in a pinned `Box`. The trait object hides its concrete type. Its lifetime is tied to `&self`, so it can borrow the service. The example uses a local executor and does not require `Send`. An API intended for movable spawned tasks may need `Send` on the returned future and appropriate bounds on the captured data.
## Calling async methods through trait objects
Native `async fn` in traits works on stable Rust for supported static-dispatch cases. That does not imply those methods can be dispatched directly through `dyn Trait`. Returning an explicitly erased future is a stable alternative, with allocation and indirection costs. The `async-trait` crate automates a related transformation; use it when you need this kind of interface.
**Intentionally fails on Rust 1.98.1.** Run `rustc --edition=2024 drills/async_dyn.rs` from the repository.
```rust
trait Fetch { async fn get(&self) -> usize; }
fn consume(_: &dyn Fetch) {}
fn main() {}
```
Actual compiler diagnostic · Rust 1.98.1
```text
error[E0038]: the trait `Fetch` is not dyn compatible
--> drills/async_dyn.rs:2:16
|
2 | fn consume(_: &dyn Fetch) {}
| ^^^^^^^^^ `Fetch` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit
--> drills/async_dyn.rs:1:24
|
1 | trait Fetch { async fn get(&self) -> usize; }
| ----- ^^^ ...because method `get` is `async`
| |
| this trait is not dyn compatible...
= help: consider moving `get` to another trait
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0038`.
```
The drill captures the current direct-dyn rejection. This shows what the pinned compiler supports today, not what Rust will support forever. Current async and field-projection initiatives aim to improve these awkward edges.
## Exercise
Explain why moving the `Box` handle is compatible with pinning its allocated future. Then remove the trait object entirely and call a concrete async method. Compare what the two designs require from allocation and the public signature.
Solution and acceptance check
The pointee stays in its allocation when the owning pointer moves. A concrete async method can return its compiler-generated future without the explicit box and vtable used here. Prefer the concrete method unless callers need to work with different implementations through one interface. Do not claim a speedup without measuring the relevant workload.
Sources: [`std::pin`](https://doc.rust-lang.org/std/pin/), [trait rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility), and [the async trait RFC](https://rust-lang.github.io/rfcs/3185-static-async-fn-in-trait.html).
---
# Stage build: process records with async tasks
Send records through a bounded queue and keep one task in charge of the counts.
Canonical: https://rust.robertdevore.com/course/22-async-build/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## Build brief
Use the parsing core in a small async program. One producer sends owned records through a queue. One consumer owns the summary. At normal completion, the queue closes, the consumer drains it, and the producer's result is checked. Keep the synchronous file CLI as the final product: async is useful to study, but it does not automatically improve sequential local file processing.
### Run the example
```sh
cargo run --locked --example 22_stage_three
```
```rust
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box> {
let (tx, mut rx) = tokio::sync::mpsc::channel(2);
let producer = tokio::spawn(async move {
for line in ["INFO ready", "WARN retry", "ERROR stop"] {
tx.send(String::from(line)).await?;
}
Ok::<_, tokio::sync::mpsc::error::SendError>(())
});
let mut summary = audit_core::Summary::default();
while let Some(line) = rx.recv().await {
summary.record(&audit_core::parse(&line)?)?;
}
producer.await??;
assert_eq!((summary.info, summary.warn, summary.error), (1, 1, 1));
Ok(())
}
```
[View the tested source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/22_stage_three.rs)
Each `String` moves into the queue. The consumer borrows it to parse an `Event`, updates the summary, and then drops the record. The event never escapes the record's lifetime. The consumer owns the counts, so it needs no shared mutable state, reference counting, or mutex.
## What the queue limit covers
The queue holds at most two pending messages. This fixture also has a fixed number and size of messages. If you replace it with network input, a bounded queue alone does not cap individual record size, number of connections, or producer-side accumulation. Apply the byte limit before a producer allocates an arbitrarily large record.
The example stops on a parse error through `?`. At that point the receiver is dropped, pending sends fail, and the short-lived runtime shuts down on return. That is enough for a finite teaching fixture. A long-running service needs code that checks how each task ends and decides whether to finish queued work, cancel it, or restart the task. Do not mistake this demonstration for a complete production server.
## Cancellation exercise
Extend the pipeline so the producer can wait for a shutdown signal. Specify whether already accepted messages are drained. Use a `JoinSet` or explicit task handles to observe completion; do not detach tasks by losing their handles. Add a test in which a shutdown arrives while the queue is full.
Solution and acceptance check
A valid policy closes admission, drops all producers' senders, drains already queued work, and awaits producer completion. A different policy may discard queued work, but the result must say it is incomplete. In the full-queue test, the consumer must keep making progress or cancellation must release the blocked send. Use deterministic synchronization rather than hoping a sleep creates the intended state.
## Review the ownership graph
The producer owns each record before sending; the channel owns it while queued; the consumer owns it while parsing. The summary remains local to the consumer. The parser has no runtime dependency and is reused unchanged. Because the parser accepts `&str`, it works here without changes. The synchronous reader remains separate behind `BufRead`.
If you introduce multiple consumers, decide how totals are merged and how failures affect completeness. A mutex is one possible choice, but local summaries returned to an aggregator can keep the critical path simpler. Choose based on how much work you have and what should happen when part of it fails.
Sources: [Tokio shared state](https://tokio.rs/tokio/tutorial/shared-state), [bounded mpsc](https://docs.rs/tokio/latest/tokio/sync/mpsc/), and [JoinSet](https://docs.rs/tokio/latest/tokio/task/struct.JoinSet.html).
---
# Unsafe boundaries and soundness
Review a small unsafe function, explain its safety rules, and check it with Miri.
Canonical: https://rust.robertdevore.com/course/23-unsafe/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## Unsafe does not suspend Rust's rules
Unsafe operations have requirements the compiler cannot completely verify. An unsafe block makes the programmer responsible for meeting those requirements; undefined behavior remains forbidden. A safe API is sound only if no allowed safe caller can trigger undefined behavior through it.
The lab reimplements a tiny slice split for study. Production application code should use the standard `split_at_mut`. The separate lab lets us study unsafe code while forbidding it in the application.
```rust
//! Study-only unsafe boundary. In application code prefer `slice::split_at_mut`.
/// Split at a checked index, preserving exclusive access to disjoint elements.
///
/// Panics if `mid > slice.len()`. Safe callers have no extra obligations.
pub fn split(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = slice.len();
assert!(mid <= len, "split index exceeds length");
let ptr = slice.as_mut_ptr();
// SAFETY: ptr comes from a valid exclusive slice. mid <= len keeps the
// offset in the allocation or one-past. Both ranges contain initialized T,
// are aligned/non-null even when empty, and partition the original range.
// Returned lifetimes are tied to slice; the original borrow cannot be used
// while these reborrows are live. ZSTs have disjoint logical elements even
// when their addresses coincide. No ownership or destructor is duplicated.
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partitions() {
for mid in 0..=4 {
let mut data = [0; 4];
let (a, b) = split(&mut data, mid);
a.fill(1);
b.fill(2);
assert_eq!(data[..mid], vec![1; mid]);
assert_eq!(data[mid..], vec![2; 4 - mid]);
}
}
#[test]
fn empty_and_zst() {
let mut empty: [u8; 0] = [];
assert!(split(&mut empty, 0).0.is_empty());
let mut z = [(); 4];
let (a, b) = split(&mut z, 2);
assert_eq!((a.len(), b.len()), (2, 2));
}
#[test]
fn drops_once() {
use std::rc::Rc;
let token = Rc::new(());
{
let mut data = [token.clone(), token.clone()];
let (a, b) = split(&mut data, 1);
std::mem::swap(&mut a[0], &mut b[0]);
}
assert_eq!(Rc::strong_count(&token), 1);
}
#[test]
#[should_panic(expected = "split index exceeds length")]
fn rejects_oob() {
split(&mut [1], 2);
}
}
```
[View the complete source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/crates/unsafe-lab/src/lib.rs)
## The safety argument
**What must remain true?** Both returned slices cover initialized elements from the original slice, stay within its allocation, retain valid alignment and provenance, and refer to disjoint logical element ranges. Their references cannot outlive the original borrow.
**Who maintains it?** The caller supplies a valid exclusive slice through a safe Rust type. The function checks the split index before pointer arithmetic. Its implementation constructs exactly the two ranges. Borrow checking relates the returned lifetimes to the input.
**Which safe callers are allowed?** Any valid mutable slice, including empty slices, zero-sized element types, and types with destructors. A split at either endpoint is valid. An out-of-range index must panic before unsafe operations. Safe callers must not need to follow extra, undocumented rules.
**How is the unsafe code kept small?** One unsafe block follows a checked boundary. The raw pointer does not escape. No allocation is freed and no element ownership is duplicated. The application never depends on this study implementation.
**What would make it unsound?** Removing the bounds check, overlapping nonempty element ranges, inventing a longer return lifetime, accepting an arbitrary unvalidated pointer, or allowing conflicting access through the original slice while the returned borrows are live.
## Aliasing is more than addresses
Zero-sized elements can share an address without representing overlapping stored bytes. A simplistic “different addresses means safe” argument would fail to cover them. Likewise, `UnsafeCell` relaxes shared-reference immutability for its contents but does not erase exclusive-reference rules or synchronize concurrent accesses.
The Reference explicitly notes that exact aliasing rules remain an active specification area. Miri checks executions under its models. A passing run does not prove all executions safe, and its models are not the final language specification. We test ordinary elements, endpoints, empty input, zero-sized elements, and drop behavior, then inspect the invariant argument separately.
## Exercise
Run the isolated lab under Miri:
```sh
rustup toolchain install nightly-2026-09-05 --profile minimal --component miri
cargo +nightly-2026-09-05 miri test -p unsafe-lab
```
Explain why no destructor may run twice and why a safe caller using `mem::forget` must not invalidate the abstraction. Do not execute deliberately undefined examples in a normal production process.
Solution and acceptance check
The function returns borrows, not new owners of elements. Dropping a slice reference does not drop its elements. The original owner retains destruction responsibility. Forgetting a reference does not produce conflicting access by itself; the implementation does not rely on its destructor running. The tests check selected cases. The safety argument must cover every allowed safe caller.
Sources: [undefined behavior](https://doc.rust-lang.org/reference/behavior-considered-undefined.html), [Rustonomicon](https://doc.rust-lang.org/nomicon/), [`from_raw_parts_mut`](https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html), and [Miri](https://github.com/rust-lang/miri).
---
# Measure performance and resource use
Measure speed and memory use before changing how the program works.
Canonical: https://rust.robertdevore.com/course/24-performance/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## A measurement starts with a question
Does parsing allocate? Does input reading dominate runtime? Does parallelism help on representative files? Each question needs its own test. “Rust is fast” does not answer any of them.
### Run the example
```sh
cargo run --locked --example 23_performance
```
```rust
use std::{hint::black_box, time::Instant};
fn main() {
let input = "INFO small message";
let start = Instant::now();
for _ in 0..10_000 {
black_box(audit_core::parse(black_box(input)).unwrap());
}
println!(
"10,000 parses: {:?}; one sample, not a benchmark conclusion",
start.elapsed()
);
}
```
[View the tested source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/23_performance.rs)
The example shows how to time code and use `black_box`. One timing result is not enough to draw a conclusion. Startup, optimization, CPU frequency, background work, and tiny sample size can dominate. `black_box` is a best-effort optimization barrier useful in experiments, not a mathematical guarantee about generated code.
## Design the experiment
Run optimized builds when evaluating optimized production behavior. Record compiler version, target, profile, input distribution, hardware, and sample count. Make sure both versions produce the same output and handle errors the same way. Include empty input, typical records, long records near the limit, and malformed data if those occur in the workload.
Separate warm-up from samples where appropriate. Report a distribution rather than one favorable run. Change one significant variable at a time. If you claim lower memory use, measure it or prove a specific bound and state precisely which allocations the bound covers.
Our parser borrows message text rather than allocating a new string. That is visible in its interface and implementation. The reader keeps one bounded record vector, while the caller's buffering layer can have its own allocation. A borrowed design still pays for reading, validation, and counting; avoiding one allocation does not mean the whole program is zero-cost.
## Optimize the bottleneck
A profile may show that filesystem I/O dominates, in which case replacing an iterator with a loop is unlikely to matter. It may show formatting costs, in which case current standard-library APIs deserve review before adding a crate. Rust 1.98 introduced integer buffer formatting APIs, but our three-line-sized summary does not justify optimizing that path without evidence.
Monomorphization can improve inlining and also increase compile time or code size. Dynamic dispatch has indirection costs and can reduce duplication. `Arc::clone` updates an atomic count; `String::clone` copies text. Treat these as different operations. Check whether unnecessary copies or allocations cause the slowdown before reaching for unsafe code.
## Exercise
Compare parsing borrowed records with a version that creates a new owned message for each record. Preserve outputs and error handling. Record at least several repeated optimized runs and explain which part of the measurement is noisy. Do not commit a machine-specific speed claim as a universal fact.
Solution and acceptance check
Keep input bytes and loop counts identical, consume results so they cannot trivially disappear, and record toolchain/profile. The owned version allocates new message strings, but the timing difference may vary. A useful conclusion states the tested workload and uncertainty. If the difference is below noise, report that rather than selecting the fastest run.
Sources: [`black_box`](https://doc.rust-lang.org/std/hint/fn.black_box.html), [Cargo profiles](https://doc.rust-lang.org/cargo/reference/profiles.html), and [Rust 1.98 release notes](https://blog.rust-lang.org/2026/08/20/Rust-1.98.0/).
---
# Stable Rust and work in progress
Tell stable features apart from nightly experiments and plans that may change.
Canonical: https://rust.robertdevore.com/course/25-horizon/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## The version this course uses
This course was verified on **2026-09-06** against **Rust 1.98.1**, edition **2024**. The September point release fixes a vtable-generation miscompilation in 1.98.0, so the course pins the corrected version. The compiler version matters when you reproduce the examples and diagnostics.
A feature can have both a stable subset and an active extension. The following table classifies the specific claim being made. A project goal means work is planned. It does not guarantee a stable release or a date you can rely on.
| Area | Today | On the horizon / status |
| --- | --- | --- |
| Async functions in traits | Static-dispatch use is STABLE | Native async dyn dispatch is an ACTIVE PROJECT GOAL; our direct-dyn drill still fails |
| Pin | Safe pinning APIs are STABLE | Field projections and broader pin/reborrow ergonomics are ACTIVE PROJECT GOALS with EXPERIMENTAL work |
| Borrow checking | Stable checks remain the course baseline | Polonius alpha is NIGHTLY work and an ACTIVE PROJECT GOAL; do not assume all lending patterns work on stable |
| Trait solver | Stable compilation is the contract used here | Global next-solver rollout is NIGHTLY; the August 21 announcement is not stable rollout |
| Const generics | Array lengths parameterized by integers are STABLE | Full const generics and const trait capabilities are evolving; const trait implementation support is NIGHTLY |
| Reference counting | Explicit `Rc::clone` and `Arc::clone` are STABLE | Reborrow and smart-pointer ergonomics are ACTIVE PROJECT GOALS |
| Allocation | `GlobalAlloc` and normal collections are STABLE | The general allocator API remains NIGHTLY; Allocators 1.0 is an ACTIVE PROJECT GOAL |
| Unsafe fields | Private fields plus documented invariants are the stable idiom | `unsafe_fields` is NIGHTLY; it does not replace a safety argument |
| Safety contracts | Written invariants and checked unsafe boundaries remain required | Primitive ownership assertions are an ACTIVE PROJECT GOAL, not a stable verification guarantee |
| Formal specification | Reference contracts guide production work | a-mir-formality and executable specification work are EXPERIMENTAL / ACTIVE PROJECT GOALS |
| C/C++ interop | Explicit ABI boundaries and maintained bindings are usable today | Seamless interop and problem-space mapping are ACTIVE PROJECT GOALS |
| Standard-library rebuilding | Normal prebuilt std targets are the baseline | Cargo `-Z build-std` is NIGHTLY |
| Build speed | Stable Cargo profiles and incremental builds are available | Fast Builds is an ACTIVE PROJECT GOAL roadmap; measure local changes |
| Semver tooling | Cargo's compatibility guidance applies today | cargo-semver-checks is ecosystem tooling, not a compiler proof of all compatibility |
### Run the example
```sh
cargo run --locked --example 24_const
```
```rust
fn first(values: &[u8; N]) -> Option {
values.first().copied()
}
fn main() {
assert_eq!(first(&[3, 4]), Some(3));
assert_eq!(first(&[]), None);
}
```
[View the tested source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/24_const.rs)
This small array example uses stable const generics, which do not let arbitrary trait methods run at compile time.
**Intentionally fails on Rust 1.98.1.** Run `rustc --edition=2024 drills/const_trait.rs` from the repository.
```rust
trait Compute { const fn compute(&self) -> usize; }
fn main() {}
```
Actual compiler diagnostic · Rust 1.98.1
```text
error[E0379]: functions in traits cannot be declared const
--> drills/const_trait.rs:1:17
|
1 | trait Compute { const fn compute(&self) -> usize; }
| ^^^^^-
| |
| functions in traits cannot be const
| help: remove the `const`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0379`.
```
The August program update also records two discontinued efforts: the specific **Experimental language specification** goal and **Continue Experimentation with Pin Ergonomics**. Treat those goal labels as HISTORICAL / SUPERSEDED, not active promises. Field-projection work and the newer end-to-end executable specification goal continue on their own terms. Cargo’s linting-system goal is reported complete; this course uses current manifest lints and Clippy rather than describing that completed goal as future work. See the [July–August program update](https://blog.rust-lang.org/inside-rust/2026/08/31/program-management-2026-jul-aug/) and [ownership-contract goal](https://github.com/rust-lang/goals/issues/734).
## How to update an explanation
First reproduce behavior on the exact stable compiler. Then read the Reference, stabilization record, and relevant goal or tracking issue. If the compiler rejects a safe pattern because its analysis is conservative, describe the currently supported expression and the limitation. Distinguish a limit in the compiler's analysis from a rule of the language.
Work on an easier alternative does not make an existing feature deprecated. Historical descriptions need dates. Proposed designs can change or fail to ship. The core exercises avoid nightly language features; only the optional unsafe verification toolchain uses nightly for Miri.
## Exercise
Choose one row, follow its official source, and identify what evidence would justify changing its status to STABLE. Record the compiler release and stabilization reference rather than editing the label based on a social post.
Solution and acceptance check
A stable release note or merged stabilization record plus a successful minimal stable probe is strong evidence. A goal marked accepted, a nightly demo, or a merged RFC alone is insufficient. Preserve the older verification date in the research history and rerun affected compiler drills.
Sources: [release fix](https://blog.rust-lang.org/2026/09/03/Rust-1.98.1/), [2026 goals](https://goals.rust-lang.org/2026/goals.html), [Polonius](https://goals.rust-lang.org/2026/polonius.html), [next-solver rollout](https://blog.rust-lang.org/2026/08/21/enabling-next-solver-on-nightly/), [unstable Cargo](https://doc.rust-lang.org/cargo/reference/unstable.html), [unsafe fields](https://doc.rust-lang.org/unstable-book/language-features/unsafe-fields.html), [const traits](https://doc.rust-lang.org/unstable-book/language-features/const-trait-impl.html), and [allocator API](https://doc.rust-lang.org/std/alloc/trait.Allocator.html).
---
# FFI, serialization, and public contracts
Define ownership, data formats, and errors when calling code outside Rust.
Canonical: https://rust.robertdevore.com/course/26-interop/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## The other side has its own rules
A foreign function interface (FFI) lets Rust call code that may follow different rules. `extern "C"` selects an ABI; it does not validate pointers, lengths, ownership, or lifetimes. `repr(C)` specifies a C-compatible layout scheme for the annotated type, but does not make every contained Rust type appropriate to expose to C.
The following example calls a C-ABI function defined in the same Rust program. It is a portable ABI-boundary exercise, not a demonstration of building or linking an external C library. The byte pointer is not dereferenced because the function only counts a provided length.
```rust
extern "C" fn record_length(_bytes: *const u8, len: usize) -> usize {
// This function never dereferences or retains the pointer.
len
}
fn main() {
let record = b"INFO ready";
assert_eq!(record_length(record.as_ptr(), record.len()), 10);
}
```
[View the complete source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/25_ffi.rs)
A real external declaration needs an unsafe extern block in edition 2024. When adding an actual C library, consult its headers and platform ABI, specify how memory is allocated and released, and test the build on supported targets. Avoid passing `String`, Rust enums with unspecified layout, or trait objects as if their representation were a stable C contract.
## Write down ownership in both directions
For each pointer parameter, ask whether null is allowed, which allocation it belongs to, how many initialized elements are accessible, whether mutation is permitted, and how long the callee retains it. For each returned pointer, identify who frees it and with which allocator. Two libraries can each be correct on their own but crash when they disagree about these rules.
Unwinding across an ABI boundary needs an explicit contract. A panic must not accidentally unwind through an incompatible foreign frame. Catching a panic works only if it unwinds. It cannot recover from an abort or undefined behavior. Do not use panic catching to make invalid pointers acceptable.
C++ has additional challenges around object lifetimes, exceptions, templates, and ownership. Bridge libraries can enforce some of these rules, but you still need to check the foreign code. Current Rust interop initiatives are improving this space; the course's application deliberately needs no FFI.
## A file format is also an interface
Our plain-text format has exact level spelling, UTF-8 messages, and a record-size rule. Adding JSON should use a maintained serializer such as Serde with an explicitly versioned schema. Derived serialization saves repetitive code. You still need to decide how to handle unknown fields, missing fields, and incompatible changes. Never persist `Debug` output as a stable format.
## Exercise
Write a contract for a hypothetical foreign function receiving bytes. Specify pointer validity, length, retention, mutability, ownership, and error reporting. Then identify which checks a safe wrapper could perform and which require trusting the foreign implementation.
Solution and acceptance check
A slice can provide initialized byte storage and a valid length for the call. The wrapper can pass its pointer and length without transferring ownership. The contract must forbid retaining the pointer beyond the borrow unless a separate ownership protocol exists. The wrapper cannot prove arbitrary foreign code obeys that promise; implementation review and testing remain necessary.
Sources: [external blocks](https://doc.rust-lang.org/reference/items/external-blocks.html), [type layout](https://doc.rust-lang.org/reference/type-layout.html), [FFI unwinding](https://doc.rust-lang.org/nomicon/ffi.html#ffi-and-unwinding), [Serde](https://serde.rs/), and [interop work](https://goals.rust-lang.org/2026/interop-problem-map.html).
---
# Capstone: ship Fieldnotes
Combine the parser and reader into a command-line tool you can install and use.
Canonical: https://rust.robertdevore.com/course/27-capstone/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## Product brief
Fieldnotes is the course's completed event-audit tool, packaged as `audit-cli`. It accepts exactly one path argument, with `-` meaning standard input. When it succeeds, it prints one summary line to stdout in a fixed order. Errors go to stderr and produce a nonzero exit code. No partial summary is printed when input is invalid.
The format is one `LEVEL message` record per line. Levels are exactly INFO, WARN, and ERROR. Messages must contain non-whitespace text. Input must be UTF-8. LF, CRLF, and an unterminated final record are supported. Each record may contain at most 4096 bytes including its newline if present. Counts use checked `u64` arithmetic.
## Build and install
```sh
cargo build --release --locked -p audit-cli
cargo install --locked --path crates/audit-cli
printf 'INFO ready\nWARN retry\nERROR stopped\n' | audit-cli -
```
Expected stdout is `INFO=1 WARN=1 ERROR=1`, followed by a newline. For a file, replace `-` with its path. The executable name is `audit-cli`; Fieldnotes is the product name used in this course. `cargo install` places the executable in Cargo's binary directory, which must be on your PATH.
```rust
use std::{
env,
fs::File,
io::{self, BufReader, Write},
process::ExitCode,
};
fn run() -> Result<(), Box> {
let mut args = env::args_os().skip(1);
let path = args.next().ok_or("usage: audit-cli ")?;
if args.next().is_some() {
return Err("usage: audit-cli ".into());
}
let summary = if path == "-" {
audit_core::summarize(io::stdin().lock())?
} else {
audit_core::summarize(BufReader::new(File::open(path)?))?
};
writeln!(
io::stdout().lock(),
"INFO={} WARN={} ERROR={}",
summary.info,
summary.warn,
summary.error
)?;
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
let _ = writeln!(io::stderr().lock(), "audit: {e}");
ExitCode::FAILURE
}
}
}
```
[View the complete source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/crates/audit-cli/src/main.rs)
## Why main stays small
Argument parsing uses `args_os`, allowing platform-native paths rather than assuming every filename is Unicode. Opening the file and choosing stdin belong in the CLI. Reading, syntax, record limits, and counts belong in the library. Printing only after the full input succeeds prevents partial counts from appearing as a complete result.
Output writing can fail, including a downstream pipe closing. The program returns a failure in that case. Stderr writing during error reporting is best-effort because another failure cannot be reported indefinitely. Handle these failures even when reading and parsing succeeded.
## Acceptance suite
Run `npm run verify:rust`. It checks the workspace, runs examples and compiler drills, and exercises the CLI with valid stdin, empty input, malformed input, invalid UTF-8, an oversized record, a missing path, and a real file. The library tests independently cover chunk boundaries and count overflow. Run `cargo test --release --workspace --locked` when changing arithmetic or profile settings.
## Your extension
Add `--allow-errors` only after specifying its output contract. Decide how rejected rows are counted, whether oversized input can be skipped without unbounded storage, and how an I/O failure differs from a malformed row. Test each of those cases as well as the new argument.
Reference design and acceptance check
Represent completeness in the result type rather than silently discarding failures. Keep strict mode's behavior unchanged. A lenient mode should report both accepted and rejected counts and still fail on unreadable input. Discarding an oversized record requires scanning to a terminator while keeping storage bounded. Document whether a rejected final unterminated record is counted.
## Check your finished tool
Your tool should count correctly, limit its record buffer, report failures clearly, and build with the pinned toolchain. It should not need unsafe code. You should be able to trace each borrow to its owner and each allocation to a reason. Performance claims need measurements. A clean compile is necessary but does not establish every product requirement.
Sources: [`args_os`](https://doc.rust-lang.org/std/env/fn.args_os.html), [Cargo install](https://doc.rust-lang.org/cargo/commands/cargo-install.html), and [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html).
---
# Release, maintain, and keep learning
Check compatibility, release a reproducible build, and keep the course up to date.
Canonical: https://rust.robertdevore.com/course/28-release/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.
## Make the release reproducible
The course repository is independently buildable with Node 24 and Rust 1.98.1. The website and Rust application have separate build paths. The website is static HTML with local assets; the application is an ordinary Cargo workspace. Neither needs another course repository at runtime or build time.
Before releasing a change, run the documented checks from a clean checkout. Keep generated build artifacts out of the source commit unless they are intentional evidence such as compiler diagnostic snapshots. Review the lockfile diff when updating dependencies. A passing test suite does not excuse an unexplained dependency change.
### Run the example
```sh
cargo run --locked --example 13_cargo
```
```rust
fn main() {
assert_eq!(env!("CARGO_PKG_NAME"), "rust-course-examples");
println!(
"package={} version={}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
);
}
```
[View the tested source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/examples/13_cargo.rs)
## Review the public API
Other programs may rely on your public fields, trait bounds, enum variants, and error types. Removing an implementation or adding a required bound can break callers even when your own tests compile. Adding variants to an exhaustive public enum can be breaking. `#[non_exhaustive]` leaves room to add variants or fields later, but limits how callers construct or match values. Decide whether you need it before publishing.
Cargo's semver guidance and tools such as cargo-semver-checks help identify API changes. They do not prove behavioral compatibility, validate a file format, or establish that every performance expectation is preserved. Write release notes around observable effects, including changes to limits and failure behavior.
The course packages set `publish = false` because they are for local study. Publishing a general-purpose crate would require selecting an appropriate name, reviewing metadata and licensing, testing a packaged archive, and establishing a support policy. Learners can install the application with `cargo install --path`; it does not need a registry release.
## A practical maintenance loop
1. Inspect the current stable release and edition guidance.
2. Review source changes for concepts marked evolving.
3. Update the evidence ledger with dates and affected lessons.
4. Compile every runnable example and regenerate intentional failures.
5. Run tests, Clippy, formatting, and the isolated Miri lab.
6. Build the site, check links and metadata, and test keyboard/mobile behavior.
7. Deploy and verify the actual production domain, including deep links.
The ledger records sources and recommendations without claiming that named maintainers reviewed or endorsed the course. Their work informs review questions; our own examples and tests supply reproducible evidence.
## Exercise
Propose a change that would allow lowercase levels. Describe whether it is a parser behavior change, an API signature change, a file-format change, or several of these. Add tests for the intended mixed-case policy and explain how the release notes should describe it.
Solution and acceptance check
Accepting lowercase broadens the file grammar even if function signatures remain unchanged. Decide whether all case combinations are valid and whether messages remain untouched. Existing strict callers might rely on rejection, so consider compatibility in context. Release notes should state exactly which inputs now succeed and whether strict mode remains available.
## Where to go next
Choose a real problem: a network service, an embedded peripheral, a library used by another team, or a performance-sensitive tool. Learn what that problem requires. Study new language features as you need them. Revisit ownership, errors, synchronization, and invariants at each boundary. The goal is software whose behavior another developer can understand and maintain.
Sources: [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html), [publishing](https://doc.rust-lang.org/cargo/reference/publishing.html), and [cargo-semver-checks](https://github.com/obi1kenobi/cargo-semver-checks).