πŸ¦€ Rust Rust 1.78+ Async Β· WASM Β· Systems ~95 questions

Senior Rust Developer

A complete set of senior-level Rust interview questions covering ownership, borrowing, lifetimes, traits, generics, async/await, unsafe code, concurrency, systems programming, WASM, and building production Rust services.

No questions match your search. Try a different keyword.

Ownership & Borrowing

12 questions
1Explain Rust's ownership model. What are the three rules of ownership and why do they exist?

Rust's ownership system is its central innovation β€” it guarantees memory safety without a garbage collector and data-race freedom without a runtime, all enforced at compile time.

The three rules:

  1. Each value in Rust has exactly one owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value is dropped (memory freed)
let s1 = String::from("hello");
let s2 = s1;          // s1 is MOVED β€” ownership transferred to s2
// println!("{}", s1); // compile error: s1 was moved

let s3 = s2.clone();  // explicit deep copy β€” both s2 and s3 are valid
println!("{} {}", s2, s3);

// Types implementing Copy trait are copied, not moved:
let x: i32 = 5;
let y = x;            // x is COPIED (i32 is Copy)
println!("{} {}", x, y);  // both valid

Why ownership exists:

  • No double-free: Only one owner, so memory is freed exactly once when the owner drops
  • No use-after-free: Compiler tracks lifetimes; you can't use a value after it's been dropped or moved
  • No dangling pointers: References are always valid β€” the referent outlives the reference
  • No GC pauses: Memory is freed deterministically at scope exit via Drop trait, not by a garbage collector

The model is inspired by linear type theory and region-based memory management. The compiler's borrow checker enforces these rules at zero runtime cost β€” if your code compiles, it is memory safe.

2What is the difference between moving, copying, and cloning in Rust?

Move: Default for types that don't implement Copy. Ownership transfers to the new binding; the old binding is invalidated. No heap allocation β€” just transferring ownership. O(1), zero-cost.

let v1 = vec![1, 2, 3];
let v2 = v1;   // v1 moved into v2 β€” stack metadata copied, heap ownership transferred
// v1 is no longer valid

Copy: For types that implement the Copy trait β€” a bitwise copy of the value on the stack. The original remains valid. Types are Copy only if all their fields are Copy and they don't manage heap resources.

// Copy types: i8..i128, u8..u128, f32, f64, bool, char, raw pointers,
//             arrays/tuples of Copy types
let x: i32 = 42;
let y = x;      // x is bitwise-copied; both x and y are valid
let arr = [1, 2, 3];
let arr2 = arr; // arrays of Copy types are Copy

Clone: Explicit deep copy. Types implement the Clone trait. For heap-allocated types like String or Vec, this allocates new heap memory and copies the content. Can be expensive β€” always explicit in Rust (unlike languages where this happens silently).

let s1 = String::from("hello");
let s2 = s1.clone();   // new heap allocation, s1 still valid

// Clone must be derived or implemented:
#[derive(Clone)]
struct Config { host: String, port: u16 }

let cfg1 = Config { host: "localhost".into(), port: 8080 };
let cfg2 = cfg1.clone();  // deep copy of the String field too

Key insight: If an assignment could be silently expensive (heap allocation), Rust makes it explicit with .clone(). Moves are always free β€” you can see exactly where allocations happen.

3Explain borrowing rules in Rust. What is the difference between shared and mutable references?

Borrowing lets you use a value without taking ownership. References are non-owning pointers that are always valid (no dangling references).

The two borrowing rules (at any given time):

  1. You can have any number of shared (immutable) references (&T)
  2. OR you can have exactly one mutable reference (&mut T)
  3. But never both simultaneously
let mut data = vec![1, 2, 3];

// Multiple shared references β€” OK
let r1 = &data;
let r2 = &data;
println!("{:?} {:?}", r1, r2);  // both used here

// After r1, r2 are last used (NLL β€” Non-Lexical Lifetimes):
let r3 = &mut data;  // OK β€” r1, r2 are no longer in use
r3.push(4);

// Simultaneous shared + mutable β€” compile error:
let r4 = &data;
let r5 = &mut data;  // ERROR: cannot borrow as mutable because also borrowed as immutable
println!("{}", r4);  // r4 still in scope

Why these rules? They prevent data races at compile time. A data race requires: two or more pointers to the same data, at least one writing, with no synchronization. The borrowing rules make this impossible in safe Rust β€” you can never have a mutable reference coexisting with any other reference to the same data.

NLL (Non-Lexical Lifetimes, Rust 2018+): The borrow checker tracks when a reference is last used, not when its lexical scope ends. This allows the mutable borrow above β€” r1 and r2 are no longer used when r3 is created.

4What is the Drop trait and how does Rust's RAII pattern work? How do you implement custom cleanup?

RAII (Resource Acquisition Is Initialization): Resources are tied to object lifetimes. When an object goes out of scope, its destructor runs automatically, releasing resources. Rust enforces this via the Drop trait β€” no forgetting to free, no accidental double-free.

struct DatabaseConnection {
    id: u32,
    // holds connection state
}

impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        println!("Closing connection {}", self.id);
        // cleanup code here: close socket, flush buffers, etc.
    }
}

{
    let conn = DatabaseConnection { id: 1 };
    // use conn...
}   // drop() called automatically here β€” guaranteed

// Drop order: fields dropped in reverse declaration order
// Structs dropped before their fields
struct Server {
    listener: TcpListener,  // dropped second
    db: DatabaseConnection, // dropped first (reverse order)
}

Manual early drop: Use std::mem::drop(value) to drop a value before its scope ends. You cannot call value.drop() directly (the compiler prevents it to avoid double-drop).

let lock = mutex.lock().unwrap();
do_work_with_lock(&lock);
drop(lock);                    // release lock early
do_expensive_work_without_lock();  // lock not held during this

ManuallyDrop: Wraps a value and prevents its destructor from running. Used in unsafe code when you're managing memory manually or transferring ownership across FFI boundaries.

mem::forget: Consumes a value without running its destructor. Leaks any resources it holds. Safe (no UB) but logically a resource leak. Useful when handing ownership to external code (FFI).

5What are smart pointers in Rust? Explain Box, Rc, Arc, Cell, and RefCell.

Box<T> β€” heap allocation with single ownership: Allocates T on the heap. Used for: recursive types (tree nodes), large values to avoid stack overflow, trait objects (Box<dyn Trait>).

// Recursive type requires Box to break infinite-size cycle
enum List {
    Cons(i32, Box),
    Nil,
}

// Trait objects β€” dynamic dispatch
fn make_animal(kind: &str) -> Box {
    match kind { "dog" => Box::new(Dog), _ => Box::new(Cat) }
}

Rc<T> β€” reference counted, single-threaded shared ownership: Multiple owners. Freed when reference count drops to zero. Not thread-safe (no atomic operations).

use std::rc::Rc;
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared);  // increments refcount, no heap alloc
let clone2 = Rc::clone(&shared);
println!("{}", Rc::strong_count(&shared));  // 3

Arc<T> β€” atomically reference counted, thread-safe shared ownership: Like Rc but with atomic refcount. Use when sharing data across threads.

Cell<T> β€” interior mutability for Copy types: Allows mutation through a shared reference. No runtime overhead, no borrowing checks β€” safe because T must be Copy (no references can exist into the cell's value).

RefCell<T> β€” interior mutability with runtime borrow checking: Moves borrowing rules to runtime. Panics if rules are violated at runtime. Used when the borrow checker can't statically verify your logic is correct.

use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
    let mut v = data.borrow_mut();  // runtime mutable borrow
    v.push(4);
}   // mutable borrow released
let v = data.borrow();              // runtime shared borrow
println!("{:?}", *v);

// Common pattern: Rc> for shared mutable data in single-threaded code
let shared_mutable = Rc::new(RefCell::new(0));
// Thread-safe equivalent: Arc>
6What is interior mutability? When and why do you use it?

Interior mutability is a design pattern that allows you to mutate data even when there are immutable references to that data. It "moves" the enforcement of borrowing rules from compile time to runtime (or uses unsafe guarantees). This is safe as long as the rules are still upheld β€” just checked differently.

When is it needed?

  • You have a shared reference (&T) but need to mutate something inside
  • The compiler can't prove your access pattern is safe, but you can reason it is
  • Implementing observer patterns, caches, lazy initialization
use std::cell::{Cell, RefCell, OnceCell};

// Cell β€” for Copy types, zero runtime cost
struct Counter { count: Cell }
impl Counter {
    fn increment(&self) {    // takes &self (not &mut self)!
        self.count.set(self.count.get() + 1);
    }
}

// OnceCell / LazyCell β€” lazy initialization
use std::cell::OnceCell;
struct Config { data: OnceCell }
impl Config {
    fn get_data(&self) -> &str {
        self.data.get_or_init(|| expensive_load())
    }
}

// Mutex / RwLock β€” thread-safe interior mutability
use std::sync::{Arc, Mutex};
let cache: Arc>> = Arc::new(Mutex::new(HashMap::new()));
// In any thread:
let mut guard = cache.lock().unwrap();
guard.insert("key".into(), "value".into());

The key insight: RefCell/Cell are not bypassing the rules β€” they're implementing them differently. RefCell maintains a runtime borrow counter and panics if you violate the rules. It's a deliberate tradeoff: you lose compile-time guarantees but gain flexibility for patterns the borrow checker can't analyze statically.

7What is the difference between String and &str in Rust? When do you use each?

String: Owned, heap-allocated, growable UTF-8 string. You own the data and can modify it. Analogous to Vec<u8> with UTF-8 guarantees.

&str (string slice): Borrowed reference to a sequence of UTF-8 bytes. Can point into a String, a string literal (stored in the binary), or any other UTF-8 buffer. Immutable view β€” you don't own the data.

// String literals are &'static str β€” embedded in binary
let s1: &str = "hello";           // points to binary

let s2: String = String::from("hello");  // heap allocated
let s3: &str = &s2;              // borrow a slice of s2
let s4: &str = &s2[1..3];        // slice: "el"

// Coercion: String auto-derefs to &str (Deref coercion)
fn takes_str(s: &str) { println!("{}", s); }
takes_str(&s2);  // &String coerces to &str β€” works!

// Building strings
let mut owned = String::new();
owned.push_str("hello");
owned.push(' ');
owned += "world";

// String formatting
let s = format!("{} {}", first, last);

Rule of thumb:

  • Function parameters: Use &str β€” accepts both string literals and owned Strings via coercion. More flexible.
  • Return values: Use String when returning owned data; &str when returning a slice of input (tied to caller's lifetime).
  • Struct fields: Use String if the struct owns the string; &'a str if borrowing with a lifetime.
// Prefer &str for parameters:
fn greet(name: &str) -> String { format!("Hello, {}!", name) }

// Not &String:
fn greet_bad(name: &String) -> String { ... }  // less flexible
8Explain slices in Rust β€” &[T], fat pointers, and how they compare to Vec<T>.

A slice &[T] is a reference to a contiguous sequence of elements. It's a fat pointer β€” two words wide: a pointer to the data and the length. It doesn't own the data.

let arr = [1, 2, 3, 4, 5];
let vec = vec![1, 2, 3, 4, 5];

let s1: &[i32] = &arr;        // slice of array
let s2: &[i32] = &arr[1..3];  // [2, 3]
let s3: &[i32] = &vec;        // slice of Vec (Deref coercion)
let s4: &[i32] = &vec[0..2];  // [1, 2]

// Fat pointer layout:
// &[T] = { ptr: *const T, len: usize }  β€” two words on the stack

// Mutable slice
let mut data = [1, 2, 3];
let ms: &mut [i32] = &mut data;
ms[0] = 99;

// Common slice operations
let s = &[1, 2, 3, 4, 5][..];
s.len()        // 5
s.first()      // Some(&1)
s.last()       // Some(&5)
s.contains(&3) // true
s.iter()       // iterator over &i32
s.windows(3)   // overlapping windows: [1,2,3], [2,3,4], [3,4,5]
s.chunks(2)    // non-overlapping: [1,2], [3,4], [5]
s.split_at(2)  // (&[1,2], &[3,4,5])

Vec<T> vs &[T]: Vec owns heap-allocated, growable data (pointer + length + capacity β€” 3 words). A slice is just a view β€” no ownership, no capacity. Use &[T] for function parameters to accept both Vec and arrays via coercion.

// Prefer &[T] in function signatures:
fn sum(nums: &[i32]) -> i32 { nums.iter().sum() }

sum(&vec![1,2,3]);   // Vec coerces to &[i32]
sum(&[1,2,3]);       // array coerces to &[i32]
9What is the Deref trait and how does deref coercion work?

The Deref trait allows a type to behave like a reference. Implementing Deref means *value dereferences to the target type. Deref coercion is the automatic conversion of &T to &U when T: Deref<Target=U>.

use std::ops::Deref;

// Box implements Deref
let boxed = Box::new(5);
println!("{}", *boxed);  // deref to i32

// Deref coercion chains:
// String: Deref    β†’ &String coerces to &str
// Vec: Deref   β†’ &Vec coerces to &[T]
// Box: Deref     β†’ &Box coerces to &T

fn print_str(s: &str) { println!("{}", s); }
let owned = String::from("hello");
print_str(&owned);    // &String β†’ &str via Deref coercion

fn sum_slice(s: &[i32]) -> i32 { s.iter().sum() }
let v = vec![1, 2, 3];
sum_slice(&v);         // &Vec β†’ &[i32] via Deref coercion

// Custom smart pointer with Deref:
struct MyBox(T);
impl Deref for MyBox {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

let x = 5;
let y = MyBox(x);
assert_eq!(5, *y);     // *y desugars to *(y.deref())

DerefMut: Like Deref but for mutable references. Enables coercion from &mut T to &mut U. For example, &mut String β†’ &mut str.

Deref coercions happen automatically at function call boundaries, assignment, and method calls. The compiler will insert as many .deref() calls as needed to make types match.

10How does Rust prevent data races at compile time? Compare to Go's race detector and Java's synchronized.

A data race requires three conditions: two or more threads accessing the same memory, at least one writing, with no synchronization. Rust's type system makes data races impossible to compile in safe code.

How Rust achieves this:

  • Send trait: A type is Send if it's safe to transfer ownership to another thread. Most types are Send. Rc<T> is not Send (non-atomic refcount). Raw pointers are not Send.
  • Sync trait: A type is Sync if a shared reference to it can be sent to another thread. T: Sync iff &T: Send. RefCell is not Sync (runtime borrow checking isn't thread-safe).
  • Ownership rules: You can't share a non-Sync type across threads. You can't mutate shared data without synchronization (Mutex, RwLock, atomics).
// This won't compile β€” Rc is not Send:
let rc = Rc::new(5);
std::thread::spawn(move || println!("{}", rc));  // ERROR!

// Use Arc instead:
let arc = Arc::new(5);
let arc_clone = Arc::clone(&arc);
std::thread::spawn(move || println!("{}", arc_clone));  // OK

// Shared mutable data requires Mutex:
let data = Arc::new(Mutex::new(vec![]));
let data_clone = Arc::clone(&data);
std::thread::spawn(move || {
    data_clone.lock().unwrap().push(1);  // safe!
});

Comparison:

  • Go race detector: Runtime tool, samples at execution. Only catches races that actually occur in a specific test run. Adds ~2Γ— overhead. Post-hoc detection.
  • Java synchronized: Runtime locking. You can forget to lock, and the compiler won't warn you. Data races are possible if locks are inconsistently applied.
  • Rust: Compile-time prevention. A data race is a type error β€” if your code compiles, it cannot have data races in safe code. No performance overhead at runtime.
11What are the rules around moving values into closures? Explain move closures and Fn, FnMut, FnOnce.

Closures capture variables from their enclosing scope. How they capture determines which Fn traits they implement.

Capture modes: Rust automatically chooses the least restrictive capture mode: by shared reference (&T), by mutable reference (&mut T), or by move (ownership). The move keyword forces move capture for all captured variables.

let s = String::from("hello");

// By reference (default when possible):
let print = || println!("{}", s);   // captures &s
print();
println!("{}", s);  // s still valid

// By move β€” used when closure must outlive the scope of s:
let greeting = move || println!("{}", s);   // s moved into closure
// println!("{}", s);  // ERROR: s was moved
std::thread::spawn(greeting);  // needs 'static or move

Fn trait hierarchy:

  • FnOnce: Can be called once. The closure may consume (move out of) captured variables. All closures implement FnOnce. A closure that moves out of a capture can only be called once.
  • FnMut: Can be called multiple times, may mutate captured variables. Closures that mutate captures implement FnMut (and FnOnce).
  • Fn: Can be called multiple times concurrently without mutation. Closures that only borrow immutably implement Fn (and FnMut and FnOnce).
// FnOnce β€” consumes its captures
let s = String::from("hi");
let consume = || drop(s);   // FnOnce only
consume();
// consume();  // ERROR: cannot call FnOnce twice

// FnMut β€” mutates captures
let mut count = 0;
let mut increment = || { count += 1; count };  // FnMut
increment(); increment();

// Fn β€” immutable captures
let msg = String::from("hello");
let greet = || println!("{}", msg);  // Fn
greet(); greet();  // can call many times
12How does Rust handle stack vs heap allocation? What is Box used for beyond simple heap allocation?

Stack allocation (default): All local variables, function arguments, and return values go on the stack. Stack allocation is extremely fast (just adjust the stack pointer). The compiler knows the size of everything on the stack at compile time.

Heap allocation: Required for dynamically sized data or data that must outlive its creating scope. String, Vec, HashMap, Box, Arc, Rc all heap-allocate.

// Stack-allocated β€” size known at compile time
let x: i32 = 5;
let arr: [i32; 1000] = [0; 1000];  // 4KB on stack β€” be careful!

// Heap via Box:
let large: Box<[i32; 100_000]> = Box::new([0; 100_000]);  // ~400KB on heap

Box<T> use cases beyond basic heap allocation:

  • Recursive types: A type containing itself would have infinite size on the stack. Box breaks the cycle with a known pointer size.
  • Trait objects: Box<dyn Trait> is the standard way to do dynamic dispatch (heap-allocated pointer to a vtable).
  • Avoid large stack copies: Moving a huge struct through function calls copies it on the stack. Box it to move just a pointer.
  • FFI: Passing Rust data to C code often requires heap allocation.
// Trait objects β€” dynamic dispatch
trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r } }

fn largest(shapes: &[Box]) -> f64 {
    shapes.iter().map(|s| s.area()).fold(0f64, f64::max)
}

let shapes: Vec> = vec![
    Box::new(Circle { r: 1.0 }),
    Box::new(Circle { r: 2.0 }),
];

Lifetimes

8 questions
1What are lifetimes in Rust and why does the compiler need them?

Lifetimes are the compiler's way of tracking how long references are valid. They ensure that references never outlive the data they point to, eliminating dangling pointers at compile time. Every reference has a lifetime, but most are inferred automatically β€” you only annotate when the compiler can't figure it out.

// This won't compile β€” dangling reference:
fn dangle() -> &str {
    let s = String::from("hello");
    &s  // ERROR: s is dropped at end of function, reference would dangle
}

// Lifetime annotation β€” tells compiler: output reference lives as long as input
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference is valid for the shorter of x's and y's lifetimes

What lifetimes are NOT: They don't change how long data lives β€” they just describe constraints. The data's actual lifetime is determined by scope/ownership as always. Lifetime annotations are purely compile-time annotations with no runtime cost.

Why the compiler needs them: When a function takes multiple references and returns one, the compiler needs to know which input the output is tied to, so it can verify the caller doesn't use the output after an input is dropped.

let result;
{
    let s1 = String::from("long string");
    let s2 = String::from("short");
    result = longest(s1.as_str(), s2.as_str());
    println!("{}", result); // OK β€” both s1 and s2 alive here
}
// result can't be used here β€” s1 and s2 dropped
2What are lifetime elision rules? When do you need explicit lifetime annotations?

Lifetime elision rules allow the compiler to infer lifetimes in common patterns without explicit annotations. There are three rules applied in order:

  1. Each reference parameter gets its own distinct lifetime parameter
  2. If there is exactly one input lifetime, it is assigned to all output lifetimes
  3. If one of the inputs is &self or &mut self, its lifetime is assigned to all output lifetimes
// These are equivalent β€” elision handles it:
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }  // explicit form

// Rule 1 + 2: single input lifetime β†’ inferred for output
fn trim(s: &str) -> &str { s.trim() }

// Rule 3: method with &self β†’ output borrows from self
impl Config {
    fn host(&self) -> &str { &self.host }  // output tied to self's lifetime
}

// Explicit annotation needed β€” two refs, unclear which output borrows from:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }

// Explicit on structs holding references:
struct Important<'a> {
    content: &'a str,   // struct can't outlive the str it references
}

// Multiple lifetime parameters:
fn first_or_default<'a, 'b>(slice: &'a [i32], default: &'b i32) -> &'a i32 {
    slice.first().unwrap_or(default)  // ERROR if returning default β€” wrong lifetime
    // Fix: make both 'a, or return &'a i32 only from slice
}

When you MUST annotate: Multiple reference parameters with a reference return; structs/enums holding references; impl blocks for types with lifetime parameters; when you need to express that two lifetimes are the same or that one outlives another.

3What is the 'static lifetime? When does it appear and what are its implications?

'static is the longest possible lifetime β€” it means the reference is valid for the entire duration of the program. A 'static reference never becomes dangling.

Two ways a value can have a 'static lifetime:

// 1. String literals β€” stored in binary, last forever
let s: &'static str = "I live forever";

// 2. Owned data with 'static bound β€” data that owns everything
// (no borrowed references with shorter lifetimes)
fn make_static() -> Box {
    Box::new(String::from("owned string"))  // String is 'static
}

// Thread::spawn requires 'static because threads can outlive their origin scope
std::thread::spawn(|| {
    // anything captured here must be 'static
    let s = String::from("hello");  // owned, so 'static
    println!("{}", s);
});

// 'static bound on generics: T must not contain borrowed refs
fn store(val: T) -> Box {
    Box::new(val)
}

Common confusion: T: 'static does NOT mean T is a reference. It means T contains no references with shorter-than-static lifetimes. An owned String satisfies String: 'static because it owns all its data and has no borrowed components.

Global statics:

static GREETING: &str = "Hello, World!";  // &'static str
static mut COUNTER: u32 = 0;  // mutable static β€” requires unsafe to access

// lazy_static or once_cell for non-const statics:
use once_cell::sync::Lazy;
static CONFIG: Lazy = Lazy::new(|| Config::load());
4How do lifetime annotations work in structs and impl blocks?
// Struct holding a reference β€” must annotate lifetime
// Means: an Excerpt instance cannot outlive the string it references
struct Excerpt<'a> {
    text: &'a str,
    line: u32,
}

// impl block must declare the lifetime parameter
impl<'a> Excerpt<'a> {
    // Rule 3: output borrows from self (elision handles it)
    fn text(&self) -> &str { self.text }

    // Multiple refs β€” explicit needed
    fn announce_and_return<'b>(&'a self, ann: &'b str) -> &'a str {
        println!("Attention: {}", ann);
        self.text  // returns self's text, which lives 'a
    }
}

// Structs with multiple lifetime parameters
struct TwoRefs<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

// Lifetime in enum
enum Either<'a, 'b> {
    Left(&'a str),
    Right(&'b str),
}

// Combining with generic type parameters
struct Wrapper<'a, T> {
    value: &'a T,
}

impl<'a, T: std::fmt::Display> Wrapper<'a, T> {
    fn print(&self) { println!("{}", self.value); }
}

Key rule: The lifetime of a struct is at most the shortest lifetime of any reference it holds. The compiler ensures you can't use an Excerpt<'a> after the string it references is dropped.

5What are higher-ranked trait bounds (HRTBs)? When do you need for<'a>?

Higher-Ranked Trait Bounds (HRTBs) express that a trait implementation must hold for all possible lifetimes. They appear when a closure or function must work with references of any lifetime, not a specific one.

// Problem: we want F to work with &str of ANY lifetime
fn apply_to_str(f: F, s: &str) -> &str
where
    F: Fn(&str) -> &str,  // This doesn't work β€” what lifetime for the &str in Fn?
{ f(s) }

// HRTB solution: for<'a> means "for any lifetime 'a"
fn apply_to_str(f: F, s: &str) -> &str
where
    F: for<'a> Fn(&'a str) -> &'a str,  // F must work for ALL lifetimes
{ f(s) }

// Usage:
apply_to_str(|s| s.trim(), "  hello  ");  // works with any lifetime

// Common in trait objects with closures:
type BoxedFn = Box Fn(&'a str) -> &'a str>;

// The Fn/FnMut/FnOnce sugar desugars to HRTBs:
// F: Fn(&str) -> &str
// is actually:
// F: for<'a> Fn(&'a str) -> &'a str

When you encounter HRTBs:

  • Storing closures that take references in trait objects
  • Writing combinators that pass references through closures
  • Implementing parsers or visitor patterns where callbacks receive references

In practice, the Rust compiler often handles HRTBs automatically when you write Fn(&str) -> &str β€” the for<'a> is implied. You only write it explicitly when the compiler can't infer it or for clarity.

6What is the "borrow checker fight" and how do you work with it productively?

New Rustaceans often "fight" the borrow checker when code that seems logically correct is rejected. This usually signals a design issue β€” the code has implicit aliasing assumptions the borrow checker correctly rejects.

Common patterns and their solutions:

// Problem: borrow while iterating and modifying
let mut v = vec![1, 2, 3];
for item in &v {
    v.push(*item * 2);  // ERROR: can't borrow v mutably while iterating
}
// Solution: collect indices or clone, operate on separate data
let to_add: Vec = v.iter().map(|x| x * 2).collect();
v.extend(to_add);

// Problem: self-referential struct
struct Node {
    data: String,
    next: Option<&Node>,  // ERROR: can't borrow from self β€” no lifetime
}
// Solution: use indices into a Vec (arena pattern), or Box/Rc/unsafe Pin

// Problem: returning reference to local data
fn get_greeting(name: &str) -> &str {
    let greeting = format!("Hello, {}!", name);
    &greeting  // ERROR: greeting dropped at end of function
}
// Solution: return String, or use a buffer passed in by caller

// Problem: simultaneous mutable + immutable borrow
struct Cache { data: Vec, sum: Option }
impl Cache {
    fn get_sum(&mut self) -> i32 {
        if let Some(s) = self.sum { return s; }
        let s: i32 = self.data.iter().sum();  // borrows self.data
        self.sum = Some(s);  // borrows self.sum β€” OK in NLL
        s
    }
}

Productive strategies:

  • Clone when performance isn't critical β€” clarity first
  • Restructure data ownership to match access patterns
  • Use indices/IDs instead of references for self-referential structures
  • Split structs so methods can borrow disjoint fields simultaneously
  • Use RefCell/Mutex when compile-time analysis is too conservative
7What is Pin and why is it needed for self-referential types and async futures?

In Rust, values can normally be moved freely in memory. This is a problem for self-referential types β€” a type that contains a pointer to itself. If the struct is moved, the internal pointer becomes dangling.

Pin<P> is a wrapper around a pointer type that guarantees the pointed-to value will not be moved in memory, as long as it doesn't implement Unpin.

use std::pin::Pin;
use std::marker::PhantomPinned;

// Self-referential struct
struct SelfRef {
    data: String,
    ptr: *const String,  // points into self.data
    _pin: PhantomPinned, // opt out of Unpin β€” prevent moving
}

impl SelfRef {
    fn new(data: String) -> Pin> {
        let mut boxed = Box::pin(SelfRef {
            data,
            ptr: std::ptr::null(),
            _pin: PhantomPinned,
        });
        let ptr = &boxed.data as *const String;
        // Safe: we know the value is pinned and won't move
        unsafe { boxed.as_mut().get_unchecked_mut().ptr = ptr; }
        boxed
    }
}

// Why async/await needs Pin:
// An async function compiles to a state machine (a Future).
// When a future is paused at an .await point, it holds references
// to local variables in its own stack frame.
// If the Future were moved between polls, those internal references
// would dangle. Pin<&mut Future> prevents this move.

async fn example() {
    let data = String::from("hello");
    let future = some_async_fn(&data);  // future holds &data
    future.await;  // data must not move between here and the .await resolution
}

Unpin: Most types implement Unpin (auto-trait) β€” it's safe to move them even when pinned. Only types with self-references (like async state machines, or types with PhantomPinned) don't implement Unpin.

8What is the Cow (Clone on Write) type? When and why is it useful?

Cow<'a, B> (Clone on Write) is a smart pointer that can hold either a borrowed reference (Cow::Borrowed(&B)) or an owned value (Cow::Owned(B::Owned)). It only clones when mutation is actually needed.

use std::borrow::Cow;

// Function that may or may not modify the input
fn sanitize(input: &str) -> Cow {
    if input.contains("bad_word") {
        // Only allocate when we need to modify
        Cow::Owned(input.replace("bad_word", "***"))
    } else {
        Cow::Borrowed(input)  // zero allocation!
    }
}

let clean = sanitize("hello world");    // Borrowed β€” no allocation
let dirty = sanitize("hello bad_word"); // Owned β€” one allocation

println!("{}", clean);  // works with both via Deref

When Cow is valuable:

  • Functions that sometimes need to modify input and sometimes can pass it through unchanged
  • Return types where you want to avoid unnecessary copies but retain the option to own
  • Serialization/deserialization where input may or may not need transformation
  • APIs that accept either owned or borrowed data
// Cow in error messages and logging β€” avoid cloning if not needed
fn process(name: Cow) -> Result<(), Error> {
    if name.is_empty() {
        return Err(Error::InvalidName(name.into_owned()));
    }
    // use name as &str via Deref
    do_something(&name);
    Ok(())
}

// Accept both &str and String:
process(Cow::Borrowed("alice"));
process(Cow::Owned(user_input));
// Or via From implementations:
process("alice".into());
process(dynamic_string.into());

Type System & Traits

10 questions
1What are traits in Rust? How do they differ from Java interfaces and Go interfaces?

Traits define shared behavior β€” a set of methods a type must implement. They're similar to interfaces but with more power: default implementations, associated types, bounds composition, and blanket implementations.

trait Summary {
    fn summarize_author(&self) -> String;

    // Default implementation β€” can be overridden
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

struct Article { title: String, author: String, content: String }

impl Summary for Article {
    fn summarize_author(&self) -> String { self.author.clone() }
    // Override default:
    fn summarize(&self) -> String {
        format!("{}, by {}", self.title, self.author)
    }
}

// Trait as parameter β€” static dispatch (monomorphized)
fn notify(item: &impl Summary) { println!("{}", item.summarize()); }
// Equivalent verbose form:
fn notify(item: &T) { println!("{}", item.summarize()); }

Comparison:

  • vs Java interfaces: Rust traits can have default method bodies (like Java 8+ default methods). Traits support associated types (not just methods). No inheritance β€” but traits can have supertrait bounds. Coherence rules prevent conflicting implementations.
  • vs Go interfaces: Both use structural typing (implicit satisfaction). Go interfaces are purely runtime; Rust traits enable both static dispatch (monomorphization, zero-cost) and dynamic dispatch (dyn Trait). Rust traits can be bounded, composed, and have associated types. Go interfaces cannot have default methods or associated types.

Blanket implementations: Implement a trait for any type satisfying another trait. The standard library uses this extensively:

// Implement ToString for everything that implements Display
impl ToString for T {
    fn to_string(&self) -> String { format!("{}", self) }
}
2What is static vs dynamic dispatch in Rust? Compare impl Trait, generics, and dyn Trait.

Static dispatch (monomorphization): The compiler generates a separate copy of a generic function for each concrete type used. Zero runtime overhead β€” the function call is direct. Increases binary size and compile time.

// Generic / impl Trait β€” static dispatch:
fn process(item: T) { println!("{}", item); }
fn process(item: impl Display) { println!("{}", item); }
// Compiler generates: process_i32, process_str, process_Point, etc.

// Return position impl Trait β€” static, but hides concrete type:
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y  // returns a closure, caller doesn't know exact type
}

Dynamic dispatch (dyn Trait): A fat pointer containing a pointer to the data and a pointer to a vtable (virtual dispatch table). Method calls go through the vtable at runtime. Small overhead (~indirect function call). Enables heterogeneous collections.

// dyn Trait β€” dynamic dispatch:
fn process(item: &dyn Display) { println!("{}", item); }

// Enables heterogeneous collections:
let items: Vec> = vec![
    Box::new(Circle { r: 1.0 }),
    Box::new(Square { side: 2.0 }),
];

// Object safety: a trait can only be used as dyn Trait if:
// - No generic methods
// - No methods that return Self
// - No associated functions without &self receiver

When to use each:

  • Generics/impl Trait: When all types are known at compile time. Performance-critical code. When you need multiple trait bounds or associated types.
  • dyn Trait: Heterogeneous collections. Plugin systems. When reducing binary size matters. When the concrete types are determined at runtime.
3What are associated types in traits? How do they differ from generic type parameters?

Associated types are type placeholders defined within a trait. Each implementation specifies the concrete type. Unlike generic parameters on the trait, there can only be one implementation per type β€” the associated type is determined by the implementing type.

// Associated type β€” one impl per type, cleaner signatures
trait Container {
    type Item;
    fn get(&self, index: usize) -> Option<&Self::Item>;
    fn len(&self) -> usize;
}

impl Container for Vec {
    type Item = i32;
    fn get(&self, i: usize) -> Option<&i32> { self.get(i) }
    fn len(&self) -> usize { self.len() }
}

// vs Generic parameter β€” multiple impls possible for same type
trait Converter {
    fn convert(&self) -> T;
}
// A type can impl Converter AND Converter

// Iterator uses associated type β€” exactly one Item type per iterator:
trait Iterator {
    type Item;
    fn next(&mut self) -> Option;
}

// Bounds on associated types:
fn print_items(c: &C) where C::Item: Display {
    for i in 0..c.len() {
        if let Some(item) = c.get(i) { println!("{}", item); }
    }
}

Rule of thumb: Use an associated type when there should be exactly one "output type" for a given implementing type. Use a generic parameter when a type should be able to implement the trait for multiple target types (e.g., From<T>).

4Explain the Iterator trait and iterator adaptors. How does Rust achieve zero-cost abstractions here?

Rust's iterator system is a prime example of zero-cost abstractions β€” high-level functional-style code that compiles to the same assembly as hand-written loops.

// Implementing Iterator requires only next():
struct Counter { count: u32, max: u32 }

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}
// You get map, filter, fold, zip, enumerate, take, skip, ... for free!

Iterator adaptors β€” lazy, zero-cost:

let v = vec![1, 2, 3, 4, 5, 6];

// Chain of adaptors β€” nothing runs until a consumer (collect, sum, etc.)
let result: Vec = v.iter()
    .filter(|&&x| x % 2 == 0)   // keep evens
    .map(|&x| x * x)              // square them
    .collect();                   // consume β€” runs everything once

// Compiles to essentially: for x in v { if x%2==0 { result.push(x*x); } }

// Useful adaptors:
v.iter().enumerate()                   // (index, &value)
v.iter().zip(other.iter())             // pairs
v.iter().flat_map(|x| vec![x, x])     // flatten nested
v.iter().take(3).skip(1)               // first 3, skip 1
v.iter().peekable()                    // peek without consuming
v.iter().chain(other.iter())           // concatenate iterators
v.windows(3)                           // overlapping windows
v.chunks(2)                            // non-overlapping chunks

// fold (reduce):
let sum = v.iter().fold(0, |acc, x| acc + x);
// or: v.iter().sum::()

Zero-cost: Iterator adaptors create small struct types that compose. The compiler inlines all the closures, then optimizes the composed struct into a tight loop β€” often matching hand-written C performance.

5What are the From, Into, TryFrom, and TryInto traits? How do they enable idiomatic conversions?
// From β€” infallible conversion FROM T
impl From for MyType {
    fn from(s: String) -> MyType { MyType { data: s } }
}
let s = String::from("hello");         // From for String
let mt = MyType::from(String::from("world"));

// Into β€” automatically implemented when From is implemented
// If MyType: From, then String: Into
let mt: MyType = String::from("hello").into();  // .into() uses From

// This is why you write From, not Into β€” you get both:
fn process(t: impl Into) {      // accepts anything that converts
    let mt: MyType = t.into();
}
process(String::from("hello"));          // works via From

// TryFrom β€” fallible conversion, returns Result
use std::convert::TryFrom;
#[derive(Debug)]
struct EvenNumber(i32);

impl TryFrom for EvenNumber {
    type Error = String;
    fn try_from(n: i32) -> Result {
        if n % 2 == 0 { Ok(EvenNumber(n)) }
        else { Err(format!("{} is not even", n)) }
    }
}

let e = EvenNumber::try_from(4)?;   // Ok(EvenNumber(4))
let e = EvenNumber::try_from(3)?;   // Err("3 is not even")

// TryInto β€” auto-impl when TryFrom exists
let e: Result = 4i32.try_into();

Common patterns: Use From/Into in function signatures (impl Into<String>) to accept multiple types flexibly. Use From to implement error conversions β€” the ? operator uses From to convert errors automatically.

6What is the newtype pattern in Rust? What problems does it solve?

The newtype pattern wraps an existing type in a single-field tuple struct, creating a distinct type. Zero runtime overhead β€” the wrapper is erased at compile time.

// Problem: confusing two semantically different integers
fn transfer(from: u64, to: u64, amount: u64) { ... }
transfer(account_id, amount, recipient);  // Wrong! Easy to mix up

// Solution: newtypes make mixing up impossible
struct AccountId(u64);
struct Amount(u64);

fn transfer(from: AccountId, to: AccountId, amount: Amount) { ... }
// transfer(account_id, amount, recipient);  // compile error!

// Zero-cost: AccountId is identical to u64 in memory
assert_eq!(std::mem::size_of::(), std::mem::size_of::());

// Implementing external traits on external types (orphan rule workaround):
struct Meters(f64);
impl std::fmt::Display for Meters {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}m", self.0)
    }
}
// Can't impl Display for f64 (external type + external trait)
// But CAN impl Display for Meters (our type)

// Restricting API surface:
struct Password(String);
impl Password {
    pub fn new(s: String) -> Result {
        // validate...
        Ok(Password(s))
    }
    pub fn verify(&self, input: &str) -> bool { /* bcrypt check */ true }
    // No public access to inner String β€” caller can't read the password
}

Use cases: Type-safe IDs (UserId, OrderId); units of measure (Meters, Seconds); validated types (Email, Password, NonEmptyString); working around the orphan rule to add trait impls to foreign types.

7How does Rust's type inference work? What are its limits and how do you help the compiler?

Rust uses Hindley-Milner-style bidirectional type inference β€” the compiler propagates type information both forward (from definition) and backward (from how a value is used). It's powerful but has limits.

// Inference from usage:
let mut v = Vec::new();       // type unknown yet
v.push(1i32);                 // compiler infers Vec

let sum = vec![1, 2, 3].iter().sum();  // ERROR: which numeric type?
let sum: i32 = vec![1, 2, 3].iter().sum();  // OK β€” annotation provides hint

// Turbofish syntax :: β€” provide type parameter inline
let parsed = "42".parse::().unwrap();
let v: Vec<_> = (0..5).collect::>();

// Closure return types inferred from body:
let double = |x| x * 2;  // inferred as fn(i32) -> i32 from usage

// When inference fails β€” help with annotations:
// 1. Add type to let binding: let x: HashMap> = ...
// 2. Turbofish: collect::>()
// 3. Intermediate binding with type: let v: Vec<_> = iter.collect();

// Complex iterator chains often need a hint at the end:
let result: HashMap = words
    .iter()
    .map(|w| (w.to_string(), w.len()))
    .collect();  // HashMap::from_iter β€” inferred from result type

Inference across function boundaries: Rust does NOT do whole-program inference β€” function signatures must be fully annotated. This is intentional: function signatures serve as contracts and documentation. Only local variable types within a function body can be inferred.

8What is the orphan rule in Rust? Why does it exist and how do you work around it?

The orphan rule: you can only implement a trait for a type if either the trait or the type is defined in the current crate. You cannot implement external traits for external types.

// crate A defines: trait Fly
// crate B defines: struct Bird
// Your crate C: CANNOT implement Fly for Bird
// (both external β€” would be an "orphan" impl)

// Why? Coherence: if two crates both implemented Fly for Bird differently,
// the compiler wouldn't know which to use. The orphan rule ensures
// there's always at most one implementation.

// Workarounds:

// 1. Newtype pattern β€” wrap the external type
use serde::{Serialize, Deserialize};
struct MyVec(Vec);
impl Serialize for MyVec { /* ... */ }  // OK β€” MyVec is local

// 2. Blanket impl with local trait
trait MyDisplay { fn my_fmt(&self) -> String; }
impl MyDisplay for std::net::IpAddr {
    fn my_fmt(&self) -> String { format!("IP: {}", self) }
}  // OK β€” MyDisplay is local

// 3. Extension traits β€” add methods to external types
trait StrExt {
    fn word_count(&self) -> usize;
}
impl StrExt for str {
    fn word_count(&self) -> usize { self.split_whitespace().count() }
}
"hello world".word_count()  // 2

New Solver (Rust 2024+): The upcoming trait solver improvements relax some coherence requirements, but the orphan rule fundamentally remains for compatibility and predictability reasons.

9What is PhantomData and when do you need it?

PhantomData<T> is a zero-sized type that tells the compiler a struct "logically" owns or borrows values of type T, even though the struct doesn't physically contain any T. This affects variance and drop checking.

use std::marker::PhantomData;

// A typed wrapper around a raw pointer:
struct MyVec {
    ptr: *mut T,
    len: usize,
    cap: usize,
    _phantom: PhantomData,  // says: "we own T values"
}
// Without PhantomData: compiler doesn't know MyVec contains T
// β†’ T would be invariant instead of covariant
// β†’ Drop checker wouldn't ensure T is dropped correctly

// Lifetime tracking via PhantomData:
struct IterMut<'a, T> {
    ptr: *mut T,
    end: *mut T,
    _phantom: PhantomData<&'a mut T>,  // "borrows T mutably for 'a"
}
// Without this, 'a wouldn't be connected to the struct

// Type-state pattern with PhantomData:
struct Connection {
    socket: TcpStream,
    _state: PhantomData,  // zero-size β€” just for type checking
}

struct Disconnected;
struct Connected;

impl Connection {
    fn connect(self, addr: &str) -> Connection {
        // ...
        Connection { socket: self.socket, _state: PhantomData }
    }
}
impl Connection {
    fn send(&mut self, data: &[u8]) { /* ... */ }
    // Can't call send() on Connection β€” compile error!
}
10What are Rust's standard operator overloading traits? How do you implement them?
use std::ops::{Add, Sub, Mul, Neg, Index, IndexMut};
use std::fmt;
use std::cmp::{PartialEq, PartialOrd, Ordering};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 { x: f64, y: f64 }

// Arithmetic
impl Add for Vec2 {
    type Output = Vec2;
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}
impl Neg for Vec2 {
    type Output = Vec2;
    fn neg(self) -> Vec2 { Vec2 { x: -self.x, y: -self.y } }
}
// AddAssign, SubAssign, MulAssign, DivAssign β€” compound assignment +=

// Display / Debug formatting
impl fmt::Display for Vec2 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// Indexing
struct Matrix { data: [[f64; 3]; 3] }
impl Index<(usize, usize)> for Matrix {
    type Output = f64;
    fn index(&self, (r, c): (usize, usize)) -> &f64 { &self.data[r][c] }
}
impl IndexMut<(usize, usize)> for Matrix {
    fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut f64 { &mut self.data[r][c] }
}

// Custom ordering
struct Temperature(f64);
impl PartialOrd for Temperature {
    fn partial_cmp(&self, other: &Self) -> Option {
        self.0.partial_cmp(&other.0)
    }
}

// Derive macros for common traits:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Priority(u8);

Memory & Pointers

6 questions
1What is Rust's memory layout? How do structs, enums, and tuples get laid out in memory?

Rust gives you precise control over memory layout, which is essential for systems programming and FFI.

use std::mem::{size_of, align_of, offset_of};

// Default struct layout β€” compiler may reorder fields for optimal alignment
struct Default { a: u8, b: u32, c: u8 }
// Compiler reorders: b(4), a(1), c(1), pad(2) β†’ 8 bytes
// Instead of: a(1), pad(3), b(4), c(1), pad(3) β†’ 12 bytes

// repr(C) β€” C-compatible layout, fields in declaration order
#[repr(C)]
struct CLayout { a: u8, _pad: [u8; 3], b: u32, c: u8, _pad2: [u8; 3] }

// repr(packed) β€” no padding (may cause unaligned access β†’ slow or UB)
#[repr(packed)]
struct Packed { a: u8, b: u32, c: u8 }  // 6 bytes, unaligned b

// repr(align(N)) β€” force at least N-byte alignment
#[repr(align(64))]  // cache-line aligned
struct CacheAligned { data: [u8; 64] }

// Enums β€” discriminant + largest variant's size
enum Message {
    Quit,            // 0 bytes data
    Move { x: i32, y: i32 },  // 8 bytes data
    Write(String),   // 24 bytes data (String = ptr+len+cap)
}
// size = discriminant + max(0, 8, 24) = typically 25+ bytes

// Null pointer optimization: Option>, Option<&T>, Option>
// These are guaranteed the same size as the non-Option version
assert_eq!(size_of::>>(), size_of::>());
// None is represented as null pointer β€” no extra space for discriminant

Zero-Sized Types (ZST): Types with no data ((), PhantomData<T>, marker structs) have zero size. They can be used freely without any memory overhead. Collections of ZSTs are optimized to not allocate.

2What are raw pointers in Rust (*const T and *mut T)? How do they differ from references?

Raw pointers bypass Rust's safety guarantees. Unlike references, they: can be null, can be dangling, can alias (multiple raw pointers to same location), and require unsafe to dereference.

let mut x = 5;
let r1: *const i32 = &x;          // immutable raw pointer
let r2: *mut i32 = &mut x;        // mutable raw pointer

// Creating raw pointers is safe; dereferencing requires unsafe:
unsafe {
    println!("{}", *r1);           // dereference β€” must guarantee validity
    *r2 = 10;
}

// Raw pointer arithmetic (for C-style array access):
let arr = [1i32, 2, 3, 4, 5];
let ptr = arr.as_ptr();
unsafe {
    let third = *ptr.add(2);       // arr[2] = 3
    let slice = std::slice::from_raw_parts(ptr, 3);  // [1, 2, 3]
}

// Null pointers:
let null: *const i32 = std::ptr::null();
let null_mut: *mut i32 = std::ptr::null_mut();
assert!(null.is_null());

// Differences from references:
// References: always valid, aligned, non-null β€” enforced by borrow checker
// Raw pointers: no guarantees β€” programmer responsible for safety
// References: &T is 'not null' by definition
// Raw pointers: explicit null check required

// Converting between raw pointers and references:
let r: &i32 = unsafe { &*r1 };    // raw β†’ reference (unsafe)
let p: *const i32 = r as *const i32;  // reference β†’ raw (safe)
3What are NonNull, Unique, and NonZero types? Why do they exist?

These wrapper types encode invariants that enable compiler optimizations (especially null-pointer optimization) without unsafe guarantees being required at every use site.

use std::ptr::NonNull;
use std::num::NonZeroU32;

// NonNull β€” guaranteed non-null *mut T
// Enables null-pointer optimization: Option> == size_of *mut T
let ptr = NonNull::new(Box::into_raw(Box::new(42))).unwrap();
// ptr is a *mut i32 that is guaranteed non-null
// Used in: Vec, Box, LinkedList internals

// Null-pointer optimization:
assert_eq!(
    std::mem::size_of::>>(),
    std::mem::size_of::<*mut i32>()
);  // Option overhead is zero!

// NonZero integer types β€” same optimization for integers
let n = NonZeroU32::new(42).unwrap();
// Option is same size as u32
// None represented as 0, Some(n) as n

// Useful in data structures:
struct Handle(NonZeroU32);  // 0 = invalid handle, naturally
// Option is free β€” uses 0 to represent None

// In practice:
fn divide(a: i32, b: NonZeroU32) -> i32 {
    a / b.get() as i32  // no runtime divide-by-zero check needed!
}
4How does Rust handle stack allocation for large data structures? What are the risks?

Rust allocates local variables on the stack by default. For large data, this can cause stack overflow β€” the default stack size is typically 8MB on Linux, less on some platforms and threads.

// Stack overflow risk:
fn dangerous() {
    let big: [u8; 8 * 1024 * 1024] = [0; 8_388_608];  // 8MB β€” may overflow!
    process(&big);
}

// Safe: heap-allocate large data
fn safe() {
    let big: Box<[u8; 8_388_608]> = vec![0u8; 8_388_608].into_boxed_slice()
        .try_into().unwrap();
    // Or:
    let big = Box::new([0u8; 8_388_608]);
}

// Recursive functions β€” beware deep recursion
fn factorial(n: u64) -> u64 {
    if n <= 1 { 1 } else { n * factorial(n - 1) }
}
// factorial(100_000) β†’ stack overflow!

// Fix: iterative or use stacker crate for dynamic stack growth
fn factorial_iter(n: u64) -> u64 {
    (1..=n).product()
}

// Custom stack size for threads:
std::thread::Builder::new()
    .stack_size(32 * 1024 * 1024)  // 32MB stack
    .spawn(|| deep_recursive_work())
    .unwrap();

// stacker crate β€” dynamically grow stack as needed:
fn deep(n: usize) {
    stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
        if n > 0 { deep(n - 1); }
    });
}

Best practices: Return large structs by value (the compiler uses NRVO/RVO to avoid copies). Pass large structs by reference. Box large arrays. Be aware that initializing a large stack array to zero ([0u8; N]) is a memset β€” still uses stack space even if immediately boxed in some cases.

5What is the global allocator in Rust? How do you use custom allocators?
// Custom global allocator β€” replace the default (system allocator)
use std::alloc::{GlobalAlloc, System, Layout};

// Use jemalloc for better performance:
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

// Or mimalloc:
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

// Tracking allocator for testing/debugging:
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
struct TrackingAllocator;
static ALLOCATED: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for TrackingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ptr = System.alloc(layout);
        if !ptr.is_null() { ALLOCATED.fetch_add(layout.size(), SeqCst); }
        ptr
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout);
        ALLOCATED.fetch_sub(layout.size(), SeqCst);
    }
}

#[global_allocator]
static A: TrackingAllocator = TrackingAllocator;

// Per-allocation custom allocator (nightly feature: Allocator trait):
// Vec::new_in(my_allocator) β€” allocate in a specific arena
// Useful for: memory pools, arena allocators, NUMA-aware allocation

// no_std with no allocator β€” embedded/kernel contexts:
#![no_std]
// Can only use stack-allocated types, no Vec/String/Box
6What is the Sized trait and dynamically sized types (DSTs)?

Sized is a marker trait automatically implemented for all types whose size is known at compile time. Most types are Sized. Dynamically Sized Types (DSTs) have a size only known at runtime.

// DSTs: str, [T], dyn Trait β€” size not known at compile time
// They can only exist behind a reference or pointer (fat pointer)

let s: &str = "hello";         // fat pointer: ptr + len
let arr: &[i32] = &[1, 2, 3]; // fat pointer: ptr + len
let obj: &dyn Display = &42;   // fat pointer: ptr + vtable ptr

// You can't have a DST directly on the stack:
// let s: str = *"hello";      // ERROR: str is not Sized!
// let a: [i32] = [1, 2, 3];   // ERROR

// ?Sized bound β€” accept both Sized and non-Sized types:
fn print_size(val: &T) {
    println!("{:?}", val);
}
print_size("hello");            // &str β€” not Sized, works with ?Sized
print_size(&42i32);             // &i32 β€” Sized, also works

// Struct with DST field (must be the last field):
struct SliceWrapper {
    len: usize,
    data: [T],   // DST field β€” SliceWrapper is also a DST
}
// Only usable behind a &SliceWrapper or Box>

// Generic function β€” implicitly Sized:
fn process(val: T) { }        // T: Sized (implicit)
fn process(val: &T) { }  // opt out β€” accepts DSTs too

Concurrency

8 questions
1How do you share state between threads in Rust? Compare Mutex, RwLock, and atomics.
use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::{AtomicUsize, Ordering};

// Mutex β€” exclusive access (one thread at a time)
let counter = Arc::new(Mutex::new(0u32));
let c = Arc::clone(&counter);
std::thread::spawn(move || {
    let mut guard = c.lock().unwrap();  // blocks until lock acquired
    *guard += 1;
});  // lock released when guard drops

// Mutex poisoning: if a thread panics while holding a lock, the mutex
// becomes "poisoned". lock() returns Err. Recover with into_inner():
match mutex.lock() {
    Ok(guard) => *guard,
    Err(poisoned) => *poisoned.into_inner(),  // recover the data
}

// RwLock β€” multiple readers OR one writer
let data = Arc::new(RwLock::new(HashMap::new()));
let d = Arc::clone(&data);
// Reader threads:
let read_guard = data.read().unwrap();   // shared β€” multiple readers OK
// Writer thread:
let mut write_guard = data.write().unwrap();  // exclusive
write_guard.insert("key", "value");

// Atomics β€” lock-free for single values
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);   // atomic increment
COUNTER.load(Ordering::Acquire);           // atomic read
COUNTER.store(42, Ordering::Release);      // atomic write
COUNTER.compare_exchange(old, new, Ordering::AcqRel, Ordering::Acquire);

Ordering semantics:

  • Relaxed: no synchronization, just atomicity. Use for counters where exact ordering doesn't matter.
  • Acquire: reads can't move before this. Use with load to "acquire" data.
  • Release: writes can't move after this. Use with store to "release" data.
  • AcqRel: both Acquire and Release. Use with read-modify-write operations.
  • SeqCst: total order across all threads. Safest but slowest. Good default when unsure.
2What are channels in Rust? Compare std::sync::mpsc, crossbeam, and flume.
use std::sync::mpsc;

// mpsc = multiple producer, single consumer
let (tx, rx) = mpsc::channel();   // unbounded
let (tx, rx) = mpsc::sync_channel(100);  // bounded (blocks sender at 100)

// Multiple senders via clone:
let tx2 = tx.clone();
std::thread::spawn(move || tx.send(1).unwrap());
std::thread::spawn(move || tx2.send(2).unwrap());

// Receiver:
for received in rx { println!("{}", received); }  // blocks until all senders drop

// mpsc limitations:
// - Only one receiver (no MPMC)
// - Unbounded channel has no backpressure

// crossbeam-channel: MPMC, better performance, select! macro
use crossbeam::channel::{unbounded, bounded, select};
let (s, r) = unbounded::();      // MPMC unbounded
let (s, r) = bounded::(100);     // MPMC bounded

// select! β€” wait on multiple channels simultaneously
select! {
    recv(r1) -> msg => println!("r1: {:?}", msg),
    recv(r2) -> msg => println!("r2: {:?}", msg),
    default(Duration::from_millis(100)) => println!("timeout"),
}

// flume: simple MPMC, close to std::sync::mpsc API, great performance
let (tx, rx) = flume::bounded(32);
let tx2 = tx.clone();  // multiple producers
// rx.clone() gives multiple consumers β€” true MPMC

When to use channels vs Mutex: Channels for transferring data ownership between threads (message passing). Mutex for shared state that multiple threads need to read/update in place. Channels are often clearer and avoid lock contention by keeping data in one place at a time.

3What is Rayon and how does it enable data parallelism in Rust?

Rayon is a data parallelism library that makes it trivial to parallelize iterator-based operations. It uses a work-stealing thread pool internally, the same model as Java's Fork/Join framework.

use rayon::prelude::*;

let numbers = vec![1i64, 2, 3, 4, 5, 6, 7, 8];

// Sequential β†’ parallel: just change .iter() to .par_iter()
let sum: i64 = numbers.par_iter().sum();
let doubled: Vec = numbers.par_iter().map(|&x| x * 2).collect();
let evens: Vec<&i64> = numbers.par_iter().filter(|&&x| x % 2 == 0).collect();

// Parallel sort:
let mut data = vec![5, 3, 1, 4, 2];
data.par_sort();
data.par_sort_unstable();  // faster, less stable

// Parallel fold/reduce:
let sum = numbers.par_iter()
    .fold(|| 0i64, |acc, &x| acc + x)  // fold per thread
    .sum();                              // combine thread results

// Parallel map + collect into specific collection:
let map: HashMap = numbers.par_iter()
    .map(|&x| (x, x * x))
    .collect();

// Custom thread pool:
let pool = rayon::ThreadPoolBuilder::new()
    .num_threads(4)
    .build()
    .unwrap();
pool.install(|| numbers.par_iter().sum::());

When to use Rayon: CPU-bound operations on large collections where work per element is significant. Not useful for I/O-bound work (use async for that). Rule of thumb: if a sequential map/filter/fold is your bottleneck and the work per element is non-trivial, Rayon is a one-line change to get parallelism.

Safety guarantee: Rayon requires T: Send + Sync for parallel iterators β€” the type system prevents data races even in parallel code.

4What is std::thread::scope and how does it enable non-'static thread spawning?

std::thread::spawn requires 'static closures because threads can outlive their creating scope. thread::scope (stable since Rust 1.63) creates a scope that guarantees all spawned threads finish before the scope exits β€” enabling non-static borrows.

use std::thread;

let data = vec![1, 2, 3, 4, 5];

// Old way β€” requires moving data or cloning:
// thread::spawn(|| println!("{:?}", data));  // ERROR: data not 'static

// thread::scope β€” borrows are allowed! Threads guaranteed to finish before scope exits
thread::scope(|s| {
    s.spawn(|| {
        println!("{:?}", data);  // borrows data β€” no move needed!
    });
    s.spawn(|| {
        println!("length: {}", data.len());  // another borrow β€” fine!
    });
    // Both threads finish here before scope exits
});
// data still usable here

// Scoped threads can borrow local variables:
let mut results = vec![0i32; 4];
thread::scope(|s| {
    for (i, slot) in results.iter_mut().enumerate() {
        s.spawn(move || {
            *slot = i as i32 * i as i32;  // write to disjoint slots
        });
    }
});
println!("{:?}", results);  // [0, 1, 4, 9]

Use cases: Parallel processing of borrowed data (parallel map without clone). Fork-join patterns where you need results from threads. Any time you want threading for performance but can't afford to move/clone data.

5What are OnceLock, LazyLock, and OnceMethods? How do you implement thread-safe singletons?
use std::sync::{OnceLock, LazyLock};

// OnceLock β€” thread-safe, initialized at most once (stable 1.70+)
static CONFIG: OnceLock = OnceLock::new();

fn get_config() -> &'static Config {
    CONFIG.get_or_init(|| Config::load_from_env())
}
// First call: loads config; subsequent calls: returns cached reference
// Thread-safe: only one thread initializes, others wait

// LazyLock β€” like OnceLock but initialized lazily with a closure (stable 1.80+)
static DB: LazyLock = LazyLock::new(|| Database::connect());
// Initialized on first access β€” cleaner than OnceLock for simple cases

// Mutable singleton β€” OnceLock>:
static GLOBAL_STATE: OnceLock> = OnceLock::new();
fn global_state() -> &'static Mutex {
    GLOBAL_STATE.get_or_init(|| Mutex::new(AppState::new()))
}

// once_cell crate (before these were stable, and for more features):
use once_cell::sync::Lazy;
static REGEX: Lazy = Lazy::new(|| Regex::new(r"\d+").unwrap());

// Compile-time singletons with const β€” even better when possible:
const MAX_SIZE: usize = 1024;
static GREETING: &str = "Hello, World!";  // 'static &str, no lazy init needed
6How do you implement lock-free data structures in Rust?
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering::*};
use crossbeam::epoch;  // hazard pointer / epoch-based reclamation

// Lock-free stack using atomic CAS (conceptual):
struct Stack {
    head: AtomicPtr>,
}
struct Node { data: T, next: *mut Node }

impl Stack {
    fn push(&self, data: T) {
        let node = Box::into_raw(Box::new(Node { data, next: std::ptr::null_mut() }));
        loop {
            let head = self.head.load(Acquire);
            unsafe { (*node).next = head; }
            // CAS: if head unchanged, set it to our new node
            if self.head.compare_exchange(head, node, Release, Relaxed).is_ok() {
                break;
            }
            // Lost the race β€” retry
        }
    }
}

// In practice: use crossbeam for lock-free collections:
use crossbeam::queue::{ArrayQueue, SegQueue};
let q: ArrayQueue = ArrayQueue::new(100);  // bounded, lock-free
let q: SegQueue = SegQueue::new();          // unbounded, lock-free
q.push(42);
q.pop();  // returns Option

// crossbeam SkipMap β€” lock-free sorted map:
use crossbeam_skiplist::SkipMap;
let map = SkipMap::new();
map.insert("key", "value");
map.get("key");

// Memory reclamation in lock-free structures:
// The hard part isn't the CAS β€” it's safely freeing removed nodes
// crossbeam's epoch-based GC handles this:
let guard = epoch::pin();
// ... access lock-free data structure ...
// Epoch advances when all threads have had a chance to see the deletion

Senior tip: Lock-free structures are notoriously difficult to implement correctly. The bugs are subtle and timing-dependent. Unless you're implementing a library, use crossbeam's proven implementations. Only implement your own when you've profiled and confirmed that contention on a Mutex is the actual bottleneck.

7What is the actor model in Rust? How is it implemented with channels and how does it compare to Actix?
// Manual actor with channels β€” simple, transparent
enum OrderMessage { Create(Order), Cancel(u64), QueryStatus(u64, Sender) }

struct OrderActor { orders: HashMap, rx: Receiver }

impl OrderActor {
    fn run(mut self) {
        while let Ok(msg) = self.rx.recv() {
            match msg {
                OrderMessage::Create(o) => { self.orders.insert(o.id, o); }
                OrderMessage::Cancel(id) => { self.orders.remove(&id); }
                OrderMessage::QueryStatus(id, reply) => {
                    let status = self.orders.get(&id).map(|o| o.status.clone());
                    let _ = reply.send(status.unwrap_or(Status::NotFound));
                }
            }
        }
    }
}

// Spawn the actor:
let (tx, rx) = flume::bounded(100);
std::thread::spawn(move || OrderActor { orders: HashMap::new(), rx }.run());

// Send messages to the actor:
tx.send(OrderMessage::Create(order)).unwrap();

// Actix β€” full actor framework:
use actix::prelude::*;
struct MyActor { count: usize }
impl Actor for MyActor { type Context = Context; }

#[derive(Message)] #[rtype(result = "usize")]
struct Increment;

impl Handler for MyActor {
    type Result = usize;
    fn handle(&mut self, _msg: Increment, _ctx: &mut Context) -> usize {
        self.count += 1; self.count
    }
}

let addr = MyActor { count: 0 }.start();
let result = addr.send(Increment).await.unwrap();  // async message passing

When to use actors: State that logically belongs to one thread (avoiding shared-state complexity). Services that process commands sequentially. Systems with many independent workers. Actix adds supervision, lifecycle management, and async-native message passing on top of the basic channel pattern.

8What are parking_lot's advantages over std synchronization primitives?
// parking_lot crate β€” faster, smaller, more ergonomic sync primitives
use parking_lot::{Mutex, RwLock, Condvar, Once, RwLockReadGuard};

// Advantages over std:
// 1. No poisoning β€” no need to handle Err from lock()
let m = Mutex::new(0);
let mut guard = m.lock();  // returns MutexGuard directly, no unwrap!
*guard += 1;

// 2. Smaller: Mutex is 1 word (std Mutex is 40+ bytes on Linux due to pthread_mutex)
assert_eq!(std::mem::size_of::>(), 4);  // 4 bytes!
assert!(std::mem::size_of::>() > 30);

// 3. RwLock allows upgradable read locks β€” convert read β†’ write without releasing:
let rw = RwLock::new(HashMap::new());
let read_guard = rw.read();
// ... check if key exists ...
drop(read_guard);
// No race condition possible β€” upgrade atomically:
let mut write_guard = rw.write();
write_guard.insert("key", "value");

// 4. Const-init β€” can use in static context without lazy init:
static MUTEX: parking_lot::Mutex> = parking_lot::const_mutex(vec![]);
// std::sync::Mutex can't be const-initialized with a non-trivial value

// 5. Better contention handling β€” WFQ (wait-free queue) based
// Generally 1.5-5x faster than std under high contention

// 6. Additional features:
rw.try_read();              // non-blocking try
rw.try_write_for(timeout);  // timeout-based
m.lock_arc()                // Arc-based lock for longer lifetimes

