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 is Send but !Sync.
  • T is Sync if and only if &T is Send.

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 be Send (it must work across .await points).
  • Recap: reference &T is Send if T is Sync.

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 all impl blocks.
  • It's better to add bounds only where they’re needed, and not on the type definition.
  • Drop bounds must be the same as T 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 than T: 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 and wasm-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 the mmap 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: