Table of Contents
- Ownership and Borrowing Deep Dive
- Lifetimes Advanced Concepts
- Concurrency and Parallelism
- Unsafe Rust
- Macros and Metaprogramming
- Performance Optimization
- Async Programming
- Common Coding Problems
- System Programming Concepts
- Best Practices and Patterns
1. Ownership and Borrowing Deep Dive
Q1: Explain the difference between Box<T>, Rc<T>, and Arc<T> with use cases.
Answer:
// Box<T> - Single ownership, heap allocation struct TreeNode { value: i32, left: Option<Box<TreeNode>>, right: Option<Box<TreeNode>>, } // Rc<T> - Multiple ownership (single-threaded) use std::rc::Rc; struct GraphNode { value: i32, edges: Vec<Rc<GraphNode>>, // Multiple nodes can share ownership } // Arc<T> - Multiple ownership (thread-safe) use std::sync::Arc; use std::thread; let data = Arc::new(vec![1, 2, 3]); let mut handles = vec![]; for _ in 0..3 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { println!("Data: {:?}", data); })); } Key Points:
Box<T>: Single ownership, compile-time known size, for recursive typesRc<T>: Reference counting, non-thread-safe, for shared ownership in single threadArc<T>: Atomic reference counting, thread-safe, for shared ownership across threads
Q2: What's the difference between Cell<T>, RefCell<T>, and Mutex<T>?
Answer:
use std::cell::{Cell, RefCell}; use std::sync::Mutex; // Cell<T> - For Copy types, no runtime borrow checking let cell = Cell::new(42); cell.set(100); let value = cell.get(); // Always returns a copy // RefCell<T> - For non-Copy types, runtime borrow checking let ref_cell = RefCell::new(String::from("hello")); // Multiple immutable borrows allowed let borrow1 = ref_cell.borrow(); let borrow2 = ref_cell.borrow(); // let mut borrow_mut = ref_cell.borrow_mut(); // Would panic at runtime drop(borrow1); drop(borrow2); // Now mutable borrow works let mut borrow_mut = ref_cell.borrow_mut(); borrow_mut.push_str(" world"); // Mutex<T> - Thread-safe interior mutability let mutex = Mutex::new(42); { let mut guard = mutex.lock().unwrap(); *guard += 10; } // Mutex automatically unlocked when guard drops Interview Tip: Emphasize that Cell has no runtime overhead but only works with Copy types, RefCell has runtime borrow checking, and Mutex provides thread safety.
Q3: How does Rust prevent data races at compile time?
Answer:
// This code won't compile - data race prevented let mut data = vec![1, 2, 3]; let ref1 = &data; let ref2 = &mut data; // ERROR: cannot borrow `data` as mutable because it's also borrowed as immutable // This is safe - compiler prevents race conditions use std::sync::{Arc, Mutex}; use std::thread; let data = Arc::new(Mutex::new(vec![1, 2, 3])); let mut handles = vec![]; for _ in 0..3 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let mut data = data.lock().unwrap(); data.push(1); // Safe: Mutex ensures mutual exclusion })); } Rust's Data Race Prevention:
- Ownership rules: Only one owner at a time
- Borrowing rules: Either one mutable reference or multiple immutable references
- Send/Sync traits: Types know if they're safe to send between threads
- Mutex/Atomics: Provide safe shared mutation
2. Lifetimes Advanced Concepts
Q4: Explain lifetime elision rules with examples.
Answer:
// Lifetime elision rules (three rules applied automatically) // Rule 1: Each input reference gets its own lifetime fn first_word(s: &str) -> &str { // Desugared: fn first_word<'a>(s: &'a str) -> &'a str &s[0..1] } // Rule 2: If only one input lifetime, output gets that lifetime fn longest(x: &str, y: &str) -> &str { // ERROR: Need explicit lifetimes if x.len() > y.len() { x } else { y } } // Fix: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str // Rule 3: If self, output gets self's lifetime struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { // Desugared: fn level<'b>(&'b self) -> i32 3 } fn announce_and_return_part(&self, announcement: &str) -> &str { // Desugared: fn announce_and_return_part<'b, 'c>(&'b self, announcement: &'c str) -> &'b str println!("{}", announcement); self.part } } Q5: What's the difference between 'static lifetime and &'static str?
Answer:
// 'static lifetime - lives for the entire program const MESSAGE: &'static str = "Hello"; // Stored in read-only memory // These are different: fn example() { // This string literal has 'static lifetime let s1: &'static str = "I live forever"; // This creates a String, then takes a reference let s2 = String::from("I'm on the heap"); let s2_ref: &str = &s2; // Not 'static // This can't be 'static because it might be deallocated // let s3: &'static str = &s2; // ERROR } // 'static constraint on generic type fn print_if_static<T: 'static>(t: T) { // T must not contain any non-static references } // Common misconception: 'static doesn't mean "lives forever in memory" // It means "can be safely treated as living for the entire program" Q6: How do lifetimes work with structs and impl blocks?
Answer:
struct Parser<'a> { input: &'a str, position: usize, } impl<'a> Parser<'a> { fn new(input: &'a str) -> Self { Parser { input, position: 0 } } fn next_word(&mut self) -> Option<&'a str> { // Returns slice with same lifetime as input let start = self.position; while self.position < self.input.len() && !self.input.chars().nth(self.position).unwrap().is_whitespace() { self.position += 1; } if start < self.position { Some(&self.input[start..self.position]) } else { None } } } // Multiple lifetimes in struct struct Pair<'a, 'b> { first: &'a str, second: &'b str, } impl<'a, 'b> Pair<'a, 'b> { fn longest(&self) -> &str where 'a: 'b, // 'a outlives 'b { if self.first.len() > self.second.len() { self.first } else { self.second } } } 3. Concurrency and Parallelism
Q7: Explain Send and Sync traits with examples.
Answer:
use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::thread; // Send: Types that can be transferred across threads // Sync: Types that can be shared across threads safely // Rc<T> is neither Send nor Sync let rc = Rc::new(5); // thread::spawn(move || { println!("{}", rc); }); // ERROR: Rc cannot be sent // Arc<T> is both Send and Sync let arc = Arc::new(5); let arc_clone = Arc::clone(&arc); thread::spawn(move || { println!("From thread: {}", arc_clone); }).join().unwrap(); // Mutex<T> is Send + Sync let mutex = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let m = Arc::clone(&mutex); handles.push(thread::spawn(move || { let mut data = m.lock().unwrap(); *data += 1; })); } // Custom type implementing Send/Sync struct ThreadSafeType { data: Arc<Mutex<Vec<i32>>>, } // Automatically Send/Sync if all fields are Send/Sync struct NotThreadSafe { data: Rc<Vec<i32>>, // Rc makes the whole struct not Send } Q8: What's the difference between thread::spawn and scope threads?
Answer:
use std::thread; // thread::spawn - Owns data, must be 'static fn spawn_example() { let data = vec![1, 2, 3]; let handle = thread::spawn(move || { // Must take ownership println!("Data: {:?}", data); }); handle.join().unwrap(); // println!("{:?}", data); // ERROR: data moved } // scoped threads - Can borrow data fn scope_example() { let data = vec![1, 2, 3]; thread::scope(|s| { s.spawn(|| { // Can borrow data, no 'static required println!("From scope: {:?}", data); }); s.spawn(|| { // Multiple threads can borrow same data println!("Also borrowing: {:?}", data); }); }); // All threads automatically joined here println!("Still have data: {:?}", data); // OK } // Real-world example: Parallel processing fn parallel_sum(data: &[i32]) -> i32 { let chunk_size = data.len() / 4; thread::scope(|s| { let mut handles = vec![]; for chunk in data.chunks(chunk_size) { handles.push(s.spawn(|| { chunk.iter().sum::<i32>() })); } handles.into_iter().map(|h| h.join().unwrap()).sum() }) } Q9: How do you implement a thread-safe singleton in Rust?
Answer:
use std::sync::{Once, OnceLock, LazyLock}; use std::sync::Mutex; // Method 1: Once (manual initialization) static INIT: Once = Once::new(); static mut CONFIG: Option<String> = None; fn get_config() -> &'static String { unsafe { INIT.call_once(|| { CONFIG = Some("production".to_string()); }); CONFIG.as_ref().unwrap() } } // Method 2: OnceLock (stable since Rust 1.70) static DATABASE: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); fn get_database() -> &'static Mutex<Vec<String>> { DATABASE.get_or_init(|| { Mutex::new(Vec::new()) }) } // Method 3: LazyLock (nightly) or lazy_static crate static CACHE: LazyLock<Mutex<Vec<i32>>> = LazyLock::new(|| { Mutex::new(vec![1, 2, 3]) }); // Method 4: Arc with lazy initialization use std::sync::Arc; struct AppState { settings: Arc<Mutex<Vec<String>>>, } impl AppState { fn new() -> Self { Self { settings: Arc::new(Mutex::new(Vec::new())), } } fn clone(&self) -> Self { Self { settings: Arc::clone(&self.settings), } } } 4. Unsafe Rust
Q10: When would you need to use unsafe Rust?
Answer:
// 1. Dereferencing raw pointers fn unsafe_deref() { let x = 5; let raw = &x as *const i32; unsafe { println!("Raw pointer value: {}", *raw); } } // 2. Calling unsafe functions (FFI) extern "C" { fn abs(input: i32) -> i32; } fn call_c_function() { unsafe { println!("Absolute value: {}", abs(-3)); } } // 3. Accessing/modifying mutable static variables static mut COUNTER: u32 = 0; fn increment_counter() { unsafe { COUNTER += 1; } } // 4. Implementing unsafe traits unsafe trait MyUnsafeTrait { // Must be implemented carefully } unsafe impl MyUnsafeTrait for i32 {} // 5. Union field access union MyUnion { i: i32, f: f32, } fn union_example() { let u = MyUnion { i: 42 }; unsafe { println!("Int: {}", u.i); } } Q11: Explain the invariants that must be maintained when implementing unsafe code.
Answer:
use std::ptr; // Invariant 1: Raw pointers must be valid fn invalid_raw_pointer() { let ptr: *const i32 = ptr::null(); unsafe { // println!("{}", *ptr); // UNDEFINED BEHAVIOR! } } // Invariant 2: No aliasing violations fn aliasing_violation() { let mut data = 10; let r1 = &mut data as *mut i32; let r2 = &mut data as *mut i32; // Two mutable pointers to same data unsafe { *r1 = 20; // *r2 = 30; // Undefined behavior! } } // Invariant 3: Proper alignment #[repr(packed)] struct Packed { x: u8, y: u32, // Not properly aligned } // Safe abstraction example struct MyVec<T> { ptr: *mut T, len: usize, capacity: usize, } impl<T> MyVec<T> { fn new() -> Self { Self { ptr: ptr::null_mut(), len: 0, capacity: 0, } } fn push(&mut self, value: T) { if self.len == self.capacity { self.grow(); } unsafe { // We guarantee ptr is valid and not aliased ptr::write(self.ptr.add(self.len), value); self.len += 1; } } unsafe fn get_unchecked(&self, index: usize) -> &T { // Caller must ensure index is in bounds &*self.ptr.add(index) } } Q12: What's the purpose of MaybeUninit<T> and when would you use it?
Answer:
use std::mem::MaybeUninit; // MaybeUninit is used for uninitialized memory fn manual_array_initialization() { // Create an uninitialized array let mut arr: [MaybeUninit<i32>; 10] = unsafe { MaybeUninit::uninit().assume_init() }; // Initialize elements for i in 0..10 { arr[i] = MaybeUninit::new(i as i32); } // Assume all elements are initialized let arr = unsafe { std::mem::transmute::<_, [i32; 10]>(arr) }; println!("{:?}", arr); } // Partial initialization struct Buffer { data: [MaybeUninit<u8>; 1024], len: usize, } impl Buffer { fn new() -> Self { Self { data: unsafe { MaybeUninit::uninit().assume_init() }, len: 0, } } fn push(&mut self, byte: u8) { if self.len < 1024 { self.data[self.len] = MaybeUninit::new(byte); self.len += 1; } } fn as_slice(&self) -> &[u8] { unsafe { std::slice::from_raw_parts( self.data.as_ptr() as *const u8, self.len ) } } } 5. Macros and Metaprogramming
Q13: Explain the difference between declarative and procedural macros.
Answer:
// Declarative macros (macro_rules!) - Pattern matching macro_rules! vec_of_strings { ($($x:expr),*) => { vec![$(String::from($x)),*] }; } // Usage let v = vec_of_strings!["a", "b", "c"]; // Procedural macros - More powerful, operate on AST // In a separate crate (requires proc-macro = true) // Derive macro use proc_macro::TokenStream; #[proc_macro_derive(MyTrait)] pub fn my_trait_derive(input: TokenStream) -> TokenStream { // Parse input and generate code input } // Attribute macro #[proc_macro_attribute] pub fn log_function(args: TokenStream, input: TokenStream) -> TokenStream { // Generate wrapper with logging input } // Function-like macro #[proc_macro] pub fn sql(input: TokenStream) -> TokenStream { // Parse SQL-like syntax at compile time input } Q14: How do you write a macro that handles different types?
Answer:
// Macro that works with different numeric types macro_rules! create_add_function { ($name:ident, $type:ty) => { fn $name(a: $type, b: $type) -> $type { a + b } }; } create_add_function!(add_i32, i32); create_add_function!(add_f64, f64); // Macro with repetition and type handling macro_rules! sum { // Base case: single number ($x:expr) => { $x }; // Recursive case: add first to sum of rest ($x:expr, $($y:expr),+) => { $x + sum!($($y),+) }; } assert_eq!(sum!(1, 2, 3, 4), 10); // Macro with type-specific handling macro_rules! as_string { ($x:expr) => { match $x { s if stringify!($x).starts_with('"') => $x.to_string(), _ => format!("{:?}", $x), } }; } // Macro with different patterns macro_rules! debug_print { // Print with label ($label:expr, $value:expr) => { println!("{}: {:?}", $label, $value); }; // Print multiple values ($($value:expr),+) => { $(println!("{:?}", $value);)+ }; // Print with custom format ($format:expr, $($value:expr),+) => { println!($format, $($value),+); }; } Q15: What's the difference between stringify! and format!?
Answer:
// stringify! - Compile-time, turns tokens into string literal macro_rules! make_function { ($name:ident) => { fn $name() { println!("Function {} called", stringify!($name)); } }; } make_function!(hello); // Generates: // fn hello() { // println!("Function {} called", "hello"); // } // format! - Runtime, creates formatted String fn runtime_format() { let name = "world"; let greeting = format!("Hello, {}!", name); // Runtime println!("{}", greeting); } // Comparison macro_rules! compare { ($x:expr) => { println!("stringify: {}", stringify!($x)); println!("format: {}", format!("{:?}", $x)); }; } compare!(1 + 2); // Output: // stringify: 1 + 2 // format: 3 6. Performance Optimization
Q16: Explain zero-cost abstractions in Rust with examples.
Answer:
// Zero-cost abstraction: Iterators fn sum_with_loop(v: &[i32]) -> i32 { let mut sum = 0; for i in 0..v.len() { sum += v[i]; } sum } fn sum_with_iterator(v: &[i32]) -> i32 { v.iter().sum() } // Both compile to identical assembly // Zero-cost abstraction: Generics fn identity<T>(x: T) -> T { x } // Monomorphized to separate functions for each type // Zero-cost abstraction: Enums enum MyOption<T> { Some(T), None, } // Compiles to tagged union, no overhead beyond C union // Trait objects (dynamic dispatch) have runtime cost fn dynamic_dispatch(x: &dyn Display) { println!("{}", x); // Virtual call overhead } // Static dispatch (generics) has zero runtime cost fn static_dispatch<T: Display>(x: T) { println!("{}", x); // Direct call, monomorphized } Q17: How do you optimize Rust code for performance?
Answer:
use std::time::Instant; // 1. Pre-allocate collections fn inefficient_allocation() { let start = Instant::now(); let mut v = Vec::new(); for i in 0..100_000 { v.push(i); // May reallocate multiple times } println!("Inefficient: {:?}", start.elapsed()); } fn efficient_allocation() { let start = Instant::now(); let mut v = Vec::with_capacity(100_000); for i in 0..100_000 { v.push(i); // No reallocation } println!("Efficient: {:?}", start.elapsed()); } // 2. Use iterators instead of manual loops fn manual_loop(v: &[i32]) -> i32 { let mut sum = 0; for i in 0..v.len() { sum += v[i]; // Bounds check each iteration } sum } fn iterator_version(v: &[i32]) -> i32 { v.iter().sum() // No bounds checks, can be auto-vectorized } // 3. Cache-friendly data structures // Bad: Vec of pointers struct Bad { data: Vec<Box<Data>>, // Poor cache locality } // Good: Contiguous data struct Good { data: Vec<Data>, // Good cache locality } // 4. Use const where possible const FACTORS: [i32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Computed at compile time // 5. Avoid unnecessary cloning fn process_string_inefficient(s: String) { let cloned = s.clone(); // Unnecessary println!("{}", cloned); } fn process_string_efficient(s: &str) { println!("{}", s); } // 6. Use small types and bit packing #[repr(packed)] struct PackedData { flags: u8, // 1 byte value: u32, // 4 bytes (unaligned access cost!) // Better: Use alignment directives } Q18: What's the difference between #[inline], #[inline(always)], and #[inline(never)]?
Answer:
// #[inline] - Hint to compiler, can ignore #[inline] fn small_function(x: i32) -> i32 { x + 1 } // #[inline(always)] - Force inlining #[inline(always)] fn very_small_function(x: i32) -> i32 { x * 2 } // #[inline(never)] - Prevent inlining #[inline(never)] fn large_function(x: i32) -> i32 { // Complex logic that shouldn't be inlined let mut result = 0; for i in 0..x { result += i; } result } // When to use each: // - #[inline]: Small functions that are called frequently // - #[inline(always)]: Extremely small functions (1-2 instructions) // - #[inline(never)]: Large functions or when you want to optimize instruction cache // Example: Generic functions often benefit from inlining #[inline] fn generic_add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T { a + b // Generic code benefits from inlining for type specialization } 7. Async Programming
Q19: Explain the difference between async/await and threads.
Answer:
use tokio::time; use std::thread; // Threads: OS-managed, preemptive, heavyweight fn thread_example() { let handle = thread::spawn(|| { for i in 0..5 { println!("Thread: {}", i); thread::sleep(std::time::Duration::from_millis(100)); } }); for i in 0..3 { println!("Main: {}", i); thread::sleep(std::time::Duration::from_millis(150)); } handle.join().unwrap(); } // Async: Language feature, cooperative, lightweight async fn async_example() { let task1 = tokio::spawn(async { for i in 0..5 { println!("Task1: {}", i); time::sleep(time::Duration::from_millis(100)).await; } }); let task2 = tokio::spawn(async { for i in 0..3 { println!("Task2: {}", i); time::sleep(time::Duration::from_millis(150)).await; } }); task1.await.unwrap(); task2.await.unwrap(); } // Key differences: // 1. Memory: Threads ~MB, Tasks ~KB // 2. Context switching: Threads OS-mediated, Tasks at await points // 3. Scalability: Threads limited, Tasks can scale to millions // 4. Blocking: Threads can block, Tasks must be non-blocking // When to use each: // - Threads: CPU-bound work, need true parallelism // - Async: I/O-bound work, many concurrent operations Q20: How do you handle errors in async code?
Answer:
use tokio::fs::File; use tokio::io::{self, AsyncReadExt}; // Basic error handling with ? in async functions async fn read_file() -> io::Result<String> { let mut file = File::open("hello.txt").await?; let mut contents = String::new(); file.read_to_string(&mut contents).await?; Ok(contents) } // Custom error type #[derive(Debug)] enum AppError { Io(io::Error), Parse(String), Timeout, } impl From<io::Error> for AppError { fn from(error: io::Error) -> Self { AppError::Io(error) } } async fn process_file() -> Result<String, AppError> { let contents = read_file().await.map_err(AppError::Io)?; if contents.is_empty() { return Err(AppError::Parse("Empty file".to_string())); } Ok(contents) } // Timeout handling use tokio::time::{timeout, Duration}; async fn with_timeout() -> Result<String, AppError> { let result = timeout( Duration::from_secs(5), read_file() ).await; match result { Ok(Ok(contents)) => Ok(contents), Ok(Err(e)) => Err(AppError::Io(e)), Err(_) => Err(AppError::Timeout), } } // Concurrent error handling use futures::future::{join_all, try_join_all}; async fn concurrent_tasks() -> Result<Vec<String>, AppError> { let tasks = vec![ read_file(), read_file(), read_file(), ]; // All succeed or first error try_join_all(tasks).await.map_err(AppError::Io) } // Error handling in select use tokio::select; async fn select_example() -> Result<(), AppError> { let task1 = with_timeout(); let task2 = with_timeout(); select! { res1 = task1 => { println!("Task1 finished first: {:?}", res1); } res2 = task2 => { println!("Task2 finished first: {:?}", res2); } } Ok(()) } Q21: What's the difference between async fn and returning impl Future?
Answer:
use std::future::Future; use std::pin::Pin; // async fn - Desugars to returning impl Future async fn async_function() -> i32 { 42 } // Desugars to: fn async_function_desugared() -> impl Future<Output = i32> { async { 42 } } // Returning impl Future directly - More control fn manual_future() -> impl Future<Output = i32> { std::future::ready(42) } // Complex case: Conditional futures fn conditional_future(flag: bool) -> impl Future<Output = i32> { if flag { // return async { 42 }; // ERROR: different types // Need to use Box or enum let fut = async { 42 }; fut.left_future() } else { let fut = async { 100 }; fut.right_future() } } // Using Box for dynamic dispatch fn dynamic_future(flag: bool) -> Pin<Box<dyn Future<Output = i32> + Send>> { if flag { Box::pin(async { 42 }) } else { Box::pin(async { 100 }) } } // Key differences: // - async fn: Simpler, always returns opaque type // - impl Future: Can return different futures based on conditions // - Box<dyn Future>: Dynamic dispatch, useful for recursion/collections 8. Common Coding Problems
Q22: Implement a thread-safe producer-consumer queue.
Answer:
use std::collections::VecDeque; use std::sync::{Arc, Mutex, Condvar}; use std::thread; struct Queue<T> { data: Mutex<VecDeque<T>>, not_empty: Condvar, } impl<T> Queue<T> { fn new() -> Self { Queue { data: Mutex::new(VecDeque::new()), not_empty: Condvar::new(), } } fn push(&self, item: T) { let mut data = self.data.lock().unwrap(); data.push_back(item); self.not_empty.notify_one(); } fn pop(&self) -> T { let mut data = self.data.lock().unwrap(); loop { if let Some(item) = data.pop_front() { return item; } data = self.not_empty.wait(data).unwrap(); } } fn try_pop(&self) -> Option<T> { let mut data = self.data.lock().unwrap(); data.pop_front() } } fn main() { let queue = Arc::new(Queue::new()); let mut handles = vec![]; // Producers for i in 0..3 { let q = Arc::clone(&queue); handles.push(thread::spawn(move || { for j in 0..5 { q.push(format!("Producer {}-{}", i, j)); thread::sleep(std::time::Duration::from_millis(10)); } })); } // Consumers for _ in 0..2 { let q = Arc::clone(&queue); handles.push(thread::spawn(move || { for _ in 0..7 { let item = q.pop(); println!("Consumed: {}", item); } })); } for handle in handles { handle.join().unwrap(); } } Q23: Implement a thread pool from scratch.
Answer:
use std::sync::{Arc, Mutex, mpsc}; use std::thread; type Job = Box<dyn FnOnce() + Send + 'static>; struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Self { let thread = thread::spawn(move || loop { let job = receiver.lock().unwrap().recv(); match job { Ok(job) => { println!("Worker {} got a job", id); job(); } Err(_) => { println!("Worker {} shutting down", id); break; } } }); Worker { id, thread: Some(thread), } } } impl ThreadPool { fn new(size: usize) -> Self { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender } } fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { drop(self.sender); // Close channel for worker in &mut self.workers { if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } // Usage fn main() { let pool = ThreadPool::new(4); for i in 0..10 { pool.execute(move || { println!("Task {} executed by thread", i); thread::sleep(std::time::Duration::from_millis(100)); }); } } Q24: Implement a custom iterator.
Answer:
struct Fibonacci { current: u64, next: u64, max: Option<u64>, } impl Fibonacci { fn new() -> Self { Fibonacci { current: 0, next: 1, max: None, } } fn with_max(max: u64) -> Self { Fibonacci { current: 0, next: 1, max: Some(max), } } } impl Iterator for Fibonacci { type Item = u64; fn next(&mut self) -> Option<Self::Item> { let current = self.current; if let Some(max) = self.max { if current > max { return None; } } self.current = self.next; self.next = current + self.next; Some(current) } } // Custom collection struct Buffer<T> { data: Vec<T>, capacity: usize, } impl<T> Buffer<T> { fn new(capacity: usize) -> Self { Buffer { data: Vec::with_capacity(capacity), capacity, } } fn push(&mut self, item: T) -> Option<T> { if self.data.len() < self.capacity { self.data.push(item); None } else { Some(item) // Buffer full } } } // Implementing IntoIterator impl<T> IntoIterator for Buffer<T> { type Item = T; type IntoIter = std::vec::IntoIter<T>; fn into_iter(self) -> Self::IntoIter { self.data.into_iter() } } // Usage fn main() { let fib = Fibonacci::with_max(100); let fib_numbers: Vec<_> = fib.collect(); println!("Fibonacci: {:?}", fib_numbers); // Using iterator adapters let fib_even: Vec<_> = Fibonacci::new() .filter(|&x| x % 2 == 0) .take(10) .collect(); println!("Even Fibonacci: {:?}", fib_even); } 9. System Programming Concepts
Q25: How do you interface with C code in Rust?
Answer:
// 1. Using extern blocks extern "C" { fn strlen(s: *const u8) -> usize; fn puts(s: *const u8) -> i32; } fn call_c_functions() { let s = b"Hello, world!\0"; unsafe { let len = strlen(s.as_ptr()); println!("Length: {}", len); puts(s.as_ptr()); } } // 2. Declaring C functions in Rust #[no_mangle] pub extern "C" fn rust_function(x: i32, y: i32) -> i32 { x + y } // 3. Working with C structs #[repr(C)] struct Point { x: f64, y: f64, } extern "C" { fn distance(p1: *const Point, p2: *const Point) -> f64; } // 4. Callbacks from C to Rust type Callback = extern "C" fn(i32) -> i32; extern "C" fn register_callback(cb: Callback) { // C code stores callback } extern "C" fn my_callback(x: i32) -> i32 { x * 2 } // 5. Build configuration (build.rs) // println!("cargo:rustc-link-search=native=/path/to/libs"); // println!("cargo:rustc-link-lib=static=foo"); // 6. Using bindgen to generate bindings // In build.rs: // bindgen::Builder::default() // .header("wrapper.h") // .generate() // .unwrap() // .write_to_file(out_path) // .unwrap(); Q26: Explain memory layout of Rust types.
Answer:
use std::mem; #[repr(C)] // C-style layout (predictable) struct CStruct { a: u8, // 1 byte b: u32, // 4 bytes c: u16, // 2 bytes } // Layout: a(1) + padding(3) + b(4) + c(2) + padding(2) = 12 bytes #[repr(packed)] // No padding struct PackedStruct { a: u8, b: u32, c: u16, } // Layout: a(1) + b(4) + c(2) = 7 bytes (but unaligned access!) #[repr(align(16))] // 16-byte alignment struct AlignedStruct { data: [u8; 10], } // Layout: 10 bytes + 6 bytes padding to align to 16 // Enum memory layout enum MyEnum { A, // 0 discriminant B(i32), // 1 discriminant + i32 C { x: f64 }, // 2 discriminant + f64 } // Size = max(variant size) + discriminant + padding // Option optimization assert_eq!(mem::size_of::<&i32>(), mem::size_of::<Option<&i32>>()); // Compiler uses null pointer optimization for Option<&T> fn print_layout<T>() { println!("Size of {}: {} bytes", std::any::type_name::<T>(), mem::size_of::<T>()); println!("Alignment: {} bytes", mem::align_of::<T>()); } fn main() { print_layout::<CStruct>(); print_layout::<Option<&i32>>(); print_layout::<Box<dyn Fn()>>(); // Fat pointer: data + vtable } 10. Best Practices and Patterns
Q27: What are the most important Rust design patterns?
Answer:
// 1. Builder Pattern struct Pizza { size: String, toppings: Vec<String>, } struct PizzaBuilder { size: String, toppings: Vec<String>, } impl PizzaBuilder { fn new() -> Self { PizzaBuilder { size: "medium".to_string(), toppings: vec![], } } fn size(mut self, size: &str) -> Self { self.size = size.to_string(); self } fn add_topping(mut self, topping: &str) -> Self { self.toppings.push(topping.to_string()); self } fn build(self) -> Pizza { Pizza { size: self.size, toppings: self.toppings, } } } // 2. RAII (Resource Acquisition Is Initialization) struct DatabaseConnection { connected: bool, } impl DatabaseConnection { fn new() -> Self { println!("Connecting to database..."); DatabaseConnection { connected: true } } } impl Drop for DatabaseConnection { fn drop(&mut self) { println!("Disconnecting from database..."); self.connected = false; } } // 3. Newtype Pattern struct Meters(f64); struct Seconds(f64); fn speed(distance: Meters, time: Seconds) -> f64 { distance.0 / time.0 } // 4. Type State Pattern struct Empty; struct Waiting; struct Confirmed; struct Order<State> { items: Vec<String>, state: std::marker::PhantomData<State>, } impl Order<Empty> { fn new() -> Self { Order { items: vec![], state: std::marker::PhantomData, } } fn add_item(self, item: String) -> Order<Waiting> { let mut items = self.items; items.push(item); Order { items, state: std::marker::PhantomData, } } } impl Order<Waiting> { fn confirm(self) -> Order<Confirmed> { Order { items: self.items, state: std::marker::PhantomData, } } } impl Order<Confirmed> { fn process(&self) { println!("Processing order: {:?}", self.items); } } // 5. Dependency Injection trait Logger { fn log(&self, message: &str); } struct ConsoleLogger; impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("{}", message); } } struct Service<T: Logger> { logger: T, } impl<T: Logger> Service<T> { fn new(logger: T) -> Self { Service { logger } } fn do_work(&self) { self.logger.log("Working..."); } } Q28: How do you design error types in a library?
Answer:
use std::fmt; use std::error::Error; // 1. Define custom error enum #[derive(Debug)] enum LibraryError { Network(String), Parse { kind: String, line: usize }, Io(std::io::Error), Timeout, #[cfg(feature = "debug")] Internal(String), } // 2. Implement Display impl fmt::Display for LibraryError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LibraryError::Network(msg) => write!(f, "Network error: {}", msg), LibraryError::Parse { kind, line } => { write!(f, "Parse error at line {}: {}", line, kind) } LibraryError::Io(e) => write!(f, "IO error: {}", e), LibraryError::Timeout => write!(f, "Operation timed out"), #[cfg(feature = "debug")] LibraryError::Internal(msg) => write!(f, "Internal error: {}", msg), } } } // 3. Implement Error trait impl Error for LibraryError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { LibraryError::Io(e) => Some(e), _ => None, } } } // 4. Provide From conversions impl From<std::io::Error> for LibraryError { fn from(error: std::io::Error) -> Self { LibraryError::Io(error) } } impl From<std::num::ParseIntError> for LibraryError { fn from(error: std::num::ParseIntError) -> Self { LibraryError::Parse { kind: error.to_string(), line: 0, } } } // 5. Create type alias type Result<T> = std::result::Result<T, LibraryError>; // 6. Usage in library fn read_config(path: &str) -> Result<String> { let content = std::fs::read_to_string(path)?; // Automatically converts if content.is_empty() { return Err(LibraryError::Network("Empty response".to_string())); } Ok(content) } // 7. For callers fn main() { match read_config("config.txt") { Ok(data) => println!("Config: {}", data), Err(e) => { eprintln!("Error: {}", e); if let Some(source) = e.source() { eprintln!("Caused by: {}", source); } } } } Interview Tips Summary
Key Areas to Master:
- Ownership & Borrowing: Understand move semantics, borrowing rules, and when to use each
- Lifetimes: Know elision rules, struct lifetimes, and common patterns
- Concurrency: Send/Sync traits, thread safety, async/await
- Error Handling: Result, Option, custom error types
- Performance: Zero-cost abstractions, memory layout, optimization techniques
- Patterns: Builder, RAII, Newtype, Type State
- Unsafe: When and why, invariants to maintain
- Macros: Declarative vs procedural, common use cases
- Testing: Unit tests, integration tests, doc tests
- Ecosystem: Common crates, tooling, FFI
Common Interview Questions to Expect:
- "Explain how ownership works with an example"
- "What's the difference between String and &str?"
- "How do you share data between threads safely?"
- "What are zero-cost abstractions?"
- "How would you design a thread pool?"
- "Explain the difference between
RcandArc" - "When would you use
unsafecode?" - "How do you handle errors in a library?"
- "What's the difference between
async fnandimpl Future?" - "How does Rust prevent data races?"