Most performance-sensitive Rust applications use parking_lot over std sync primitives. The ergonomics (no unwrap on lock) alone make it worth the dependency for many codebases.

Async & Tokio

10 questions
1How does async/await work in Rust? What is a Future and how does the executor drive it?

Rust's async/await is built on a zero-cost, poll-based model. Unlike goroutines or green threads, Rust doesn't have a built-in runtime β€” you choose the executor (Tokio, async-std, smol).

The Future trait:

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll;
}

pub enum Poll {
    Ready(T),    // computation complete, here's the result
    Pending,     // not ready yet, will notify when ready via cx.waker()
}

How async fn compiles: An async fn desugars into a state machine (a struct implementing Future). Each .await point is a potential suspension point β€” the state machine captures all local variables and resumes from the right point when polled again.

// This async fn:
async fn fetch_and_process(url: &str) -> String {
    let response = http_get(url).await;      // may suspend here
    let text = response.text().await;         // may suspend here
    process(text)
}

// Compiles to roughly:
enum FetchAndProcessState {
    Start { url: String },
    WaitingForResponse { pending_resp: HttpFuture },
    WaitingForText { pending_text: TextFuture },
    Done,
}
impl Future for FetchAndProcessState {
    type Output = String;
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll {
        loop { match *self {
            Start => { /* kick off http_get */ }
            WaitingForResponse { ref mut f } => {
                match Pin::new(f).poll(cx) {
                    Poll::Ready(r) => { *self = WaitingForText { ... }; }
                    Poll::Pending => return Poll::Pending,  // suspend!
                }
            }
            // ...
        }}
    }
}

Executor role: Calls Future::poll(). When a future returns Pending, it registers a Waker. When I/O completes, the reactor calls the waker, which schedules the future to be polled again. Tokio's executor uses work-stealing across OS threads.

2What is Tokio and how do you structure a Tokio application? Explain the runtime, tasks, and I/O.
use tokio::{task, time, net, io, fs, sync};

// Entry point β€” creates the Tokio runtime
#[tokio::main]
async fn main() {
    // async code here
}

// Explicit runtime β€” for libraries or when you need control:
let rt = tokio::runtime::Builder::new_multi_thread()
    .worker_threads(4)
    .enable_all()                  // enable IO, timer, signals
    .build()
    .unwrap();
rt.block_on(async { /* main async work */ });

// Spawning tasks (concurrent, independent units of work):
let handle = tokio::spawn(async {
    // runs concurrently on Tokio's thread pool
    expensive_async_work().await
});
let result = handle.await.unwrap();  // JoinHandle

// Tasks are Send β€” if your future captures non-Send data, use spawn_local

// Tokio async I/O:
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
    let (mut socket, addr) = listener.accept().await?;
    tokio::spawn(async move {   // each connection in its own task
        let mut buf = vec![0; 1024];
        loop {
            match socket.read(&mut buf).await {
                Ok(0) => return,              // connection closed
                Ok(n) => { socket.write_all(&buf[..n]).await.unwrap(); }
                Err(_) => return,
            }
        }
    });
}

tokio::spawn vs tokio::task::spawn_blocking:

// CPU-bound work β€” run in a blocking thread pool, don't block the async executor
let result = tokio::task::spawn_blocking(|| {
    compute_fibonacci(1_000_000)   // blocking CPU work
}).await.unwrap();

// Never call blocking operations directly in async context:
// std::fs::read_to_string("file.txt")  // BAD β€” blocks executor thread
// Use tokio::fs instead:
tokio::fs::read_to_string("file.txt").await?;  // GOOD
3What is the difference between tokio::spawn, tokio::join!, tokio::select!, and FuturesUnordered?
use tokio::{join, select, time};
use futures::stream::FuturesUnordered;
use futures::StreamExt;

// join! β€” run futures concurrently, wait for ALL to complete
let (res1, res2, res3) = join!(
    fetch("https://api1.example.com"),
    fetch("https://api2.example.com"),
    fetch("https://api3.example.com"),
);  // all three run concurrently; returns when all done

// try_join! β€” like join! but returns Err immediately if any fails:
let (a, b) = tokio::try_join!(
    fallible_op_1(),
    fallible_op_2(),
)?;

// select! β€” wait for the FIRST future to complete, cancel others
select! {
    result = fetch("url") => println!("got result: {:?}", result),
    _ = time::sleep(Duration::from_secs(5)) => println!("timeout!"),
}

// select! with cancel-safety warning: some futures are not cancel-safe
// tokio::io::AsyncReadExt::read IS NOT cancel-safe β€” partial reads lost on cancel
// Use tokio::io::ReadBuf or ensure you handle partial reads

// tokio::spawn β€” spawns independent concurrent task (different from join/select)
// spawn gives you a JoinHandle, task runs independently even if you drop the handle
let h1 = tokio::spawn(fetch("url1"));
let h2 = tokio::spawn(fetch("url2"));
let (r1, r2) = join!(h1, h2)?;  // join the task handles

// FuturesUnordered β€” drive many futures, yield results as they complete
let mut futs: FuturesUnordered<_> = urls.iter()
    .map(|url| fetch(url))
    .collect();

while let Some(result) = futs.next().await {
    // process each result as it arrives β€” fastest first, not order of insertion
    process(result?);
}

When to use each: join! for a fixed small number of concurrent operations where you need all results. select! for racing operations or adding timeouts/cancellation. FuturesUnordered for a dynamic set of futures. spawn for truly independent background work.

4What are async channels in Tokio? Compare mpsc, broadcast, watch, and oneshot.
use tokio::sync::{mpsc, broadcast, watch, oneshot, Mutex, RwLock, Semaphore};

// mpsc β€” multiple producer, single consumer (async version of std mpsc)
let (tx, mut rx) = mpsc::channel::(100);  // bounded
let (tx, mut rx) = mpsc::unbounded_channel::();
tx.send("hello".into()).await?;
while let Some(msg) = rx.recv().await { process(msg); }

// broadcast β€” one sender, many receivers; each receiver gets every message
let (tx, _) = broadcast::channel::(16);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tx.send("hello".into())?;  // both rx1 and rx2 get "hello"
// Receivers that fall behind lose messages (circular buffer)

// watch β€” single value that can be updated, observers get latest
let (tx, rx) = watch::channel("initial");
tx.send("updated")?;              // replace the value
let current = *rx.borrow();       // read current value
let mut rx2 = rx.clone();         // multiple readers
rx2.changed().await?;             // wait until value changes
println!("{}", *rx2.borrow());    // get new value
// Perfect for: config updates, state broadcasts, feature flags

// oneshot β€” single message, from one sender to one receiver
let (tx, rx) = oneshot::channel::>();
tokio::spawn(async move { tx.send(do_work().await).ok(); });
let result = rx.await?;  // wait for the single message
// Perfect for: request-response, task completion notification

Selection guide: mpsc for task work queues; broadcast for event publication to multiple subscribers; watch for shared state notifications (latest value matters, not all values); oneshot for getting a single result back from a spawned task.

5How do you handle cancellation and timeouts in async Rust?
use tokio::time::{timeout, sleep, Duration};

// timeout β€” wraps a future, returns Err if it doesn't complete in time
match timeout(Duration::from_secs(5), fetch_data()).await {
    Ok(Ok(data)) => process(data),
    Ok(Err(e))   => eprintln!("fetch error: {}", e),
    Err(_)       => eprintln!("timed out after 5 seconds"),
}

// CancellationToken β€” structured cancellation propagation
use tokio_util::sync::CancellationToken;
let token = CancellationToken::new();
let child_token = token.child_token();  // inherits parent cancellation

// In a task:
tokio::spawn(async move {
    select! {
        _ = child_token.cancelled() => {
            println!("task cancelled, cleaning up...");
            cleanup().await;
        }
        result = do_work() => { process(result); }
    }
});

// Cancel everything when main shuts down:
token.cancel();  // signals all child tokens too

// Graceful shutdown pattern:
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
// In tasks: select! { _ = shutdown_rx.recv() => return, result = work() => ... }
// On signal: shutdown_tx.send(()).unwrap();

// AbortHandle β€” abort a spawned task:
let handle = tokio::spawn(long_running_task());
let abort_handle = handle.abort_handle();
// Later:
abort_handle.abort();
match handle.await {
    Ok(result) => {},
    Err(e) if e.is_cancelled() => println!("task was cancelled"),
    Err(e) => println!("task panicked: {}", e),
}

Cancel safety: Not all futures are cancel-safe. A future is cancel-safe if dropping it in a select! branch doesn't lose data. tokio::sync::mpsc::Receiver::recv() is cancel-safe. AsyncReadExt::read_exact() is NOT β€” if cancelled mid-read, bytes are lost. Document cancel-safety for your futures.

6What is async Rust's relationship with blocking code? How do you bridge sync and async?
// PROBLEM: Blocking in async context starves other tasks
async fn bad() {
    std::thread::sleep(Duration::from_secs(1));  // BLOCKS THE EXECUTOR!
    let data = std::fs::read_to_string("file.txt").unwrap();  // BLOCKS!
}

// SOLUTION 1: Use async equivalents:
async fn good() {
    tokio::time::sleep(Duration::from_secs(1)).await;  // yields to executor
    let data = tokio::fs::read_to_string("file.txt").await.unwrap();
}

// SOLUTION 2: spawn_blocking for unavoidable blocking code (DB, CPU work):
async fn with_blocking() -> Result> {
    // Runs in dedicated blocking thread pool β€” doesn't block async threads
    let result = tokio::task::spawn_blocking(|| {
        let conn = sqlite::open("db.sqlite")?;  // synchronous DB
        conn.query("SELECT ...")
    }).await??;
    Ok(result)
}

// SOLUTION 3: block_in_place β€” stay on current thread but allow other tasks to steal
// Use when you need TLS or can't move to another thread:
async fn with_block_in_place() {
    tokio::task::block_in_place(|| {
        // blocking work here β€” executor can steal this thread's tasks to other threads
        compute_intensive_work()
    });
}

// Calling async from sync code:
fn sync_caller() {
    let rt = tokio::runtime::Handle::current();
    // If inside a Tokio runtime:
    rt.block_on(async { fetch_data().await });
    // Or from outside a runtime:
    tokio::runtime::Runtime::new().unwrap().block_on(async { ... });
}
7How do you implement an async HTTP server in Rust with Axum? Walk through routing, extractors, and middleware.
use axum::{
    Router, routing::{get, post},
    extract::{Path, Query, State, Json, Extension},
    response::{Json as JsonResponse, IntoResponse},
    http::StatusCode,
    middleware::{self, Next},
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

// Shared application state
#[derive(Clone)]
struct AppState { db: Arc }

// Request/response types
#[derive(Deserialize)]
struct CreateUserRequest { name: String, email: String }
#[derive(Serialize)]
struct UserResponse { id: u64, name: String }

// Handler β€” async function returning impl IntoResponse
async fn create_user(
    State(state): State,      // shared state extractor
    Json(req): Json, // JSON body extractor (auto-validates)
) -> Result<(StatusCode, JsonResponse), AppError> {
    let user = state.db.create_user(req.name, req.email).await?;
    Ok((StatusCode::CREATED, JsonResponse(UserResponse { id: user.id, name: user.name })))
}

async fn get_user(
    State(state): State,
    Path(id): Path,                // path parameter extractor
) -> Result, AppError> {
    let user = state.db.get_user(id).await?.ok_or(AppError::NotFound)?;
    Ok(JsonResponse(UserResponse { id: user.id, name: user.name }))
}

// Middleware
async fn auth_middleware(
    req: axum::http::Request,
    next: Next,
) -> impl IntoResponse {
    let token = req.headers().get("Authorization");
    if validate_token(token).is_err() {
        return StatusCode::UNAUTHORIZED.into_response();
    }
    next.run(req).await
}

// Router assembly
let app = Router::new()
    .route("/users", post(create_user))
    .route("/users/:id", get(get_user))
    .layer(middleware::from_fn(auth_middleware))
    .layer(tower_http::trace::TraceLayer::new_for_http())
    .with_state(AppState { db: Arc::new(db) });

tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
8What are async traits in Rust? How has the situation changed from the async-trait crate to native support?

Async methods in traits were historically impossible to express in stable Rust because each async fn returns an opaque, unique impl Future type β€” traits can't express "each implementation returns a different type" without associated types or boxing.

// Before Rust 1.75: async-trait crate (proc macro, adds Box allocation)
use async_trait::async_trait;

#[async_trait]
trait Fetcher {
    async fn fetch(&self, url: &str) -> Result;
}
// Desugars to: fn fetch(&self, url: &str) -> Box + Send + '_>

// Rust 1.75+ (stable): Native async functions in traits!
trait Fetcher {
    async fn fetch(&self, url: &str) -> Result;
    // Each impl returns its own anonymous Future type β€” no boxing!
}

impl Fetcher for HttpClient {
    async fn fetch(&self, url: &str) -> Result {
        self.client.get(url).send().await?.text().await.map_err(Into::into)
    }
}

// But: dyn Fetcher still requires boxing (RPITIT β€” return position impl Trait in traits)
// For dyn dispatch with async traits, still need boxing:
fn make_fetcher() -> Box { Box::new(HttpClient::new()) }
// This won't work until "dyn-async-traits" or similar is stabilized

// Workaround for dyn: explicit boxing via associated type
trait DynFetcher {
    fn fetch<'a>(&'a self, url: &'a str) -> Pin> + Send + 'a>>;
}

// Or use trait_variant crate (from Rust team, ergonomic wrapper):
#[trait_variant::make(FetcherSend: Send)]
trait Fetcher {
    async fn fetch(&self, url: &str) -> Result;
}
// Generates a Send-bound version for use with dyn
9How do you implement streaming responses and async iterators in Rust?
use futures::{Stream, StreamExt, SinkExt};
use tokio_stream::wrappers::ReceiverStream;
use async_stream::stream;

// The Stream trait β€” async iterator
pub trait Stream {
    type Item;
    fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll>;
}

// Creating streams with async-stream crate (ergonomic macro):
fn fibonacci() -> impl Stream {
    stream! {
        let (mut a, mut b) = (0u64, 1u64);
        loop {
            yield a;                    // yield each value
            (a, b) = (b, a + b);
        }
    }
}

// Consuming a stream:
let mut fib = fibonacci();
while let Some(n) = fib.next().await {
    println!("{}", n);
    if n > 1000 { break; }
}

// Stream from channel (ReceiverStream):
let (tx, rx) = tokio::sync::mpsc::channel(32);
let stream = ReceiverStream::new(rx);

// Stream adaptors (just like Iterator):
fibonacci()
    .take(10)
    .filter(|&n| n % 2 == 0)
    .map(|n| n * 2)
    .for_each(|n| async move { println!("{}", n) })
    .await;

// Axum SSE streaming response:
use axum::response::sse::{Event, Sse};
async fn sse_handler() -> Sse>> {
    let stream = stream! {
        for i in 0..10 {
            tokio::time::sleep(Duration::from_secs(1)).await;
            yield Ok(Event::default().data(format!("tick {}", i)));
        }
    };
    Sse::new(stream)
}
10What are common async Rust pitfalls and anti-patterns?
  • Holding a Mutex across an .await:
// BAD β€” std::Mutex held across .await β€” may deadlock
async fn bad(m: &Mutex) {
    let guard = m.lock().unwrap();
    do_async_work().await;   // another task may try to lock m β€” deadlock!
    drop(guard);
}
// GOOD β€” use tokio::sync::Mutex (async-aware), or drop before await:
async fn good(m: &tokio::sync::Mutex) {
    let guard = m.lock().await;
    do_sync_work(&guard);
    drop(guard);             // drop before async work
    do_async_work().await;
}
  • Spawning tasks that outlive their data: Use Arc to share data across spawn boundaries.
  • Not handling JoinHandle errors: A spawned task that panics returns Err(JoinError) β€” if you drop the handle, the panic is silently lost.
  • Accidental sequential await:
// BAD β€” sequential (takes 2 seconds)
let a = fetch_one().await;
let b = fetch_two().await;

// GOOD β€” concurrent (takes max(t1, t2) seconds)
let (a, b) = tokio::join!(fetch_one(), fetch_two());
  • Infinite select! loops without backpressure β€” can starve some branches if others are always ready.
  • Forgetting .await β€” let f = some_async_fn() without .await creates a Future but never runs it. The compiler warns about unused futures.
  • Large futures from many locals across await points β€” each local variable in scope at an .await is stored in the state machine, making it large. Extract into smaller functions or use Box::pin for recursion.

Error Handling

6 questions
1How does Rust's Result and ? operator work? What is the From trait's role?
use std::num::ParseIntError;

// Result β€” explicit error handling
fn parse_int(s: &str) -> Result {
    s.trim().parse::()
}

// The ? operator β€” desugars to match + early return:
fn process(s: &str) -> Result {
    let n = s.parse::()?;   // if Err, return Err immediately
    // equivalent to:
    // let n = match s.parse::() {
    //     Ok(v) => v,
    //     Err(e) => return Err(e.into()),  // .into() calls From conversion
    // };
    Ok(n * 2)
}

// ? with different error types β€” From handles conversion:
#[derive(Debug)]
enum AppError {
    Parse(ParseIntError),
    Io(std::io::Error),
}
impl From for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
impl From for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

fn load_and_parse(path: &str) -> Result {
    let content = std::fs::read_to_string(path)?;  // ? converts io::Error β†’ AppError via From
    let n = content.trim().parse::()?;          // ? converts ParseIntError β†’ AppError via From
    Ok(n)
}

The ? operator calls .into() on the error before returning, which uses the From trait to convert the error type. This lets you use ? with multiple error types as long as you have From implementations.

2What are the best libraries for error handling in Rust? Compare thiserror, anyhow, and eyre.
// thiserror β€” for library code with typed errors
// Generates Display, From, and Error trait impls via derive macros
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("user {id} not found")]
    NotFound { id: u64 },

    #[error("authentication failed: {0}")]
    Auth(String),

    #[error("database error")]
    Database(#[from] sqlx::Error),  // #[from] generates From impl

    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
}

