EuroRust 2024: Day 1 Notes
I recently attended EuroRust 2024, the European Rust conference.
I figured I'd share some notes and findings as I found the event to be very insightful.
Conference Intro
The conference took place in Vienna and was scheduled over 2 days. It was a real blast!
It was actually the 3rd EuroRust conference overall, and I've attended every one of them. This year, since I'm now using Rust professionally, I've paid extra attention and decided to note things down.
Notes: "Through the Fire and the Flames" by Jon Gjengset
One of the best talks of the conference, by an renowned Rust educator (famous for his book and YouTube channel).
The talk went through some of the more "exotic" concepts of Rust that are kind of hard to explain, and did a pretty good job at that. I have scribbed some notes below:
Send
and Sync
Send
means "&mut
across threads" (e.g. safe to send to another thread).!Send
is thread-local.Sync
means "&
across threads" (e.g. safe to share with other threads).- E.g.
Cell
isSend
but!Sync
. T
isSync
if and only if&T
isSend
.
Future
- Task execution is single threaded, and Futures are like onions, they can wrap many more Futures inside.
- A top-level Future ran by an actual executor is called a Task.
spawn
is parallelism (multi-thread)select
is concurrency (same thread)
Future+Send
- Relevant to multi-threaded runtimes (e.g. multi-worker, work-stealing like Tokio).
- Future is
Send
-> All things captured in the Future must beSend
(it must work across.await
points). - Recap: reference
&T
isSend
ifT
isSync
.
Lifetimes ('a
)
- A lifetime is "proof" of reference validity, or a "consume date".
- Lifetimes can usually be shortened.
- More than one lifetime is needed for returns longer than shortest lifetime passed in.
unsafe
is for forcing lifetime breakage (e.g. cannot check lifetime statically).
where
Bounds
- Rust does signature-level verification. This is specific to how Rust semantics work.
- A function body can’t add inferred type assumptions. It all must be laid out in the signature.
where
must be repeated in allimpl
blocks.- It's better to add bounds only where they’re needed, and not on the type definition.
Drop
bounds must be the same asT
bounds.
where + 'a
aka. HRTBs
Higher-Ranked Trait Bounds are bounds for "any lifetime".
where+Future+Send
- Be explicit and precise in
where
annotations. E.g.Arc<T>: Send
is better thanT: Send+Sync
.
Notes: "Build bigger in less time: code testing beyond the basics" by Predrag Gruevski
This talk was delivered by the author of Trustfall, a universal query engine. It went through many advanced testing techniques.
Invariant Testing
- Avoid "silent bugs", e.g. breaking preconditions and invariants (a binary search sorted array input example was given).
- Can use
debug_assert!
to add extra checks in debug compilations. - Can use
cargo-semver-checks
crate for public library APIs, to avoid unexpected breakage.
Snapshot Testing
- It's about auto-generating expected value.
- Derives for traits can be conditional, e.g.
#[cfg_attr(test, derive(Serialize))]
- Can use a popular insta library for this that comes batteries included.
- Good practice: review all changes to snapshots in Pull Requests, not just the code.
- One random observation from the slides: the author used
.ron
format (the ron crate) that can be used in Serde to serialize arbitrary structs, enums, tuples, etc.
Deterministic Simulation
The idea of deterministic simulations is essentially this:
┌─────────────────────┐ ┌────┐ ┌──────────┐
│ │ │ │ │ │
│ │ │ │ │ simulator│
│ non-deterministic │ │ │ │ │
│ │ ────────▶ │ │ └──────────┘
│ monolith │ │ └────────────┐
│ │ │ deterministic │
│ │ │ logic │
└─────────────────────┘ └─────────────────┘
- The simulator part should basically be dependency injection into deterministic logic, that lets you simulate faults and other "unexpected" conditions.
- One of my friends noted that "this is basically what functional programming is all about". And I kind of agree. If you keep your code clean and "pure", you get this for free.
- I’ve used this pattern successfully for years now.
Notes: "A gentle introduction to procedural macros" by Sam Van Overmeire
The talk focused on procedural macros, which in Rust are essentially acting on "token streams" (e.g. arbitrary code) and producing token streams.
- Declarative vs proc macros -- declarative are simplistic, proc let you do pretty much anything with the source code.
- The quote crate can produce token streams (e.g. turn strings into transformed Rust code)
- The syn crate is the opposite, it parses Rust code from strings.
- Can use a combo of the two crates to produce a derive macro quite easily. The author demonstrated a simple derive macro that constructed a AWS SQS client with custom payload types, by dynamically adding methods based on attributes/fields defined in a struct.
- cargo-expand can be used to expand macros, for debugging. It only works with the nightly (non-stable compiler) though.
Notes: "Practical Rust for Web Audio" by Attila Haraszti
The talk went over practical considerations of doing Web Audio with Rust and WASM. I found the Rust WASM insights particularly interesting.
SharedArrayBuffer
can be used in JS for "views" of shared memory, without copying.Worker.postMessage()
API can be used to offload work to web workers in JS.- Best practice of working with audio is to allocate buffers in WASM (Rust) and pass them to JS.
- Use
ManuallyDrop
to create long-lived objects. !#[no_std]
is desirable for WASM, since it removes the standard library code from WASM binaries, to thin them up.- The
wee_alloc
memory allocator library recommended for Rust WASM leaks memory in author's experience. - An alternative allocator is
talc
which takes up ~7KiB and has proven to be good quality in author's experience. - Useful WASM tools:
wasm-opt from binaryen
,wabt
andwasm-tools
(e.g.wasm-tools shrink
).
Notes: "Augmented docs: a love letter to rustdoc and docs.rs" by Francois Mockers
This talk was a quick look at good documentation practices and highlighted some cool features of the Rust docs generator I didn't quite know!
- There's a docs.rs "about" doc that documents how to make the most out of the public Rust docs host (for public crates).
#![doc(html_logo_url = "...)]
is a thing.- There's a whole lot of other doc attrs!
- You can
include_str!
in docs, e.g. entire README files. - There's a way of automatically documenting your crate's features: document_features.
- You can alias doc members
#[doc(alias = "...")]
to make things easier to find. cfg(doc)
only gets compiled when building for docs.#[doc(cfg(feature = "..."))]
can document feature-specific code.- You can embed HTML in docs.
- There's special
cargo doc
flags:--extend-css
,--html-in-header
, for customizing the styles, including extra scripts, etc. - Use katex_doc to embed math formula in docs.
- rustdoc_json_types documents the JSON output format of the Rust doc generator.
- Cool tip: you can use
usize -> vec
,option<t> -> t
and similar signatures in docs search. Very useful for the standard library (the docs can do search in function signatures!). - There's a way of reporting the documentation coverage % -- I just didn't quite catch how :)
Some useful links the author has shared:
- Diátaxis -- a guideline for writing good documentation. It splits the documentation into 4 categories (tutorials, how-tos, explanations, and references) and they have a chunky guide on writing documentation.
- The Good Docs Project -- documentation guidelines, templates, and a lot of tips.
- mdBook -- a tool to generate books out of md files, can test presented Rust code.
- clap tutorial -- an example of how to create a docs page that's for docs only (not for code).
Notes: "The Impact of Memory Allocators on Performance: A Deep Dive" by Arthur Pastel
- Dynamic (heap) memory allocation is expensive -- there's a memory allocator library involved.
const
(inline) !=static
(binary embedded).- On a high-level
malloc
(e.g. in glibc) uses themmap
syscall to get memory from the OS. - Can set the global program memory allocator with
#[global_allocator]
. - Alternative allocators: mimalloc, jemalloc, bumpalo
The author showed quite a few benchmark results for various allocators, across multiple synthetic workloads. Take-aways I noted:
bumpalo
takes a lot of time to allocate many large chunks of memory.jemalloc
scales poorly with number of threads.- For few large allocations, or for many small allocations,
bumpalo
wins. mimalloc
is very competitive, but non-deterministic.- Regardless of the allocator, pre-allocating memory helps performance.
- For small vectors, can use a crate like smallvec to have them stack-allocated.
Notes: "Proving macro expansion with expandable" by Sasha Pourcelot
- Checking if macros are "actually expandable to valid code" is a tough problem.
- The Rust compiler doesn't verify macros all that well.
- The author is working on a new crate, expandable to verify macro expansions better.
Notes: "Runtime Scripting for Rust Applications" by Niklas Korz
- Rust can be scripted in various languages, e.g.
mlua
,deno
(nodejs alternative),Python
. - SWC is now a viable alternative to compiling JS (e.g. to Babel).
- The author focused on showcasing how to script Rust in JS using
deno
. Because Deno is written in Rust, it has native bindings and tools to expose JS in Rust.
Notes: "Unleashing 🦀 The Ferris Within" by Victor Ciura
- Windows kernel is now Rust + C + C++.
- Rust can be used to write Windows kernel modules, the author shared some success
stories and challenges associated in Rust rewrite of some Windows OS components.
- A big challenge is C++ inter-op.
- They migrate away from C# to Rust to save costs (Rust takes up less resources and is cheaper to run compared to C# on Microsoft/Azure scale).
- Rust is also used to mitigate security vulnerabilities, most of the ones detected at Microsoft are fully mitigated by just using Rust and not C/C++.
More Notes!
This concludes my notes from day 1 of the conference. I have more notes: