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?"