// Callers can match on specific variants:
match api_call() {
    Err(ApiError::NotFound { id }) => { /* 404 */ }
    Err(ApiError::Auth(_)) => { /* 401 */ }
    Err(e) => { /* 500 */ }
    Ok(v) => { /* use v */ }
}

// anyhow β€” for application code / binaries (no pattern matching needed)
// Erases error type β€” just a boxed error with context
use anyhow::{Context, Result, bail, ensure};

fn load_config(path: &str) -> Result {
    let data = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path))?;
    let config: Config = serde_json::from_str(&data)
        .context("config file is not valid JSON")?;
    Ok(config)
}

// bail! β€” create and return an error
fn validate(n: i32) -> Result<()> {
    ensure!(n > 0, "n must be positive, got {}", n);  // ensure! like assert! but returns Err
    if n > 1000 { bail!("n too large: {}", n); }
    Ok(())
}

// eyre β€” like anyhow but with better error reporting (color-eyre for pretty prints)
use color_eyre::eyre::{Result, WrapErr};
fn main() -> Result<()> {
    color_eyre::install()?;  // pretty error handler
    load_config("config.json").wrap_err("Failed to load configuration")?;
    Ok(())
}

Rule: Use thiserror in libraries (typed errors, callers can match). Use anyhow/eyre in applications (ergonomic, human-readable errors with context chains). Never use Box<dyn Error> directly β€” prefer these crates.

3How do you design custom error types for a Rust library? What should error types implement?
use std::{fmt, error::Error};
use thiserror::Error;

// Minimal requirements for a good library error type:
// 1. Implement std::error::Error (requires Debug + Display)
// 2. Each variant communicates the kind of failure
// 3. Non-exhaustive for forward compatibility
// 4. Source errors accessible via Error::source()

#[derive(Error, Debug)]
#[non_exhaustive]  // callers can't exhaustively match β€” allows adding variants
pub enum DbError {
    #[error("connection failed: {0}")]
    Connection(String),

    #[error("query failed: {query}")]
    Query {
        query: String,
        #[source]   // Error::source() returns this
        cause: Box,
    },

    #[error("record not found: id={id}")]
    NotFound { id: u64 },

    #[error("serialization error")]
    Serialization(#[from] serde_json::Error),  // #[from] + #[source] combined
}

// Custom result alias β€” avoids repeating the error type:
pub type Result = std::result::Result;

// Providing context (anyhow-style chaining):
impl DbError {
    pub fn with_context(self, msg: impl Into) -> Self {
        // wrap with additional context...
        self
    }
}

// Error categories β€” useful for deciding HTTP status codes:
impl DbError {
    pub fn is_not_found(&self) -> bool {
        matches!(self, DbError::NotFound { .. })
    }
    pub fn is_transient(&self) -> bool {
        matches!(self, DbError::Connection(_))  // safe to retry
    }
}
4When should you use panic! vs Result vs Option in Rust?

Option<T>: The value may or may not exist β€” absence is normal, not an error. Examples: map lookup, first element of empty list, optional config field. Use when the caller should decide what "no value" means.

Result<T, E>: The operation may fail, and the failure has information. The caller should handle the error. Examples: file I/O, network requests, parsing, database queries. Use for anything that can fail due to external conditions.

panic!: For programmer errors β€” precondition violations, invariants that "should never" be broken, unreachable code branches. Examples: index out of bounds, assertion failure, logic bugs.

// Option β€” no value is normal
fn find_user(id: u64) -> Option { map.get(&id).cloned() }
let user = find_user(42).unwrap_or_default();

// Result β€” failure has context
fn parse_config(s: &str) -> Result { ... }
let cfg = parse_config(input)?;

// panic β€” programmer error (should never happen in correct code)
fn get_index(v: &[i32], i: usize) -> i32 {
    assert!(i < v.len(), "index {} out of bounds for len {}", i, v.len());
    v[i]
}

// Prefer expect() over unwrap() β€” adds context to panic message:
let conn = db.connect().expect("DB connection should succeed at startup");

// When to convert Option β†’ Result:
let user = find_user(id).ok_or(ApiError::NotFound { id })?;

// When to convert Result β†’ Option (if you only care about success):
let n: Option = "42".parse::().ok();

Library vs application: Libraries should almost never panic (except for violated invariants). Applications can panic on unrecoverable startup failures. Always document which functions can panic with # Panics in docs.

5How do you propagate and add context to errors in Rust? What is error chaining?
use anyhow::{Context, Result};

// Adding context to errors as they propagate:
fn load_user(id: u64) -> Result {
    let path = format!("/data/users/{}.json", id);
    let data = std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read user file: {}", path))?;
    let user: User = serde_json::from_str(&data)
        .with_context(|| format!("invalid JSON in user file for id={}", id))?;
    Ok(user)
}

// The resulting error chain (with anyhow):
// Error: failed to process request for user 42
//   Caused by: failed to read user file: /data/users/42.json
//   Caused by: No such file or directory (os error 2)

// Printing the full chain:
if let Err(e) = load_user(42) {
    eprintln!("Error: {}", e);
    eprintln!("Caused by: {:#}", e);  // {:#} prints chain
    // Or iterate the chain:
    let mut source = e.source();
    while let Some(s) = source {
        eprintln!("  Caused by: {}", s);
        source = s.source();
    }
}

// With thiserror β€” access source:
#[derive(Error, Debug)]
pub enum MyError {
    #[error("parsing failed")]
    Parse(#[source] ParseError),  // source = cause
}

let err = MyError::Parse(parse_err);
println!("{}", err);              // "parsing failed"
println!("{:?}", err.source());  // Some(ParseError{...})
6How do you test error conditions in Rust? What patterns work well?
#[cfg(test)]
mod tests {
    use super::*;

    // Test that Ok is returned:
    #[test]
    fn test_valid_input() {
        let result = parse_int("42");
        assert_eq!(result.unwrap(), 42);
        // Or with assert_matches:
        assert!(matches!(result, Ok(42)));
    }

    // Test specific error variant:
    #[test]
    fn test_invalid_input_returns_parse_error() {
        let result = parse_int("not_a_number");
        assert!(result.is_err());
        // With thiserror β€” match on variant:
        match result.unwrap_err() {
            AppError::ParseError(_) => {}   // expected
            e => panic!("unexpected error: {:?}", e),
        }
    }

    // Test panic with should_panic:
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn test_out_of_bounds_panics() {
        let v = vec![1, 2, 3];
        let _ = v[10];
    }

    // assert_matches! (stable 1.82+) for pattern matching in assertions:
    #[test]
    fn test_error_variant() {
        let result: Result<(), AppError> = try_operation();
        assert_matches!(result, Err(AppError::NotFound { id: 42 }));
    }

    // proptest / quickcheck for property-based error testing:
    use proptest::prelude::*;
    proptest! {
        #[test]
        fn parse_never_panics(s in ".*") {
            // Must not panic for any string, even if it returns Err
            let _ = parse_int(&s);
        }
    }
}

Unsafe Rust

6 questions
1What is unsafe in Rust? What are the five unsafe superpowers and what invariants must you uphold?

unsafe blocks opt out of Rust's safety guarantees for code that the programmer can verify correct but the borrow checker cannot. The keyword doesn't disable the borrow checker β€” it enables five additional capabilities that could cause undefined behavior if misused.

The five unsafe superpowers:

  1. Dereference raw pointers (*const T, *mut T)
  2. Call unsafe functions or methods (including C FFI)
  3. Access or modify mutable static variables
  4. Implement unsafe traits (e.g., Send, Sync)
  5. Access fields of unions
unsafe fn dangerous() { /* may violate invariants */ }

fn safe_wrapper() {
    unsafe {
        // Programmer guarantees: the pointer is valid, aligned, points to init'd T,
        // no aliasing violations, no concurrent mutation
        let raw_ptr: *mut i32 = &mut 42 as *mut i32;
        *raw_ptr = 100;

        dangerous();           // call unsafe function

        static mut COUNTER: u32 = 0;
        COUNTER += 1;         // access mutable static
    }
}

// Unsafe trait β€” implementing means you're upholding safety invariants:
unsafe trait SafeToSendAcrossThreads {}
unsafe impl SafeToSendAcrossThreads for MyType {}  // promise it's safe

What you must uphold:

  • Raw pointers must be non-null, aligned, and pointing to valid initialized memory of the right type
  • No data races (even through raw pointers)
  • No undefined behavior from invalid bit patterns
  • FFI types match the C ABI exactly
  • No dangling references β€” referenced data must live long enough

Unsafe β‰  no guarantees: Even in an unsafe block, the borrow checker still enforces all the rules it can. You only get the five superpowers β€” you can't violate type safety or memory rules that the checker CAN enforce.

2How do you write safe abstractions over unsafe code? What is the "unsafe sandwich" pattern?

The goal of unsafe code is to implement safe abstractions. The "unsafe sandwich": unsafe code in the middle, safe public API on the outside β€” callers get safety, implementors reason about invariants.

// Safe abstraction over raw pointer manipulation:
pub struct MyVec {
    ptr: std::ptr::NonNull,
    len: usize,
    cap: usize,
}

impl MyVec {
    pub fn new() -> Self {
        MyVec {
            ptr: NonNull::dangling(),  // valid non-null pointer to empty allocation
            len: 0,
            cap: 0,
        }
    }

    // Public safe API β€” push validates all invariants before unsafe code
    pub fn push(&mut self, item: T) {
        if self.len == self.cap { self.grow(); }  // ensure capacity
        // SAFETY: self.len < self.cap after grow(), ptr is valid for cap elements,
        //         we own this memory exclusively (no aliasing)
        unsafe {
            std::ptr::write(self.ptr.as_ptr().add(self.len), item);
        }
        self.len += 1;
    }

    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len { return None; }
        // SAFETY: index < len, ptr is valid, T is initialized at this offset
        Some(unsafe { &*self.ptr.as_ptr().add(index) })
    }

    // Private unsafe helper β€” only called from safe methods that verify preconditions
    unsafe fn grow(&mut self) {
        let new_cap = if self.cap == 0 { 1 } else { self.cap * 2 };
        let layout = Layout::array::(new_cap).unwrap();
        let new_ptr = std::alloc::alloc(layout) as *mut T;
        // copy existing elements, update ptr...
        self.cap = new_cap;
    }
}

// Document SAFETY reasoning in comments above every unsafe block:
// SAFETY: 

Key principle: Minimize the surface area of unsafe. The safe public API enforces all preconditions before the unsafe block executes. Every unsafe block should have a // SAFETY: comment explaining why the preconditions are met.

3How does Rust's FFI work? How do you call C code from Rust and expose Rust to C?
// Calling C from Rust:
extern "C" {
    fn abs(n: i32) -> i32;
    fn strlen(s: *const libc::c_char) -> libc::size_t;
    fn malloc(size: libc::size_t) -> *mut libc::c_void;
    fn free(ptr: *mut libc::c_void);
}

fn main() {
    let result = unsafe { abs(-5) };  // calling C is always unsafe

    // Passing Rust strings to C (null-termination required!):
    use std::ffi::CString;
    let c_str = CString::new("hello").unwrap();  // adds null terminator
    let len = unsafe { strlen(c_str.as_ptr()) };

    // Receiving C strings:
    use std::ffi::CStr;
    let c_ptr: *const libc::c_char = get_c_string();
    let rust_str = unsafe { CStr::from_ptr(c_ptr).to_str().unwrap() };
}

// Exposing Rust to C:
#[no_mangle]  // prevent name mangling
pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }

// C structs must be repr(C):
#[repr(C)]
pub struct Point { x: f64, y: f64 }

#[no_mangle]
pub extern "C" fn new_point(x: f64, y: f64) -> *mut Point {
    Box::into_raw(Box::new(Point { x, y }))  // caller owns, must free
}
#[no_mangle]
pub unsafe extern "C" fn free_point(ptr: *mut Point) {
    if !ptr.is_null() { drop(Box::from_raw(ptr)); }
}

// bindgen β€” auto-generate Rust bindings from C headers:
// In build.rs:
// bindgen::Builder::default().header("lib.h").generate().unwrap()
//     .write_to_file(out_dir.join("bindings.rs")).unwrap();
4What is undefined behavior in Rust? What actions cause UB?

Undefined Behavior (UB) means the compiler is allowed to do anything β€” delete your code, compile to incorrect instructions, corrupt memory, emit nasal demons. UB is only possible in unsafe blocks.

Actions that cause UB in Rust:

  • Dereferencing a null, dangling, or misaligned raw pointer
  • Reading uninitialized memory (MaybeUninit exists for this reason)
  • Creating an invalid value for a type: &str with invalid UTF-8, bool with value other than 0/1, char outside Unicode scalar values, NonNull<T> holding null, enum with invalid discriminant
  • Data race: concurrent unsynchronized access with at least one write
  • Violating aliasing rules: having &mut T and any other reference to same T simultaneously
  • Stack overflow (technically implementation-defined)
  • Calling a function via a function pointer of the wrong type
// Tools to find UB:
// Miri β€” official Rust UB detector, runs in an interpreter
// cargo miri test

// AddressSanitizer:
// RUSTFLAGS="-Z sanitizer=address" cargo test

// MaybeUninit β€” safe way to handle uninitialized memory:
use std::mem::MaybeUninit;
let mut x: MaybeUninit = MaybeUninit::uninit();
unsafe { x.as_mut_ptr().write(42); }   // initialize
let value = unsafe { x.assume_init() }; // now safe to read
5How do you use std::mem functions safely? Explain transmute, swap, take, and replace.
use std::mem;

// mem::transmute β€” reinterpret bits as a different type (DANGEROUS)
// Requires same size, valid bit pattern for target type
unsafe {
    let float: f32 = 1.0;
    let bits: u32 = mem::transmute(float);  // f32 bits as u32
    // SAFE only when types have same size and any bit pattern is valid for target

    // SAFER alternative for float↔int:
    let bits = f32::to_bits(1.0f32);   // use dedicated methods
}

// mem::swap β€” swap two values without allocation
let mut a = String::from("hello");
let mut b = String::from("world");
mem::swap(&mut a, &mut b);
assert_eq!(a, "world");  // no clone needed!

// mem::take β€” take a value out of a mutable reference, leave Default in place
let mut name = String::from("Alice");
let taken = mem::take(&mut name);  // name is now ""
assert_eq!(taken, "Alice");
assert_eq!(name, "");  // String::default()

// mem::replace β€” replace a value, return the old one
let mut v = vec![1, 2, 3];
let old = mem::replace(&mut v, vec![4, 5, 6]);
assert_eq!(old, vec![1, 2, 3]);
assert_eq!(v, vec![4, 5, 6]);

