Mehdi Akiki
Writing / Series

Rust Under the Hood

Async, types, memory, and compiler behaviour

Practical explanations of the Rust behaviour that only becomes obvious after reading compiler output, measuring a program, or working close to the language and its tools.

The same Rust writing is also mapped as one system, from compiler internals to async runtimes, in the Rust Systems Atlas.

The model behind the series

Rust gives us strong guarantees, but the most useful mental models often sit below the surface syntax. Futures become state machines. A harmless-looking local changes a future's size. Variance, drop checking, pinning, and pointer provenance shape which abstractions are actually sound.

This series makes those mechanics observable. Each article starts from a concrete question, builds the smallest useful model, and then connects it to decisions we make in real code.

Core principles

01

Inspect what the compiler builds

Use expanded code, type information, layouts, MIR, diagnostics, and small experiments instead of relying on folklore.

02

Connect semantics to design

Language rules matter because they change API boundaries, memory use, concurrency, and the safety of an abstraction.

03

Keep examples reproducible

Prefer focused programs and commands that a reader can run, change, and use to disprove the explanation.

Start here

Entry points into this subject, ordered as a reading path.

02

DefId vs HirId in the Rust Compiler

Two identifiers, two different jobs. DefId names a definition across the whole compiler pipeline and across crates. HirId points to a node in the syntax tree of the current crate. Mixing them up is the first stumbling block when reading rustc source code.

Investigations

Multi-part work with the code and measurements that produced its evidence.

10 parts

Types under the hood

Ten parts on what a type is and where it goes: sets of values, layouts and padding, erasure and monomorphization, the bits the processor actually sees, and what the checker knows that the binary forgets. Small programs in Rust, C, TypeScript and Python, with one script that reproduces every output.

3 parts

Rust build times

A measured diagnostic tree for slow Rust builds: cargo --timings for the shape, the fingerprint log for unexpected rebuilds, -Ztime-passes for the phase inside one crate, and what splitting a workspace into more crates actually changes.

3 parts

Async Rust under the hood

Five async Rust problems reproduced in one small tokio program: an oversized future, a future that is not Send, a blocking call, a select! loop that loses data, and a shutdown that hangs. Then the cost of async fn through dyn Trait, and cleanup when Drop cannot await.

Articles

Through the layers

Why a Rust Future Is Not Send Across .await

A Rust future is Send only when every stored state is safe to move across threads. Trace captures and locals across await to fix spawn errors.

Through the layers

Why Rust Async Futures Get So Large

Rust async functions store every value needed after an await. Learn to measure future size, identify saved fields, and reduce it without boxing everything.

Through the layers

Building a Sandboxed Code Playground in Rust

How I built a custom code execution engine backed by a Rust/Axum microservice, embedded directly in blog articles. Covers the security model, subprocess isolation with setrlimit, HMAC request signing, and the tradeoffs behind every decision.

Through the layers

A Rust FFI Boundary Is More Than repr(C)

A tested Rust-and-C FFI boundary covering ABI layout, pointer validation, ownership, panics, error reporting, callbacks, versioning, and cross-language harnesses.

Through the layers

Why Procedural Macro Errors Point at the Wrong Code

A span comparison harness showing why generated Rust errors point at the macro invocation, how to preserve input spans, and how to test diagnostics with compile-fail fixtures.

Through the layers

Where a Rust String Literal Actually Lives

When you write let s: &str = "hello world", there are really two things in one line, living in two different places. Here is what each one is, where it lives, and why that explains lifetimes.

Through the layers

Acquire Release Ordering in Rust

Learn how Rust's Acquire and Release memory orderings work, their importance in concurrent programming, and how they enable lock-free synchronization between threads.

Through the layers

Why Aho–Corasick Streaming Still Needs a Buffer

An automaton can recognize a cross-chunk match with little state, yet a streaming API may still need bytes. The missing distinction is recognition state versus output state.

Interrupted execution

Cancellation Safety in Async Rust, Explained

Async cancellation usually means dropping a future after Pending. See how select loops lose partial progress and how to design restartable operations.

Shorter pieces and reference

Older, introductory or narrower pieces on the same subject.

Engineering context

When implementation details shape the product

I work on systems where compiler behaviour, runtime constraints, correctness, and performance are part of the design, not cleanup after it.