Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
147: async fn Desugaring
I've always found that the word "sugar" in programming is a bit of a trap. When we say something is "syntactic sugar," it sounds like it's just a cosmetic change—like a shortcut for your fingers. But with async fn in Rust, the desugaring process is actually a massive architectural shift. It's not just a shortcut; it's a complete transformation of your function into a state machine.
Let's actually look at this in action. I'll start with something simple: a function that pretends to fetch a user's name from a database.
The No-Op Surprise
async fn get_username(id: u32) -> String {
// Imagine a network call here
format!("User-{}", id)
}
fn main() {
let name = get_username(42);
println!("Got: {}", name);
}
If you're new to async, your first instinct is that this should print "Got: User-42". But if you try to run this, the compiler will scream at you. It'll tell you that you're trying to print a type that doesn't implement Display. Specifically, it'll tell you that get_username returns an impl Future<Output = String>, not a String.
This is our first clue. I wrote -> String, but the compiler sees -> impl Future. The async keyword has fundamentally changed the return type of the function.
Tricking the Compiler for Type Info
I want to know exactly what the compiler thinks this "Future" is. Since the type is anonymous (the compiler generates a unique name for it), I can't just type it out. A trick I always use is to intentionally cause a type mismatch to force the compiler to tell me what it's looking at.
fn main() {
let name = get_username(42);
let _: () = name; // I know this is wrong; I'm doing this on purpose.
}
The error message will look something like: expected `()`, found opaque type `impl Future<Output = String>`. This confirms that the async fn is actually a regular function that returns a object implementing the Future trait. The "desugaring" is essentially this:
// What I wrote:
async fn get_username(id: u32) -> String { ... }
// What the compiler sees (roughly):
fn get_username(id: u32) -> impl Future<Output = String> {
async move {
// the original function body goes here
}
}
Where the Locals Go
Now, here is where it gets interesting. If a function returns immediately, that's fine. But async functions usually have .await points. When a function hits an .await, it pauses. But where do the local variables live while the function is paused? They can't stay on the stack because the function has technically already "returned" the Future object to the caller.
Let's add a local variable to see how it complicates things:
async fn fetch_and_process(id: u32) -> String {
let query = format!("SELECT name FROM users WHERE id = {}", id);
let result = db_call(&query).await; // Pause point
format!("Processed: {}", result)
}
In a normal function, query would be on the stack. In an async fn, the compiler desugars this into a hidden struct. It's essentially doing this behind the scenes:
enum FetchAndProcessState {
Start { id: u32 },
WaitingForDb { query: String }, // 'query' is moved into the state machine!
Finished,
}
struct FetchAndProcessFuture {
state: FetchAndProcessState,
}
impl Future for FetchAndProcessFuture {
type Output = String;
fn poll(mut self: Pin&mut Self, cx: &mut Context<'_>) -> Poll<Self::Output> {
// The logic to transition between states based on whether
// the db_call is ready
}
}
This is the "aha!" moment. The async fn is just a factory that produces a state machine. Every variable that exists across an .await point is moved into that struct. This is why async in Rust is so efficient—there's no hidden heap allocation for every single call (unless you Box it); it's just a struct that grows to fit the largest possible state of your function.
The Cost of the Sugar
Understanding this desugaring explains a lot of the "weird" parts of Rust async. For example, why do we need Pin? Because if the FetchAndProcessFuture struct contains a reference to one of its own fields (a self-referential pointer), moving that struct in memory would invalidate the pointer. Since the state machine is just a struct being passed around, we have to "pin" it in place to ensure those internal references stay valid.
Next time you write async fn, don't think of it as a function that runs asynchronously. Think of it as a function that builds a state machine which can be driven to completion by an executor.
📋 Practical Task
Manual Future State Machine Implementation
In this exercise, you will simulate the desugaring process. Instead of using async fn, you are going to manually implement the Future trait for a custom state machine.
The Goal: Create a TimerFuture that completes after a specific number of "polls" (to simulate time passing without needing a real system clock).
Requirements:
- Define a struct
TimerFuturethat holds acount: u32. - Implement
FutureforTimerFuture. - The
pollmethod should decrement thecount. - If
countis greater than 0, returnPoll::Pending. - If
countreaches 0, returnPoll::Ready("Timer Finished!"). - In your
mainfunction, use a simple loop or a basic executor to poll the future until it returnsReady, printing the current count at each step.
There are no comments for now.