// Common use of take/replace: move out of a field you don't own:
struct Handler { state: Option> }
impl Handler {
    fn transition(&mut self) {
        let current = self.state.take().unwrap();  // take out of Option
        self.state = Some(current.next_state());   // replace with new state
    }
}
6How do you use Miri to detect undefined behavior in Rust code?

Miri is an interpreter for Rust's Mid-level Intermediate Representation (MIR) that detects undefined behavior at runtime β€” memory accesses, use-after-free, data races, invalid values, aliasing violations.

# Install and run:
rustup component add miri
cargo miri test          # run all tests under Miri
cargo miri run           # run main under Miri

# What Miri catches:
# - Out-of-bounds memory access
# - Use-after-free
# - Use of uninitialized memory
# - Violated aliasing rules (Stacked Borrows model)
# - Invalid enum discriminant
# - Data races (with -Zmiri-race-detection)
# - Misaligned pointer access

# Stacked Borrows β€” Miri's aliasing model:
# Tracks a "stack" of borrows on each memory location
# Pushing a &mut T invalidates all previously created references
# Violations β†’ Miri error, even if code "works" by accident

// Example Miri would catch:
fn aliasing_violation() {
    let mut x = 5;
    let r1 = &x as *const i32;
    let r2 = &mut x;  // invalidates r1 in Stacked Borrows
    *r2 = 10;
    unsafe { let _ = *r1; }  // Miri: error β€” r1 was invalidated!
}

# Limitations:
# - Slow (10-100x slower than native execution)
# - Not all operations supported (syscalls, some intrinsics)
# - Stacked Borrows may be stricter than the actual memory model
# - Not a proof β€” a test not caught by Miri may still have UB

# Tree Borrows β€” newer, less conservative aliasing model in Miri:
# MIRIFLAGS="-Zmiri-tree-borrows" cargo miri test

Ecosystem & Tooling

6 questions
1What are Rust's most important crates? What does the senior Rust ecosystem look like?

Async runtime: tokio (dominant), async-std, smol

Web frameworks: axum (modern, tower-native), actix-web (high performance), warp, poem

HTTP client: reqwest (async, built on hyper), ureq (sync, no async)

Serialization: serde (universal β€” JSON, YAML, TOML, MessagePack, Bincode), serde_json, toml, rmp-serde

Database: sqlx (async, compile-time query checking), diesel (sync ORM), sea-orm (async ORM)

Error handling: thiserror (libraries), anyhow (applications), color-eyre

CLI: clap (argument parsing), indicatif (progress bars), dialoguer (interactive prompts)

Concurrency: rayon (data parallelism), crossbeam (channels, atomic, lock-free), parking_lot (faster sync primitives)

Testing: proptest (property-based), fake (fake data), mockall (mocking), insta (snapshot testing), criterion (benchmarking)

Tracing / Observability: tracing + tracing-subscriber (structured logging), opentelemetry-rust

Cryptography: ring, rustls (TLS), sha2, aes-gcm

Parsing: nom (parser combinators), pest (PEG grammars), winnow

Configuration: config, dotenvy, figment

Utilities: itertools, once_cell, derive_more, strum (enum utilities), bytes (zero-copy byte buffers)

2How do macros work in Rust? Compare declarative macros (macro_rules!) and procedural macros.
// Declarative macros (macro_rules!) β€” pattern matching on token trees
macro_rules! vec {
    // Pattern: vec![] β€” empty
    () => { Vec::new() };
    // Pattern: vec![expr, expr, ...]
    ($($x:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(v.push($x);)+  // expand for each $x
            v
        }
    };
}

// Custom macro example:
macro_rules! assert_approx_eq {
    ($left:expr, $right:expr, $eps:expr) => {
        let diff = ($left - $right).abs();
        assert!(diff < $eps, "Expected |{} - {}| < {}, got {}", $left, $right, $eps, diff);
    };
}

assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);

// Procedural macros β€” operate on TokenStream, much more powerful
// Three kinds:
// 1. Custom derive: #[derive(MyTrait)]
// 2. Attribute macros: #[my_attribute]
// 3. Function-like macros: my_macro!(...)

// In a proc-macro crate:
use proc_macro::TokenStream;
#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    let ast: syn::DeriveInput = syn::parse(input).unwrap();
    let name = &ast.ident;
    quote::quote! {
        impl HelloWorld for #name {
            fn hello() { println!("Hello from {}!", stringify!(#name)); }
        }
    }.into()
}
// syn: parse Rust code; quote: generate Rust code

// Usage:
#[derive(HelloWorld)]
struct MyStruct;
3How does Cargo work? Explain workspaces, build scripts, features, and conditional compilation.
# Cargo workspace β€” multiple crates sharing one Cargo.lock
# workspace Cargo.toml:
[workspace]
members = ["crates/server", "crates/db", "crates/shared"]
resolver = "2"   # use resolver v2 (required for correct feature resolution)

# Cargo features β€” conditional compilation
# In Cargo.toml:
[features]
default = ["json"]           # enabled by default
json = ["serde/derive", "serde_json"]
postgres = ["sqlx/postgres"]
full = ["json", "postgres"]

# In code:
#[cfg(feature = "json")]
pub mod json_support { ... }

// Conditional compilation attributes:
#[cfg(target_os = "linux")]
fn linux_specific() { ... }

#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn fast_path() { ... }

#[cfg(debug_assertions)]  // only in debug builds
fn debug_check() { ... }

// cfg! macro β€” runtime condition:
if cfg!(target_os = "windows") { /* windows code */ }

# Build scripts (build.rs) β€” run before compilation:
// build.rs
fn main() {
    // Compile C code:
    cc::Build::new().file("src/native.c").compile("native");
    // Generate bindings:
    bindgen::Builder::default().header("include/lib.h")
        .generate().unwrap().write_to_file("src/bindings.rs").unwrap();
    // Set environment variables for code:
    println!("cargo:rustc-env=BUILD_DATE={}", chrono::Utc::now().date_naive());
    // Rerun only when these files change:
    println!("cargo:rerun-if-changed=src/native.c");
}
4How do you write and run tests in Rust? Explain unit tests, integration tests, and doc tests.
// Unit tests β€” in same file as code, can test private functions
#[cfg(test)]
mod tests {
    use super::*;      // access private items in the parent module

    #[test]
    fn test_add() { assert_eq!(add(2, 3), 5); }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow_panics() { add(i32::MAX, 1); }

    #[test]
    fn test_result() -> Result<(), Box> {
        let n: i32 = "42".parse()?;  // ? works in tests returning Result
        assert_eq!(n, 42);
        Ok(())
    }

    // Test helpers
    fn make_test_user() -> User { User { name: "Alice".into(), age: 30 } }
}

// Integration tests β€” in tests/ directory, test public API only
// tests/api_test.rs:
use mylib::PublicApi;
#[test]
fn test_public_api() { assert_eq!(PublicApi::compute(5), 25); }

// Doc tests β€” code examples in documentation that actually compile and run:
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = mylib::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

// Run specific tests:
// cargo test                       β€” all tests
// cargo test test_add              β€” tests matching "test_add"
// cargo test --test integration    β€” only integration tests
// cargo test -- --nocapture        β€” show println! output
// cargo test -- --ignored          β€” run #[ignore]d tests
// cargo nextest run                β€” faster test runner (parallel)
5What is Rust's WASM support? How do you compile Rust to WebAssembly?
# Compile to WASM:
rustup target add wasm32-unknown-unknown  # browser WASM
rustup target add wasm32-wasi            # WASI (non-browser)
cargo build --target wasm32-unknown-unknown --release

# wasm-bindgen β€” bridge between Rust and JavaScript:
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: u32, b: u32) -> u32 { a + b }

#[wasm_bindgen]
pub struct Point { x: f64, y: f64 }

#[wasm_bindgen]
impl Point {
    #[wasm_bindgen(constructor)]
    pub fn new(x: f64, y: f64) -> Self { Point { x, y } }
    pub fn distance(&self) -> f64 { (self.x*self.x + self.y*self.y).sqrt() }
}

# Build and pack:
# wasm-pack build --target web        β†’ generates JS/TS bindings + .wasm
# wasm-pack build --target bundler    β†’ for webpack/vite

# In JavaScript:
// import init, { add, Point } from './pkg/my_module.js';
// await init();
// console.log(add(2, 3));   // 5
// const p = new Point(3, 4);
// console.log(p.distance()); // 5

# web-sys β€” access browser APIs from Rust:
use web_sys::{window, Document, Element};
let window = window().unwrap();
let document = window.document().unwrap();
let element = document.create_element("div").unwrap();

# Yew β€” React-like component framework for Rust WASM:
use yew::prelude::*;
#[function_component]
fn App() -> Html {
    let counter = use_state(|| 0);
    let onclick = { let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1)) };
    html! {  }
}
6How do you publish a Rust crate to crates.io? What makes a good library API in Rust?
# Publishing to crates.io:
# 1. Add metadata to Cargo.toml:
[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
description = "A useful library"
license = "MIT OR Apache-2.0"    # Rust convention: dual license
homepage = "https://github.com/me/my-crate"
repository = "https://github.com/me/my-crate"
documentation = "https://docs.rs/my-crate"
keywords = ["utility", "parsing"]
categories = ["parsing"]
readme = "README.md"

# 2. cargo login (get token from crates.io)
# 3. cargo publish --dry-run
# 4. cargo publish

# Semantic versioning:
# 0.x.y: unstable API, breaking changes in minor versions
# 1.x.y: stable, breaking changes only in major versions
# cargo-semver-checks β€” detect accidental breaking changes

# API design principles for Rust libraries:
# 1. Use &str not &String; &[T] not &Vec (Rust API Guidelines)
# 2. Accept impl Into for ergonomic conversion
# 3. Return concrete types, accept trait objects (if needed)
# 4. Use #[non_exhaustive] on enums/structs you may extend
# 5. Use builder pattern for complex configuration
# 6. Document everything: //! for modules, /// for items
# 7. Provide doc tests β€” they're also unit tests
# 8. Use #[must_use] on Results and important values
# 9. Implement std traits: Debug, Clone, Display, PartialEq as appropriate
# 10. Avoid panicking in library code β€” use Result instead

Systems & Performance

6 questions
1How do you write zero-copy parsing and serialization in Rust?
// Zero-copy parsing: return references into the input instead of clones
// serde with 'de lifetime:
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Message<'a> {
    #[serde(borrow)]
    content: &'a str,    // borrows from input β€” no allocation!
    id: u64,
}

let json = r#"{"content": "hello", "id": 42}"#;
let msg: Message = serde_json::from_str(json)?;
// msg.content points into `json` β€” no String allocated

// bytes crate β€” reference-counted byte buffer for zero-copy network parsing:
use bytes::{Bytes, Buf, BufMut, BytesMut};

let data: Bytes = Bytes::from("hello world");
let slice: Bytes = data.slice(0..5);  // "hello" β€” shares underlying memory
// Bytes is cheap to clone (arc-based reference counting)

// nom β€” zero-copy parser combinators:
use nom::{bytes::complete::take_while, IResult};

fn parse_word(input: &str) -> IResult<&str, &str> {
    take_while(|c: char| c.is_alphabetic())(input)
    // Returns &str pointing into `input` β€” zero allocation
}

// Manual zero-copy with lifetime:
struct Frame<'a> {
    header: &'a [u8],  // borrows from input buffer
    payload: &'a [u8],
}

fn parse_frame(buf: &[u8]) -> Result {
    let (header, rest) = buf.split_at(4);
    let payload_len = u32::from_be_bytes(header.try_into()?) as usize;
    let payload = &rest[..payload_len];  // slice into buf β€” no copy
    Ok(Frame { header, payload })
}
2How do you write a custom allocator or use arena allocation in Rust?
// Arena allocation β€” allocate many objects, free all at once
// bumpalo crate β€” fast bump allocator:
use bumpalo::Bump;

let arena = Bump::new();

// All allocations from this arena:
let x: &mut i32 = arena.alloc(42);
let s: &str = arena.alloc_str("hello");
let v: &mut Vec = arena.alloc(Vec::new());

// No individual frees needed β€” all memory freed when arena drops
// Extremely fast: allocation is just a pointer bump (no bookkeeping)
// Perfect for: ASTs, parsing results, request-scoped data

// typed-arena β€” like bumpalo but homogeneous:
use typed_arena::Arena;
let arena: Arena = Arena::new();
let s1: &String = arena.alloc(String::from("hello"));
let s2: &String = arena.alloc(String::from("world"));
// All Strings freed when arena drops

// indextree / slotmap β€” alternative to pointer-based trees:
use slotmap::{SlotMap, DefaultKey};
let mut map: SlotMap = SlotMap::new();
let key1 = map.insert("hello".into());
let key2 = map.insert("world".into());
// Use keys instead of raw pointers β€” stable keys even after removals

// id-arena β€” allocate objects with stable IDs, O(1) lookup:
use id_arena::{Arena, Id};
struct Node { value: i32, children: Vec> }
let mut arena: Arena = Arena::new();
let root = arena.alloc(Node { value: 1, children: vec![] });
let child = arena.alloc(Node { value: 2, children: vec![] });
arena[root].children.push(child);
3How do you use SIMD intrinsics in Rust for performance-critical code?
// Auto-vectorization β€” let the compiler do it:
fn sum_f32(data: &[f32]) -> f32 {
    data.iter().sum()   // compiler may auto-vectorize with SIMD
}

// Check with: RUSTFLAGS="-C target-cpu=native" cargo build --release
// rustc --emit=asm to verify vectorization

// Explicit SIMD with std::arch (nightly or specific targets):
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;

#[target_feature(enable = "avx2")]
unsafe fn dot_product_avx2(a: &[f32], b: &[f32]) -> f32 {
    assert_eq!(a.len(), b.len());
    assert!(a.len() % 8 == 0);  // AVX2: 256-bit = 8 Γ— f32

    let mut sum = _mm256_setzero_ps();
    for i in (0..a.len()).step_by(8) {
        let va = _mm256_loadu_ps(&a[i]);
        let vb = _mm256_loadu_ps(&b[i]);
        sum = _mm256_fmadd_ps(va, vb, sum);  // fused multiply-add
    }
    // Horizontal sum of 8 f32s:
    let sum128 = _mm_add_ps(_mm256_extractf128_ps(sum, 0), _mm256_extractf128_ps(sum, 1));
    let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128));
    let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1));
    _mm_cvtss_f32(sum32)
}

// portable-simd (nightly) β€” safe, portable SIMD:
#![feature(portable_simd)]
use std::simd::f32x8;

fn sum_simd(data: &[f32]) -> f32 {
    let chunks = data.chunks_exact(8);
    let remainder = chunks.remainder();
    let simd_sum = chunks.map(f32x8::from_slice).fold(f32x8::splat(0.0), |acc, x| acc + x);
    simd_sum.reduce_sum() + remainder.iter().sum::()
}

// wide crate β€” safe portable SIMD for stable Rust:
use wide::f32x8;
let a = f32x8::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
let b = f32x8::splat(2.0);
let result = a * b;
4How do you write embedded or no_std Rust? What are the constraints?
// no_std β€” opt out of the standard library
// Typical for: embedded, kernels, bootloaders, WASM without WASI
#![no_std]

// Still have access to:
// - core: primitive types, traits, basic utilities (no heap, no OS)
// - alloc: heap allocation (Box, Vec, String) if you provide an allocator

#![no_std]
extern crate alloc;
use alloc::{vec::Vec, string::String, boxed::Box};

// Provide a panic handler (required in no_std):
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}  // or: print error, reset device
}

// Embedded example (cortex-m):
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use cortex_m_semihosting::hprintln;

#[entry]
fn main() -> ! {
    hprintln!("Hello from embedded Rust!").unwrap();
    loop {}
}

// Provide allocator for no_std + alloc:
use linked_list_allocator::LockedHeap;
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

// RTIC β€” real-time interrupt-driven concurrency framework:
#[rtic::app(device = stm32f4::stm32f407)]
mod app {
    #[shared] struct Shared { led: Led }
    #[local]  struct Local  { button: Button }
    #[init]   fn init(cx: init::Context) -> (Shared, Local) { ... }
    #[task(binds = TIM2, shared = [led])]
    fn timer_tick(cx: timer_tick::Context) { cx.shared.led.lock(|l| l.toggle()); }
}
5How do you profile and benchmark Rust code? What tools do you use?
// Criterion.rs β€” statistical benchmarking (stable)
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, black_box};

