Top 50 Rust Interview Questions & Answers (2026)
About Rust
Top 50 Rust interview questions covering ownership, borrowing, lifetimes, concurrency, async programming, and systems programming concepts. Companies hiring for Rust roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a Rust Interview
Expect a mix of conceptual and practical Rust questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the Rust questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every Rust developer must know.
01
What is Rust and what problems does it solve?
Rust is a systems programming language created by Mozilla Research and first released in 2015. It solves the fundamental tension in systems programming between memory safety and performance: languages like C and C++ are fast but prone to memory bugs (buffer overflows, use-after-free, data races), while garbage-collected languages (Java, Go) are safe but have unpredictable latency from the GC. Rust achieves memory safety without a garbage collector through its ownership and borrow checker system — a set of compile-time rules that prevent memory errors entirely. This makes Rust ideal for systems software, WebAssembly, embedded devices, network services, and any domain where both safety and performance are critical.
02
What are Rust's ownership rules?
Rust's ownership system is governed by three rules enforced at compile time: 1) Each value has exactly one owner — a variable that holds a value is its owner. 2) There can only be one owner at a time — you cannot have two variables that simultaneously own the same data. 3) When the owner goes out of scope, the value is dropped — Rust automatically inserts a drop() call, freeing the memory. When you assign a value to another variable or pass it to a function, ownership moves: the original variable becomes invalid and the compiler rejects any further use of it. This eliminates double-free and use-after-free bugs at compile time without needing a garbage collector.
03
What is borrowing in Rust?
Borrowing allows you to use a value without taking ownership of it, by creating a reference. An immutable reference &T allows read-only access and multiple immutable borrows can exist simultaneously. A mutable reference &mut T allows read-write access, but only one mutable reference can exist at a time and no immutable references can coexist with it — this prevents data races at compile time. References must always point to valid data (the borrow checker enforces this via lifetimes). Borrowing is how Rust lets you pass data to functions without moving ownership, so the caller retains ownership after the function returns.
04
What is the borrow checker in Rust?
The borrow checker is Rust's compile-time static analysis component that enforces the ownership and borrowing rules. It tracks the scope and lifetime of every reference and ensures no reference outlives the data it points to (preventing dangling pointers), no mutable reference coexists with any other reference to the same data (preventing data races), and no value is used after it has been moved or dropped. When the borrow checker rejects your code, the compiler produces a detailed error message explaining the violation and often suggests how to fix it. The borrow checker is the mechanism that gives Rust its memory safety guarantees without runtime overhead — all checks happen at compile time.
05
What are lifetimes in Rust?
Lifetimes are Rust's way of tracking how long references are valid. Every reference in Rust has a lifetime — the scope during which the reference must remain valid. Most of the time the compiler infers lifetimes automatically through lifetime elision rules, so you do not need to annotate them. Explicit lifetime annotations are needed when the compiler cannot infer them — typically in functions that return references or in structs that hold references. The syntax is 'a (a tick followed by a name): fn longest<'a>(x: &'a str, y: &'a str) -> &'a str. Lifetimes prevent dangling references — the borrow checker uses them to verify that returned references are always valid for as long as they are used.
06
How do you define structs and impl blocks in Rust?
A struct groups related data under a named type: struct Point { x: f64, y: f64 }. You instantiate it with Point { x: 1.0, y: 2.0 }. An impl block adds methods and associated functions to a struct. Methods take &self (immutable access), &mut self (mutable access), or self (takes ownership). Associated functions (like static methods) do not take a self parameter — Point::new() is a common convention for constructors. Multiple impl blocks are allowed for the same type. Structs plus impl blocks are how Rust achieves object-oriented encapsulation without classes or inheritance.
07
What are Rust enums and how do they differ from C enums?
Rust enums are algebraic data types that can hold data in each variant, making them far more powerful than C enums which are just named integers. Each variant can contain different types and amounts of data: enum Shape { Circle(f64), Rectangle(f64, f64), Point }. The standard library's most important enums are Option<T> (either Some(T) or None) and Result<T, E> (either Ok(T) or Err(E)). Enums are used with match expressions for exhaustive pattern matching — the compiler ensures all variants are handled. Internally, Rust enums are stored as tagged unions, so they are stack-allocated and have zero overhead compared to manual tagged union implementations in C.
08
What is the match expression in Rust?
The match expression is Rust's powerful pattern matching construct, similar to a switch statement but far more capable. It matches a value against a series of patterns and executes the corresponding arm. Match is exhaustive: the compiler forces you to handle every possible case — forgetting a variant is a compile error, not a runtime surprise. Patterns can match literal values, ranges (1..=5), enum variants with destructuring, tuples, structs, guards (if condition), and the catch-all _. Each arm can bind matched values to variables. For example, matching on Option<i32>: match value { Some(n) => println!("{}", n), None => println!("empty") }.
09
What is Option<T> in Rust and why does Rust have no null?
Rust has no null — Tony Hoare called null his "billion-dollar mistake" because null references cause crashes across virtually all languages. Instead, Rust uses Option<T>, an enum with two variants: Some(T) (contains a value) and None (represents the absence of a value). The type system enforces that you cannot use an Option<T> value as if it were T without first checking for None — typically with match, if let, unwrap_or(), or the ? operator. This makes the possibility of absence explicit in the type signature, eliminating null pointer exceptions at compile time. Functions that may not return a value must declare their return type as Option<T>.
10
What is Result<T, E> in Rust?
Result<T, E> is Rust's primary error handling mechanism, an enum with two variants: Ok(T) (operation succeeded and contains the value) and Err(E) (operation failed and contains the error). Functions that can fail return Result instead of throwing exceptions. The caller must handle both variants — ignoring a Result produces a compiler warning. Rust has no exceptions, so errors are values that flow through the call stack explicitly. This makes error handling visible in function signatures and prevents unexpected control flow. The ? operator provides ergonomic error propagation: it unwraps Ok and returns early with Err if the operation failed, eliminating repetitive match boilerplate.
11
What is Cargo and what are its main commands?
Cargo is Rust's official build system and package manager, included with every Rust installation. Key commands: cargo new my-project creates a new project with a Cargo.toml manifest and a src/main.rs entry point. cargo build compiles the project in debug mode (fast compilation, no optimizations). cargo build --release compiles with full optimizations for production. cargo run compiles and executes the project. cargo test runs all unit and integration tests. cargo add serde adds a dependency. cargo check type-checks code without producing a binary (much faster than a full build — ideal during development). cargo doc --open generates and opens HTML documentation.
12
What are closures in Rust?
Closures in Rust are anonymous functions that can capture variables from their enclosing scope. The syntax is |params| expression or |params| { body }. Rust closures implement one of three traits depending on how they capture the environment: FnOnce (can be called once, consumes captured variables — every closure implements this), FnMut (can be called multiple times, mutably borrows captured variables), Fn (can be called any number of times, immutably borrows captured variables). Closures are passed to iterator methods like map(), filter(), and fold(). The move keyword forces a closure to take ownership of all captured variables, which is necessary for closures passed to threads.
13
What are traits in Rust?
Traits in Rust are similar to interfaces in other languages — they define a set of methods that a type must implement to "be" that trait. Define a trait with trait Drawable { fn draw(&self); } and implement it for a type with impl Drawable for Circle { fn draw(&self) { ... } }. Traits enable polymorphism: a function can accept any type that implements a trait using generics (fn render<T: Drawable>(item: T)) or trait objects (fn render(item: &dyn Drawable)). The standard library's most important traits include Display, Debug, Clone, Copy, Iterator, From/Into, and PartialEq. Unlike interfaces, traits can have default method implementations.
14
What are iterators and the Iterator trait in Rust?
The Iterator trait in Rust requires implementing a single method: fn next(&mut self) -> Option<Self::Item>. It returns Some(item) for each element and None when exhausted. In practice, you rarely implement next() manually — you use the rich set of iterator adapters that the trait provides: map() (transform each element), filter() (keep elements matching a predicate), flat_map(), take(n), skip(n), zip(), enumerate() (add index), and consumers like collect() (gather into a collection), fold(), sum(), any(), all(). Rust iterators are lazy: adapters build a pipeline that does no work until a consumer is called. This is a zero-cost abstraction — the compiler optimizes iterator chains into tight loops.
15
What is Vec<T> in Rust?
Vec<T> is Rust's dynamically-sized, heap-allocated contiguous array — the most commonly used collection. Create one with vec![1, 2, 3] or Vec::new(). Push elements with v.push(4), access by index with v[0] (panics on out-of-bounds) or v.get(0) (returns Option). The Vec owns its elements — when the Vec is dropped, all elements are dropped. Internally it manages a heap-allocated buffer, doubling capacity when it grows beyond the current capacity. Pre-allocate with Vec::with_capacity(n) to avoid reallocations when you know the approximate size upfront. Iterate over a Vec with a for loop or iterator methods — borrowing it with &v or consuming it.
16
What is the difference between String and &str in Rust?
String is an owned, heap-allocated, growable UTF-8 string. You can modify it, append to it with push_str(), and it is dropped when it goes out of scope. &str (string slice) is a borrowed reference to a sequence of UTF-8 bytes — it does not own the data and cannot be modified. String literals like "hello" are &'static str (stored in the binary). The relationship: String can be converted to &str cheaply by taking a slice (&my_string), but going from &str to String requires allocation (my_str.to_string() or String::from(my_str)). Functions that only need to read a string should accept &str — it works with both String and string literals, making the API more flexible.
17
What are Rust modules and the use statement?
Rust's module system organizes code into a tree of namespaces. Declare an inline module with mod my_module { ... } or in a separate file src/my_module.rs (or src/my_module/mod.rs for a directory module). Items (functions, structs, enums) are private by default; use pub to make them accessible from outside the module. The use statement brings items into scope to avoid fully qualified names: use std::collections::HashMap;. Use use with aliases: use std::io::Result as IoResult;. The pub use combination re-exports items from the current module, building a clean public API surface that hides internal module organization from library consumers.
18
What are if let and while let in Rust?
if let is syntactic sugar for a match that only handles one pattern, avoiding the verbosity of a full match when you only care about one case. Instead of match opt { Some(v) => do_something(v), _ => () }, you write if let Some(v) = opt { do_something(v) }. You can add an else branch for the non-matching case. while let loops as long as the pattern matches: while let Some(top) = stack.pop() { ... } — this is the idiomatic way to process a stack or iterator-like structure. Both constructs make code more readable when full exhaustive matching is unnecessary.
19
What is pattern matching with destructuring in Rust?
Destructuring is Rust's ability to break apart composite types (structs, enums, tuples) into their component parts in a single let binding or match arm. Tuple destructuring: let (x, y) = (1, 2);. Struct destructuring: let Point { x, y } = point;. Enum destructuring in match: match shape { Shape::Circle(radius) => ..., Shape::Rectangle(w, h) => ... }. You can use _ to ignore fields, .. to ignore remaining fields in a struct, and nested destructuring for complex types. Destructuring also works in function parameters: fn print_coords(&(x, y): &(i32, i32)). It is one of Rust's most ergonomic features for working with structured data.
20
What is a panic in Rust and when does it occur?
A panic is Rust's mechanism for handling unrecoverable errors — situations where the program cannot continue safely. When a panic occurs, Rust unwinds the stack (running drop handlers), prints an error message with a backtrace (if RUST_BACKTRACE=1 is set), and terminates the thread (or the entire process if it is the main thread). Common causes: calling .unwrap() on a None or Err value, index out of bounds (v[100] when the vec has fewer elements), integer overflow in debug mode, and explicit panic!("message"). In production code, prefer returning Result over panicking. Use std::panic::catch_unwind() to catch panics at a boundary if needed (e.g., in FFI). Configure panic = "abort" in Cargo.toml for smaller binary size at the cost of no stack unwinding.
Practical knowledge for developers with hands-on experience.
01
How does error handling with the ? operator work in Rust?
The ? operator is syntactic sugar for propagating errors from Result and Option values without writing repetitive match boilerplate. When applied to a Result<T, E>, ? either unwraps the Ok(T) value (continuing execution) or immediately returns Err(e) from the enclosing function. For Option<T>, it either unwraps Some(T) or returns None. The ? operator also calls From::from() on the error type, enabling automatic error type conversion. This means you can use ? in a function that returns a different error type as long as the conversion is implemented. It is only usable in functions that return Result or Option. The main() function can return Result<(), Box<dyn Error>> to use ? at the top level.
02
What are the thiserror and anyhow crates for error handling?
thiserror is used for library crate error handling. It provides a #[derive(Error)] macro that generates the std::error::Error and Display implementations for your custom error enum, eliminating the boilerplate. Each variant can have a #[error("message")] attribute and #[from] to auto-implement From conversions. anyhow is used for application (binary) error handling. Its anyhow::Error type is a type-erased error box that can hold any error implementing std::error::Error, with automatic context chaining via .context("what failed"). The philosophy: use thiserror in library crates where callers need to match on specific error variants, and anyhow in application code where you just need to propagate and display errors clearly.
03
What are trait objects (dyn Trait) in Rust?
Trait objects (dyn Trait) enable runtime polymorphism — storing and calling values of different types through a common interface when the concrete type is not known at compile time. A trait object is a fat pointer: a pair of (data pointer, vtable pointer) where the vtable holds pointers to the concrete type's method implementations. Syntax: &dyn Drawable, Box<dyn Drawable>. Trait objects require the trait to be object-safe: no generic methods, no methods returning Self. The trade-off versus generics: trait objects allow heterogeneous collections (a Vec<Box<dyn Animal>> holding Dogs, Cats, etc.) but incur a virtual dispatch overhead and prevent inlining. Generics (static dispatch) have zero overhead but generate monomorphized code for each concrete type.
04
What are generics in Rust and how do where clauses work?
Generics let you write code that works over many types while maintaining type safety. A generic function: fn largest<T: PartialOrd>(list: &[T]) -> &T. The T: PartialOrd is a trait bound — it constrains which types are valid. Where clauses move complex bounds out of the function signature into a more readable position: fn foo<T, U>(t: T, u: U) where T: Display + Clone, U: Debug. Rust implements generics through monomorphization: the compiler generates a separate concrete function for each unique type argument at compile time, resulting in zero-cost abstractions with no runtime overhead. The trade-off is larger binary size compared to dynamic dispatch. Generics work with functions, structs, enums, and impl blocks.
05
What are Rust smart pointers?
Rust smart pointers add capabilities beyond regular references. Box<T> allocates a value on the heap; used for large data, recursive types, and trait objects. Rc<T> (Reference Counted) enables multiple ownership for single-threaded scenarios — a value is dropped when the last Rc clone is dropped. Arc<T> is the thread-safe equivalent of Rc using atomic reference counting — use it when sharing data across threads. RefCell<T> provides interior mutability: it enforces borrowing rules at runtime (not compile time), panicking if violated — useful with Rc for shared mutable data (Rc<RefCell<T>>). Mutex<T> and RwLock<T> provide interior mutability safe for multi-threaded use (combined with Arc).
06
How does async/await work in Rust?
Rust's async/await enables asynchronous programming without blocking threads. Mark a function with async fn to make it return a Future<Output = T> instead of T — the function body does not execute until the future is polled. Use .await inside an async function to suspend execution until a future completes, yielding control to the async runtime to run other tasks. Critically, Rust's async functions are zero-cost: they compile to state machines with no heap allocation overhead per await point (unlike goroutines or threads). Rust's standard library provides the building blocks (Future trait, async/await syntax) but no runtime — you must bring your own executor like Tokio or async-std to drive futures to completion.
07
What is the Tokio runtime and how is it used in Rust?
Tokio is the most popular async runtime for Rust, providing an executor (polls futures to completion), async I/O (TCP, UDP, files via tokio::net, tokio::fs), timers (tokio::time::sleep()), synchronization primitives (tokio::sync::Mutex, channels), and a thread pool for CPU-bound tasks (tokio::task::spawn_blocking()). Annotate your main function with #[tokio::main] to run it as an async function inside a Tokio runtime. Spawn concurrent tasks with tokio::spawn(async { ... }) — tasks are lightweight (similar to goroutines). The Tokio ecosystem (axum, reqwest, sqlx) all build on Tokio, making it the de-facto standard for async web services and network programs in Rust.
08
What are Rust channels for message passing?
Rust follows the message-passing concurrency model with channels. The standard library provides std::sync::mpsc (multi-producer, single-consumer): let (tx, rx) = mpsc::channel();. Multiple senders can clone tx and send values; the single receiver calls rx.recv() (blocking) or rx.try_recv(). The channel is typed — only values of the specified type can be sent. For async code, Tokio provides tokio::sync::mpsc (multi-producer single-consumer), tokio::sync::broadcast (multi-producer multi-consumer, all receivers get every message), and tokio::sync::oneshot (sends exactly one value). Channels enforce the principle that data has a single owner: sending a value moves ownership into the channel, and the receiver takes ownership — preventing data races without locks.
09
How do you use thread::spawn in Rust?
std::thread::spawn creates a new OS thread. It takes a closure and runs it in the new thread: let handle = thread::spawn(|| { println!("from thread"); });. The closure must be 'static (contain no non-static references) and implement Send (safe to send across threads). Use the move keyword to transfer ownership of variables into the closure: thread::spawn(move || { use_my_data(data); }). The function returns a JoinHandle<T> where T is the closure's return type. Call handle.join() (returns Result) to wait for the thread to finish and get its return value. If threads need to share data, use Arc<Mutex<T>> — never raw shared mutable state.
10
How does unit testing and integration testing work in Rust?
Rust has first-class testing support. Unit tests live in the same file as the code they test, inside a module annotated with #[cfg(test)] so they are only compiled during cargo test. Mark test functions with #[test]. Use assert_eq!(a, b), assert_ne!(a, b), assert!(condition), or panic!() to fail a test. For tests expected to panic, use #[should_panic]. Integration tests live in a top-level tests/ directory, each file is compiled as a separate crate, and they can only access your crate's public API — testing the public interface as an external user would. Run all tests with cargo test, run a specific test with cargo test test_name, and run tests with output visible using cargo test -- --nocapture.
11
What is unsafe Rust and what does it allow?
Unsafe Rust is a subset of Rust inside unsafe { } blocks that allows five additional capabilities not permitted in safe Rust: 1) Dereference raw pointers (*const T, *mut T). 2) Call unsafe functions or methods. 3) Access or modify mutable static variables. 4) Implement unsafe traits. 5) Access fields of union types. Unsafe does NOT disable the borrow checker — it merely allows these additional operations. The programmer must manually uphold safety invariants within unsafe blocks. The goal is to contain unsafety: you write a small unsafe abstraction and expose a safe public API. For example, Vec uses unsafe internally but is safe to use. Never write unsafe code unless you fully understand the invariants required.
12
What is the difference between procedural macros and declarative macros in Rust?
Declarative macros (macro_rules!) match against patterns in the source code and expand to Rust code based on the matching arms, similar to hygienic textual substitution. They operate on tokens and are defined in terms of what patterns to match and what code to emit. Examples: vec![], println!(), assert_eq!(). Procedural macros are actual Rust code (functions) that receive a token stream as input and return a new token stream. They are compiled as a separate crate with proc-macro = true. There are three kinds: #[derive(MyTrait)] (add implementations automatically), #[my_attribute] (transform the attributed item), and my_macro!() (function-like, but with full Rust logic). Procedural macros are more powerful and complex, enabling libraries like Serde, Tokio, and Clap.
13
What are derive macros and which common ones does Rust provide?
Derive macros (using #[derive(...)]) automatically generate trait implementations for a type based on its structure, eliminating manual boilerplate. The Rust standard library provides these commonly-derived traits: Debug (enables {:?} formatting for debugging), Clone (enables .clone() for deep copies), Copy (marks types that are bit-copyable — requires Clone), PartialEq and Eq (enables == and != comparisons), PartialOrd and Ord (enables ordering), Hash (enables use as HashMap key). Third-party derives are ubiquitous: serde::Serialize/Deserialize (JSON, etc.), thiserror::Error, and clap::Parser for CLI arguments.
14
What is Serde and how is it used in Rust?
Serde (SERialization/DEserialization) is Rust's de-facto standard framework for converting Rust data structures to and from formats like JSON, YAML, TOML, MessagePack, and many others. Add serde = { version = "1", features = ["derive"] } and serde_json = "1" to Cargo.toml. Annotate your struct with #[derive(Serialize, Deserialize)]. Serialize to JSON: serde_json::to_string(&my_struct). Deserialize from JSON: serde_json::from_str::<MyStruct>(json_str). Serde uses a zero-copy deserialization strategy and is extremely fast. Field-level attributes like #[serde(rename = "user_name")], #[serde(skip_serializing_if = "Option::is_none")], and #[serde(default)] give precise control over the serialization format.
15
What is Clap and how do you use it for CLI argument parsing?
Clap (Command Line Argument Parser) is the most popular Rust crate for building CLI applications with rich argument parsing, help text generation, and validation. The derive API (recommended) lets you define your CLI structure as a Rust struct: annotate it with #[derive(Parser)], and each field becomes a CLI argument. Use #[arg(short, long)] for flags, #[arg(value_name = "FILE")] for positional arguments, and #[command(author, version, about)] for app metadata read from Cargo.toml. Parse args with let cli = Cli::parse(); — Clap automatically generates --help and --version flags. Subcommands are modeled as enums with #[derive(Subcommand)]. Clap validates arguments at startup and exits with a helpful error message if inputs are invalid.
16
What are lifetimes in structs and function signatures in Rust?
Explicit lifetime annotations are required in two main places. In function signatures: when a function takes multiple reference parameters and returns a reference, the compiler needs to know which input lifetime the output reference is tied to. Example: fn first_word<'a>(s: &'a str) -> &'a str — this tells the compiler the returned reference lives at least as long as the input s. In structs: if a struct holds a reference, it needs a lifetime parameter to ensure the struct cannot outlive the data it references. Example: struct Excerpt<'a> { text: &'a str }. This prevents creating an Excerpt that holds a dangling reference to a String that has been dropped. Lifetimes are never at runtime — they are purely a compile-time annotation for the borrow checker.
17
What are associated types vs generic parameters in Rust traits?
Associated types in traits define a placeholder type that an implementing type specifies once. The canonical example is Iterator: trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }. An implementor says type Item = i32; — there is only one concrete Iterator implementation per type, and you refer to the item type as I::Item. Generic parameters on traits allow multiple implementations for the same type. For example, trait From<T> lets a type implement From<String>, From<i32>, etc. simultaneously. The rule of thumb: use associated types when there is only one sensible output type for a given implementation (Iterator, Deref), and generic parameters when you want to allow multiple implementations for different input types (From, Into).
18
What is interior mutability in Rust?
Interior mutability is a design pattern that allows you to mutate data even when you only have an immutable reference to it — bypassing Rust's normal borrowing rules by moving the borrow check to runtime. Cell<T> works for Copy types — get/set values without references. RefCell<T> works for any type — call .borrow() for an immutable borrow or .borrow_mut() for a mutable borrow; if the rules would be violated, it panics at runtime instead of failing at compile time. Mutex<T> and RwLock<T> do the same but with OS-level locking for thread safety. Interior mutability is necessary for cases where the borrow checker's static analysis is too conservative — e.g., graph data structures, observer pattern implementations, and mock objects in tests.
19
What are Rust's concurrency primitives (Send and Sync traits)?
Send and Sync are marker traits that encode thread-safety at the type level. A type is Send if it is safe to transfer its ownership to another thread. Almost all Rust types are Send; notable exceptions are Rc<T> (use Arc<T> instead) and raw pointers. A type is Sync if it is safe to share a reference to it across multiple threads (i.e., &T is Send). Types with interior mutability like Cell<T> and RefCell<T> are NOT Sync — use Mutex<T> instead. The compiler automatically derives Send and Sync for types whose fields are Send/Sync. These traits make Rust's thread-safety guarantees compile-time checkable — thread::spawn requires its closure to be Send.
20
What is a HashMap in Rust and how is it used?
HashMap<K, V> is Rust's standard hash map in std::collections. Create one with HashMap::new() or use collect() on an iterator of tuples. Insert with map.insert(key, value) (returns the old value if the key existed). Access with map.get(&key) (returns Option<&V>) or map[&key] (panics if missing). The entry API is the idiomatic way to insert-or-update: map.entry(key).or_insert(default) inserts only if the key is absent and returns a mutable reference to the value. Iterate over key-value pairs with for (k, v) in &map. Keys must implement Eq and Hash. By default, Rust uses a DoS-resistant hashing algorithm (SipHash). For performance-critical code, consider FxHashMap or AHashMap from external crates.
Deep expertise questions for senior and lead roles.
01
What are zero-cost abstractions and monomorphization in Rust?
Zero-cost abstractions is Rust's guarantee that high-level constructs (iterators, generics, trait implementations, async/await) compile down to code as efficient as hand-written low-level code — you don't pay a runtime cost for the abstraction layer. The mechanism behind this for generics is monomorphization: when the compiler encounters a generic function like fn max<T: Ord>(a: T, b: T) -> T, it generates a separate, fully concrete copy of the function for each unique type it is called with (max_i32, max_f64, etc.) at compile time. Each monomorphized copy can be fully optimized and inlined by LLVM. The trade-off is longer compile times and larger binary sizes compared to dynamic dispatch, but zero runtime overhead. Iterator chains are a canonical example: they compile to tight loops with no heap allocation or function call overhead.
02
What are Pin<T> and Unpin in Rust async programming?
Pin<T> is a wrapper type that guarantees an object will not be moved in memory after it has been pinned. This is necessary for self-referential types — most commonly the state machines generated by async functions with multiple .await points. These state machines contain references to their own fields; if the state machine were moved in memory, those self-references would become dangling pointers. Pin prevents movement by making the safe API inaccessible unless the inner type implements Unpin. Unpin is a marker trait (auto-implemented for most types) that says "it is safe to move this type even when pinned." For async code, the Future trait requires fn poll(self: Pin<&mut Self>, ...) — the pin ensures the future's state machine is not moved between polls. Use Box::pin() or pin_mut!() to pin a value.
03
How does the async runtime work internally in Rust (Waker, Poll, Executor)?
Rust's async system is built on the Future trait: fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>. Poll is either Ready(value) (the future is done) or Pending (not done yet). When a future returns Pending, it is responsible for ensuring the Waker (obtained from cx.waker()) will be called when the future is ready to make progress — typically by registering it with an I/O event loop (like epoll/kqueue). An Executor (the runtime, e.g., Tokio) maintains a queue of tasks. It polls a task; if it returns Pending, the executor parks it and moves on. When the Waker is invoked (by an I/O event), it re-queues the task for polling. This cooperative scheduling allows thousands of tasks to run efficiently on a small thread pool, with no overhead per idle task.
04
What are the rules for writing correct unsafe Rust?
Writing correct unsafe Rust requires manually upholding invariants that the compiler normally enforces. Key rules: Never create undefined behavior — Rust's UB rules mirror C's: no data races, no use-after-free, no out-of-bounds access, no null dereferences, no violating Rust's aliasing rules (no &T and &mut T to the same memory simultaneously). Validate all unsafe preconditions — document them clearly with // SAFETY: comments explaining why the code is sound. Minimize unsafe surface area — wrap unsafe code in safe abstractions and audit the boundary. Uphold Send/Sync requirements — only implement these manually if your type truly satisfies the thread-safety invariants. Use tools like Miri (Rust's interpreter that detects UB in unsafe code), sanitizers (-Z sanitizer=address), and cargo audit to verify unsafe code correctness.
05
How does Rust FFI (Foreign Function Interface) work with C?
Rust can call C functions and be called from C using its FFI. To call C from Rust, declare an extern "C" block with the function signatures: extern "C" { fn strlen(s: *const c_char) -> usize; }. Calling these is unsafe because Rust cannot verify C's memory safety. Link the C library via build.rs (using the cc crate) or #[link(name = "mylib")]. Use #[repr(C)] on structs to ensure they have C-compatible memory layout. To expose Rust functions to C, annotate with #[no_mangle] (prevents name mangling) and extern "C": #[no_mangle] pub extern "C" fn my_func() -> i32 { 42 }. The bindgen crate auto-generates Rust FFI bindings from C header files, and cbindgen generates C headers from Rust. Always use std::ffi types (CStr, CString) for string interop.
06
What is procedural macro metaprogramming with syn, quote, and proc_macro2?
Writing procedural macros involves three foundational crates. proc_macro2 is a wrapper around the compiler's proc_macro crate that allows creating token streams outside of procedural macro contexts (enabling testing). syn parses a Rust token stream into a strongly-typed Abstract Syntax Tree (AST) — e.g., syn::parse_macro_input!(input as syn::DeriveInput) gives you a structured representation of a struct or enum you can programmatically inspect (field names, types, attributes). quote generates Rust code from a quasi-quoted template where you interpolate values: quote! { impl MyTrait for #name { fn method(&self) -> #return_type { #body } } }. Together they form the standard toolchain for derive macros — syn to read code, quote to write code. The workflow: take a TokenStream, parse with syn, analyze the AST, generate code with quote, return as TokenStream.
07
How do you implement a custom global allocator in Rust?
Rust allows replacing the default memory allocator (mimalloc on most platforms or the system allocator) with a custom one by implementing the GlobalAlloc trait and registering it with the #[global_allocator] attribute. The trait requires two unsafe methods: fn alloc(&self, layout: Layout) -> *mut u8 (allocate memory matching the given size and alignment) and fn dealloc(&self, ptr: *mut u8, layout: Layout) (free previously allocated memory). A common use case is switching to a faster allocator like mimalloc or jemalloc: add the mimalloc crate and write #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;. Custom allocators are also used in embedded systems (to use a fixed-size arena instead of dynamic heap), for memory tracking/profiling, and for adding guard pages or canaries around allocations for security.
08
How do you compile Rust to WebAssembly?
Rust is one of the best-supported languages for WebAssembly (Wasm). Add the wasm32-unknown-unknown target with rustup target add wasm32-unknown-unknown and build with cargo build --target wasm32-unknown-unknown --release. The wasm-bindgen crate is the key tool: it generates JavaScript glue code enabling Rust functions to call browser APIs and vice versa. Annotate functions with #[wasm_bindgen] to expose them to JavaScript. Use the web-sys crate for browser Web APIs (DOM, fetch, canvas) and js-sys for JavaScript built-ins. wasm-pack orchestrates the build pipeline: it compiles Rust to Wasm, runs wasm-bindgen, and packages the result as an npm module. Use cases include compute-intensive browser tasks (image processing, cryptography, parsers), Rust on Cloudflare Workers, and portable plugin systems.
09
What are Rust performance optimization techniques?
Key Rust performance techniques: Avoid unnecessary allocations — prefer stack-allocated types, use &str over String, reuse Vec buffers with clear() + extend(). Use iterators over index loops — the optimizer handles iterator chains better. Profile before optimizing — use cargo flamegraph or perf to find actual bottlenecks. SIMD — use std::simd (nightly) or the packed_simd / wide crates for data-parallel operations (process 8 floats at once with AVX). Profile-Guided Optimization (PGO): build with instrumentation, run workloads, rebuild using the profile data — LLVM optimizes hot paths. LTO (Link-Time Optimization): set lto = true in Cargo.toml release profile — enables inlining across crate boundaries. Avoid dynamic dispatch — prefer generics over dyn Trait in hot paths to allow inlining. Use Cow<'a, str> for borrowed-or-owned flexibility without always allocating.
10
What are Rust editions and what changed across 2015, 2018, and 2021?
Rust editions are a mechanism for introducing backwards-incompatible syntax and semantic changes without breaking existing code — crates opt into an edition in Cargo.toml with edition = "2021", and different edition crates interoperate seamlessly. Rust 2015 (the original stable release): no edition keyword needed, extern crate required to use external crates. Rust 2018: eliminated the extern crate declaration (crates in Cargo.toml are automatically in scope), introduced the async/await syntax, dyn Trait became required (previously bare Trait meant trait objects), improved module system (no more mod.rs required, path clarity improvements), non-lexical lifetimes (NLL) enabled by default for smarter borrow checking. Rust 2021: closure capture becomes more precise (closures capture individual fields not whole structs), IntoIterator for arrays, disjoint capture in closures reduces the need for explicit moves, and the panic!() macro formatting was standardized. A new edition is planned approximately every 3 years.