fn bench_sort(c: &mut Criterion) {
    let mut group = c.benchmark_group("sorting");

    for size in [100usize, 1000, 10000] {
        group.bench_with_input(BenchmarkId::new("std_sort", size), &size, |b, &s| {
            let mut data: Vec = (0..s as i32).rev().collect();
            b.iter(|| {
                let mut v = data.clone();
                v.sort();
                black_box(v)  // prevent compiler from optimizing away
            });
        });
    }
    group.finish();
}

criterion_group!(benches, bench_sort);
criterion_main!(benches);
// cargo bench

// flamegraph / perf β€” CPU profiling:
// cargo flamegraph -- --bench my_bench
// perf record --call-graph dwarf cargo run --release
// perf report

// Samply β€” modern macOS/Linux profiler:
// samply record cargo run --release

// DHAT β€” heap profiling:
use dhat::{Dhat, DhatAlloc};
#[global_allocator]
static ALLOC: DhatAlloc = DhatAlloc;
fn main() {
    let _dhat = Dhat::start_heap_profiling();
    // ... run workload ...
}  // generates dhat-heap.json β€” view in DHAT viewer

// cargo-profiling suite:
// cargo-asm    β€” view generated assembly for a function
// cargo-llvm-ir β€” view LLVM IR
// cargo-bloat  β€” find what's taking up space in your binary
// twiggy       β€” WASM size profiling
6What makes Rust well-suited for building CLI tools, and how do you structure a production CLI?
use clap::{Parser, Subcommand, Args};
use anyhow::Result;

#[derive(Parser)]
#[command(name = "mytool", version, about = "A great CLI tool")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
    #[arg(short, long, global = true)]
    verbose: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Process a file
    Process(ProcessArgs),
    /// Show status
    Status,
}

#[derive(Args)]
struct ProcessArgs {
    #[arg(value_name = "FILE")]
    input: std::path::PathBuf,
    #[arg(short, long, default_value = "output.txt")]
    output: std::path::PathBuf,
    #[arg(long, value_delimiter = ',')]
    tags: Vec,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    // tracing for structured output:
    tracing_subscriber::fmt()
        .with_max_level(if cli.verbose { tracing::Level::DEBUG } else { tracing::Level::INFO })
        .init();

    match cli.command {
        Commands::Process(args) => process_cmd(args)?,
        Commands::Status => status_cmd()?,
    }
    Ok(())
}

// Production CLI features:
// - indicatif: progress bars and spinners
// - console: terminal styling (colors, bold)
// - dialoguer: interactive prompts (select, confirm, input)
// - owo-colors: colorized output
// - tabled: formatted tables in terminal

// Rust CLI advantages:
// - Single static binary β€” trivial distribution (just copy the binary)
// - Fast startup (<1ms vs Java's 100ms+)
// - Low memory β€” no JVM/Python overhead
// - Cross-compilation: build Linux binary from macOS trivially

AI / ML Integration

7 questions
1How do you integrate LLM APIs into a Rust application? Build an async streaming client.
use reqwest::Client;
use serde::{Deserialize, Serialize};
use futures::StreamExt;
use tokio::io::{AsyncWriteExt, stdout};

#[derive(Serialize)]
struct ChatRequest {
    model: String,
    messages: Vec,
    stream: bool,
    max_tokens: u32,
}

#[derive(Serialize, Deserialize, Clone)]
struct Message { role: String, content: String }

#[derive(Deserialize)]
struct StreamChunk {
    #[serde(rename = "type")]
    chunk_type: String,
    delta: Option,
}

#[derive(Deserialize)]
struct Delta { text: Option }

async fn stream_completion(prompt: &str, api_key: &str) -> anyhow::Result {
    let client = Client::new();
    let request = ChatRequest {
        model: "claude-opus-4-5".into(),
        messages: vec![Message { role: "user".into(), content: prompt.into() }],
        stream: true,
        max_tokens: 1024,
    };

    let response = client.post("https://api.anthropic.com/v1/messages")
        .header("x-api-key", api_key)
        .header("anthropic-version", "2023-06-01")
        .json(&request)
        .send()
        .await?;

    let mut stream = response.bytes_stream();
    let mut full_text = String::new();
    let mut stdout = stdout();

    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        let text = std::str::from_utf8(&chunk)?;
        for line in text.lines() {
            if let Some(data) = line.strip_prefix("data: ") {
                if let Ok(parsed) = serde_json::from_str::(data) {
                    if let Some(delta) = parsed.delta {
                        if let Some(text) = delta.text {
                            stdout.write_all(text.as_bytes()).await?;
                            stdout.flush().await?;
                            full_text.push_str(&text);
                        }
                    }
                }
            }
        }
    }
    Ok(full_text)
}
2How do you run local LLMs in Rust? What libraries are available?
// llama-cpp-rs β€” bindings to llama.cpp for local inference
use llama_cpp::{LlamaModel, LlamaParams, SessionParams};

let model = LlamaModel::load_from_file(
    "llama-3-8b-q4_k_m.gguf", LlamaParams::default()
)?;
let mut session = model.create_session(SessionParams::default())?;

session.advance_context("Explain Rust ownership in one sentence.")?;
let output = session.start_completing_with(
    StandardSampler::default(), 256
)?;
for token in output {
    print!("{}", token?);
}

// Ollama API client β€” simplest approach (Ollama runs locally)
async fn ollama_complete(prompt: &str) -> anyhow::Result {
    let client = reqwest::Client::new();
    let response = client.post("http://localhost:11434/api/generate")
        .json(&serde_json::json!({
            "model": "llama3.2",
            "prompt": prompt,
            "stream": false
        }))
        .send().await?
        .json::().await?;
    Ok(response["response"].as_str().unwrap_or("").to_string())
}

// candle β€” pure Rust ML framework from HuggingFace
use candle_core::{Tensor, Device};
use candle_nn::{Linear, Module};
let device = Device::Cuda(0)?;  // or Device::Cpu
let x = Tensor::ones(&[2, 768], candle_core::DType::F32, &device)?;

// rust-bert β€” HuggingFace transformers in Rust
use rust_bert::pipelines::text_generation::{TextGenerationConfig, TextGenerationModel};
let model = TextGenerationModel::new(Default::default())?;
let outputs = model.generate(&["Rust is"], None)?;
3How do you implement embedding generation and vector search in Rust?
use fastembed::{TextEmbedding, InitOptions, EmbeddingModel};
use qdrant_client::{Qdrant, qdrant::{CreateCollectionBuilder, VectorsConfigBuilder,
    Distance, PointStruct, UpsertPointsBuilder, SearchPointsBuilder}};

// fastembed-rs β€” fast local embeddings (ONNX Runtime)
async fn generate_embeddings() -> anyhow::Result<()> {
    let model = TextEmbedding::try_new(InitOptions {
        model_name: EmbeddingModel::AllMiniLML6V2,
        show_download_progress: true,
        ..Default::default()
    })?;

    let docs = vec!["Rust is memory safe", "Ownership prevents data races"];
    let embeddings = model.embed(docs, None)?;  // Vec>
    println!("Embedding dim: {}", embeddings[0].len());
    Ok(())
}

// Qdrant client β€” vector database
async fn vector_search() -> anyhow::Result<()> {
    let client = Qdrant::from_url("http://localhost:6334").build()?;

    // Create collection
    client.create_collection(CreateCollectionBuilder::new("rust_docs")
        .vectors_config(VectorsConfigBuilder::default()
            .size(384)               // all-MiniLM-L6-v2 dimensions
            .distance(Distance::Cosine))).await?;

    // Upsert vectors
    let points = vec![
        PointStruct::new(1, vec![0.1f32; 384],
            serde_json::json!({"content": "Rust ownership rules"}).try_into()?)
    ];
    client.upsert_points(UpsertPointsBuilder::new("rust_docs", points)).await?;

    // Search
    let results = client.search_points(
        SearchPointsBuilder::new("rust_docs", vec![0.1f32; 384], 5)
            .with_payload(true)).await?;

    for result in results.result {
        println!("Score: {:.3}, Payload: {:?}", result.score, result.payload);
    }
    Ok(())
}
4How do you build high-performance AI inference servers in Rust?
use axum::{Router, routing::post, Json, extract::State};
use tokio::sync::{Semaphore, mpsc};
use std::sync::Arc;

// Inference server with request batching
struct InferenceServer {
    model: Arc,           // shared model, thread-safe
    semaphore: Arc,       // limit concurrent inferences
    batch_tx: mpsc::Sender,
}

#[derive(serde::Deserialize)]
struct InferRequest { text: String }
#[derive(serde::Serialize)]
struct InferResponse { embedding: Vec, latency_ms: u64 }

async fn infer(
    State(server): State>,
    Json(req): Json,
) -> Json {
    let start = std::time::Instant::now();

    // Acquire semaphore β€” limit to N concurrent GPU inferences
    let _permit = server.semaphore.acquire().await.unwrap();

    // spawn_blocking β€” run ONNX inference in thread pool (blocking C++ call)
    let model = Arc::clone(&server.model);
    let embedding = tokio::task::spawn_blocking(move || {
        model.embed(&req.text)  // blocking call to ONNX Runtime
    }).await.unwrap();

    Json(InferResponse {
        embedding,
        latency_ms: start.elapsed().as_millis() as u64,
    })
}

// Dynamic batching β€” accumulate requests, batch for GPU efficiency
struct BatchWorker {
    model: Arc,
    rx: mpsc::Receiver,
    max_batch: usize,
    timeout: Duration,
}

impl BatchWorker {
    async fn run(mut self) {
        let mut batch = Vec::new();
        loop {
            tokio::select! {
                Some(req) = self.rx.recv() => {
                    batch.push(req);
                    if batch.len() >= self.max_batch { self.flush(&mut batch).await; }
                }
                _ = tokio::time::sleep(self.timeout) => {
                    if !batch.is_empty() { self.flush(&mut batch).await; }
                }
            }
        }
    }
}
5How do you use ONNX Runtime from Rust for cross-framework model deployment?
use ort::{Session, SessionBuilder, inputs, Value, GraphOptimizationLevel};
use ndarray::Array2;

// Load any ONNX model (PyTorch, TensorFlow, scikit-learn all export to ONNX)
fn load_model() -> anyhow::Result {
    let session = SessionBuilder::new()?
        .with_optimization_level(GraphOptimizationLevel::Level3)?
        .with_execution_providers([
            ort::CUDAExecutionProvider::default().build(),  // GPU if available
            ort::CPUExecutionProvider::default().build(),   // fallback to CPU
        ])?
        .with_model_from_file("model.onnx")?;
    Ok(session)
}

// Run inference
fn predict(session: &Session, input: &[f32], input_shape: [usize; 2]) -> anyhow::Result> {
    let input_array = Array2::from_shape_vec(input_shape, input.to_vec())?;
    let input_tensor = Value::from_array(input_array)?;

    let outputs = session.run(inputs!["input" => input_tensor]?)?;
    let output = outputs["output"].try_extract_tensor::()?;
    Ok(output.view().to_owned().into_raw_vec())
}

// Benchmark-friendly: pre-allocate input/output buffers
struct InferenceEngine {
    session: Session,
    input_buf: Vec,
    batch_size: usize,
}

impl InferenceEngine {
    fn run_batch(&mut self, texts: &[&str]) -> Vec> {
        // Tokenize β†’ fill input_buf β†’ run ONNX β†’ parse outputs
        // Zero allocation in the hot path if buffers are pre-sized
        texts.iter().map(|t| self.infer_one(t)).collect()
    }
}

// Key Rust advantage for ML serving:
// - Zero-cost FFI to ONNX Runtime C++ library
// - No GC pauses during inference
// - Precise memory control β€” pre-allocate, reuse
// - Async I/O + sync inference on thread pool = maximum throughput
6How does Rust's ownership model benefit AI/ML inference pipelines?

Rust's ownership model solves several pain points common in ML inference systems:

1. Predictable latency β€” no GC pauses:

// Java/Python: GC can pause for 10-500ms during inference β€” bad for SLAs
// Rust: deterministic memory management β€” no pauses
// Critical for real-time applications: autonomous vehicles, trading, gaming AI

// Rust inference p99 latency is consistent; JVM p99 can spike 10-100x p50

2. Safe parallel inference without data races:

// The type system prevents concurrent modification of model state
let model = Arc::new(load_model());  // read-only shared model
for _ in 0..num_workers {
    let m = Arc::clone(&model);
    tokio::spawn(async move {
        m.predict(&batch).await  // safe concurrent reads β€” no mutex needed
    });
}
// Mutable state (batches, results) isolated per task β€” no races possible

3. Zero-copy data paths:

// Pass slices to ONNX Runtime without copying:
fn infer(session: &Session, data: &[f32]) -> Vec {
    let view = ArrayView1::from(data);   // no copy β€” view into caller's buffer
    let output = session.run(...)?;
    output.extract_tensor().to_vec()     // one copy at the output only
}

// Compare Python: tensor creation almost always allocates new memory

4. Embedding Rust in Python/Java ML pipelines (PyO3/JNI):

use pyo3::prelude::*;
#[pyfunction]
fn fast_tokenize(py: Python, texts: Vec) -> PyResult>> {
    Ok(texts.par_iter()  // Rayon parallel β€” releases Python GIL
        .map(|t| tokenizer.encode(t))
        .collect())
}
// Call Rust from Python for 10-50x speedup on tokenization/preprocessing

5. Embedded ML: Rust runs on microcontrollers with 64KB RAM. ONNX models quantized to INT8 can run inference on-device without networking β€” impossible in Python, awkward in C++, natural in Rust's no_std environment.

7How do you implement a RAG pipeline in Rust for a production search service?
use fastembed::TextEmbedding;
use qdrant_client::Qdrant;
use reqwest::Client;
use serde::{Deserialize, Serialize};

struct RagPipeline {
    embedder: TextEmbedding,
    vector_db: Qdrant,
    llm_client: Client,
    api_key: String,
}

impl RagPipeline {
    // STEP 1: Ingest documents
    async fn ingest(&self, docs: &[Document]) -> anyhow::Result<()> {
        let texts: Vec<&str> = docs.iter().map(|d| d.content.as_str()).collect();
        let embeddings = self.embedder.embed(texts, None)?;

        let points: Vec<_> = docs.iter().zip(embeddings.iter())
            .map(|(doc, emb)| PointStruct::new(
                doc.id,
                emb.clone(),
                serde_json::json!({"content": doc.content, "source": doc.source})
                    .try_into().unwrap()
            ))
            .collect();

        self.vector_db.upsert_points(
            UpsertPointsBuilder::new("docs", points)
        ).await?;
        Ok(())
    }

    // STEP 2: Retrieve relevant chunks
    async fn retrieve(&self, query: &str, top_k: u64) -> anyhow::Result> {
        let query_emb = self.embedder.embed(vec![query], None)?;
        let results = self.vector_db.search_points(
            SearchPointsBuilder::new("docs", query_emb[0].clone(), top_k)
                .with_payload(true)
        ).await?;

        Ok(results.result.into_iter()
            .filter_map(|r| r.payload.get("content")
                .and_then(|v| v.as_str().map(String::from)))
            .collect())
    }

    // STEP 3: Generate grounded response
    async fn generate(&self, query: &str, context: &[String]) -> anyhow::Result {
        let ctx = context.join("\n\n---\n\n");
        let prompt = format!(
            "Answer based only on this context:\n{}\n\nQuestion: {}\nAnswer:",
            ctx, query
        );

        let response = self.llm_client
            .post("https://api.anthropic.com/v1/messages")
            .header("x-api-key", &self.api_key)
            .header("anthropic-version", "2023-06-01")
            .json(&serde_json::json!({
                "model": "claude-haiku-4-5",
                "max_tokens": 1024,
                "messages": [{"role": "user", "content": prompt}]
            }))
            .send().await?.json::().await?;

        Ok(response["content"][0]["text"].as_str().unwrap_or("").to_string())
    }

    // Full RAG query
    pub async fn query(&self, question: &str) -> anyhow::Result {
        let chunks = self.retrieve(question, 5).await?;
        self.generate(question, &chunks).await
    }
}