Rust Interview Preparation: Commonly Asked Questions – Set 4

Table of Contents

Introduction

This comprehensive guide covers advanced Rust interview questions focusing on concurrency, async programming, macros, unsafe Rust, and performance optimization. These questions are designed for mid-level to senior Rust developer positions.

Table of Contents

  1. Concurrency and Parallelism
  2. Async Programming
  3. Macros and Metaprogramming
  4. Unsafe Rust
  5. Performance Optimization
  6. Advanced Trait Patterns
  7. System Programming
  8. WebAssembly
  9. Embedded Rust
  10. Interview Tips and Scenarios

1. Concurrency and Parallelism

Q1: Explain the difference between threads, async tasks, and processes in Rust.

Answer:

use std::thread;
use std::time::Duration;
use tokio::task;
// 1. OS Threads - 1:1 threading model
fn thread_example() {
let handle = thread::spawn(|| {
for i in 1..5 {
println!("Thread: {}", i);
thread::sleep(Duration::from_millis(100));
}
});
for i in 1..5 {
println!("Main: {}", i);
thread::sleep(Duration::from_millis(100));
}
handle.join().unwrap();
}
// 2. Async Tasks - Green threads / M:N threading
async fn async_example() {
let task1 = task::spawn(async {
for i in 1..5 {
println!("Task 1: {}", i);
tokio::time::sleep(Duration::from_millis(100)).await;
}
});
let task2 = task::spawn(async {
for i in 1..5 {
println!("Task 2: {}", i);
tokio::time::sleep(Duration::from_millis(50)).await;
}
});
let _ = tokio::join!(task1, task2);
}
// 3. Processes - Separate OS processes
use std::process::Command;
fn process_example() {
let output = Command::new("ls")
.arg("-l")
.output()
.expect("Failed to execute command");
println!("Output: {}", String::from_utf8_lossy(&output.stdout));
}

Q2: How do you prevent data races in concurrent Rust code?

Answer:

use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicUsize, Ordering}};
use std::thread;
// 1. Using Mutex for exclusive access
fn mutex_example() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
// 2. Using RwLock for read/write separation
fn rwlock_example() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Readers
for _ in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let read = data.read().unwrap();
println!("Read: {:?}", *read);
}));
}
// Writer
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut write = data.write().unwrap();
write.push(4);
println!("Wrote: {:?}", *write);
}));
for handle in handles {
handle.join().unwrap();
}
}
// 3. Using atomic types for simple operations
fn atomic_example() {
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", counter.load(Ordering::SeqCst));
}

Q3: Explain the difference between Send and Sync traits.

Answer:

use std::thread;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
// Send: Type can be transferred across threads
// Sync: Type can be safely referenced across threads
struct MyStruct {
data: i32,
}
// Most types are automatically Send + Sync
// 1. Types that are !Send
struct NotSend {
data: Rc<i32>, // Rc is !Send
}
// 2. Types that are !Sync
struct NotSync {
data: std::cell::RefCell<i32>, // RefCell is !Sync
}
// 3. Custom implementation (rarely needed)
unsafe impl Send for MyStruct {} // Only if you're sure it's safe
unsafe impl Sync for MyStruct {} // Only if you're sure it's safe
fn main() {
// Send example
let data = Arc::new(5); // Arc is Send
let handle = thread::spawn(move || {
println!("Data: {}", data);
});
handle.join().unwrap();
// Sync example
let data = Arc::new(Mutex::new(5)); // Mutex is Sync
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut val = data_clone.lock().unwrap();
*val += 1;
});
handle.join().unwrap();
println!("Value: {}", *data.lock().unwrap());
}

Q4: Implement a thread-safe work queue with multiple producers and consumers.

Answer:

use std::sync::{Arc, Mutex, Condvar};
use std::collections::VecDeque;
use std::thread;
use std::time::Duration;
struct WorkQueue<T> {
queue: Mutex<VecDeque<T>>,
not_empty: Condvar,
}
impl<T> WorkQueue<T> {
fn new() -> Self {
WorkQueue {
queue: Mutex::new(VecDeque::new()),
not_empty: Condvar::new(),
}
}
fn push(&self, item: T) {
let mut queue = self.queue.lock().unwrap();
queue.push_back(item);
self.not_empty.notify_one(); // Wake up one waiting consumer
}
fn pop(&self) -> T {
let mut queue = self.queue.lock().unwrap();
// Wait until queue is not empty
while queue.is_empty() {
queue = self.not_empty.wait(queue).unwrap();
}
queue.pop_front().unwrap()
}
fn try_pop(&self) -> Option<T> {
let mut queue = self.queue.lock().unwrap();
queue.pop_front()
}
fn len(&self) -> usize {
self.queue.lock().unwrap().len()
}
}
// Multiple producers and consumers example
fn main() {
let queue = Arc::new(WorkQueue::new());
let mut handles = vec![];
// Producers
for i in 0..3 {
let queue = Arc::clone(&queue);
handles.push(thread::spawn(move || {
for j in 0..5 {
queue.push(format!("Producer {}-{}", i, j));
thread::sleep(Duration::from_millis(10));
}
}));
}
// Consumers
for i in 0..4 {
let queue = Arc::clone(&queue);
handles.push(thread::spawn(move || {
loop {
let item = queue.pop();
println!("Consumer {} got: {}", i, item);
thread::sleep(Duration::from_millis(20));
// Stop after processing enough items
if queue.len() < 2 {
break;
}
}
}));
}
for handle in handles {
handle.join().unwrap();
}
}

2. Async Programming

Q5: Explain the async/await model in Rust. How is it different from other languages?

Answer:

use tokio::time::{sleep, Duration};
use futures::future::join_all;
// Rust's async is zero-cost and doesn't require a runtime
// but most practical applications use tokio or async-std
// 1. Basic async function
async fn fetch_data(id: u32) -> String {
println!("Fetching data {}", id);
sleep(Duration::from_millis(100)).await;
format!("Data for {}", id)
}
// 2. Concurrent execution
async fn concurrent_fetches() {
let fetches: Vec<_> = (1..=5)
.map(|id| fetch_data(id))
.collect();
let results = join_all(fetches).await;
println!("Results: {:?}", results);
}
// 3. Async with select!
use tokio::select;
async fn select_example() {
let task1 = fetch_data(1);
let task2 = fetch_data(2);
select! {
result = task1 => println!("Task 1 completed: {}", result),
result = task2 => println!("Task 2 completed: {}", result),
}
}
// 4. Understanding Future
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct MyFuture {
state: u32,
}
impl Future for MyFuture {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.state == 5 {
Poll::Ready(self.state)
} else {
self.state += 1;
Poll::Pending
}
}
}
#[tokio::main]
async fn main() {
concurrent_fetches().await;
select_example().await;
// Using custom future
let my_future = MyFuture { state: 0 };
let result = my_future.await;
println!("Custom future result: {}", result);
}

Q6: What are the differences between tokio and async-std?

Answer:

// tokio (more mature, production-ready)
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn tokio_server() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, addr) = listener.accept().await?;
println!("Connection from: {}", addr);
tokio::spawn(async move {
let mut buf = [0; 1024];
let n = socket.read(&mut buf).await.unwrap();
socket.write_all(&buf[0..n]).await.unwrap();
});
}
}
// async-std (simpler, closer to std)
use async_std::net::TcpListener;
use async_std::prelude::*;
async fn async_std_server() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8081").await?;
while let Ok((stream, addr)) = listener.accept().await {
println!("Connection from: {}", addr);
async_std::task::spawn(async move {
// Handle connection
});
}
Ok(())
}
// Key differences:
// 1. Runtime: tokio uses work-stealing, async-std uses per-task
// 2. API: async-std mimics std, tokio has its own patterns
// 3. Ecosystem: tokio has larger ecosystem
// 4. Performance: tokio often faster for high-concurrency

Q7: Explain cancellation safety in async Rust.

Answer:

use tokio::time::{sleep, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
// Cancellation-safe operations can be dropped without causing issues
// 1. Non-cancellation-safe operation
async fn unsafe_operation() -> String {
// This might leave state inconsistent if cancelled
let mut data = vec![];
for i in 0..100 {
data.push(i);
sleep(Duration::from_millis(10)).await; // Cancellation point
}
format!("Data length: {}", data.len())
}
// 2. Making it cancellation-safe with a guard
struct OperationGuard {
data: Vec<i32>,
completed: Arc<AtomicBool>,
}
impl OperationGuard {
fn new(completed: Arc<AtomicBool>) -> Self {
OperationGuard {
data: Vec::with_capacity(100),
completed,
}
}
async fn run(&mut self) -> String {
for i in 0..100 {
if self.completed.load(Ordering::SeqCst) {
return "Cancelled".to_string();
}
self.data.push(i);
sleep(Duration::from_millis(10)).await;
}
format!("Data length: {}", self.data.len())
}
}
// 3. Using tokio's cancellation-safe primitives
use tokio::sync::oneshot;
async fn safe_with_channel() -> Result<String, &'static str> {
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
// Do work
sleep(Duration::from_millis(100)).await;
tx.send("Done").unwrap();
});
// This select is cancellation-safe
tokio::select! {
result = rx => Ok(result.unwrap()),
_ = sleep(Duration::from_millis(50)) => {
Err("Timeout")
}
}
}
// 4. Using structured concurrency
async fn structured_operation() -> String {
let completed = Arc::new(AtomicBool::new(false));
let mut guard = OperationGuard::new(Arc::clone(&completed));
let handle = tokio::spawn(async move {
guard.run().await
});
// Let it run for a while then cancel
sleep(Duration::from_millis(500)).await;
completed.store(true, Ordering::SeqCst);
handle.await.unwrap()
}

3. Macros and Metaprogramming

Q8: Explain the difference between declarative and procedural macros.

Answer:

// 1. Declarative macros (macro_rules!)
macro_rules! vec_of_strings {
($($x:expr),*) => {
vec![$($x.to_string()),*]
};
}
// 2. Procedural macros (more powerful, custom syntax)
// These are defined in separate crates
// #[derive] macro example
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
age: u8,
}
// Attribute-like macro
// #[route(GET, "/path")]
fn route_handler() {}
// Function-like macro
// sql!(SELECT * FROM users WHERE id = 1);
// Example of custom derive macro (conceptual)
// In a separate crate:
// use proc_macro::TokenStream;
// 
// #[proc_macro_derive(MyTrait)]
// pub fn my_trait_derive(input: TokenStream) -> TokenStream {
//     // Parse input and generate implementation
// }
// Usage:
trait Hello {
fn hello(&self) -> String;
}
// Macro to implement Hello for structs
macro_rules! impl_hello {
($type:ty) => {
impl Hello for $type {
fn hello(&self) -> String {
format!("Hello from {}", stringify!($type))
}
}
};
}
struct Person {
name: String,
}
impl_hello!(Person);
fn main() {
let strings = vec_of_strings!("a", "b", "c");
println!("{:?}", strings);
let person = Person { name: "Alice".to_string() };
println!("{}", person.hello());
}

Q9: Write a macro that implements a DSL for simple HTTP routes.

Answer:

use std::collections::HashMap;
// Macro to define routes
macro_rules! routes {
($($method:ident $path:literal => $handler:expr),* $(,)?) => {
{
let mut router = Router::new();
$(
router.add_route(stringify!($method), $path, Box::new($handler));
)*
router
}
};
}
type Handler = Box<dyn Fn(&str) -> String>;
struct Router {
routes: HashMap<String, HashMap<String, Handler>>,
}
impl Router {
fn new() -> Self {
Router {
routes: HashMap::new(),
}
}
fn add_route(&mut self, method: &str, path: &str, handler: Handler) {
self.routes
.entry(method.to_string())
.or_insert_with(HashMap::new)
.insert(path.to_string(), handler);
}
fn handle(&self, method: &str, path: &str, body: &str) -> Option<String> {
self.routes
.get(method)
.and_then(|routes| routes.get(path))
.map(|handler| handler(body))
}
}
// Usage
fn main() {
let router = routes! {
GET "/" => |_| "Hello, World!".to_string(),
POST "/users" => |body| format!("Creating user: {}", body),
GET "/users/:id" => |id| format!("Getting user: {}", id),
};
// Test the router
if let Some(response) = router.handle("GET", "/", "") {
println!("GET /: {}", response);
}
if let Some(response) = router.handle("POST", "/users", "{\"name\":\"Alice\"}") {
println!("POST /users: {}", response);
}
if let Some(response) = router.handle("GET", "/users/42", "") {
println!("GET /users/42: {}", response);
}
}

Q10: Explain hygiene in Rust macros.

Answer:

// Macro hygiene prevents name clashes between macro and caller scope
// 1. Unhygienic (C preprocessor style) would cause issues
// Rust macros are hygienic
macro_rules! create_var {
($name:ident, $value:expr) => {
let $name = $value;
println!("Inside macro: {}", $name);
};
}
fn hygiene_example() {
let x = 10;
// This won't conflict with outer x
create_var!(x, 5);
println!("Outside macro: {}", x); // Still prints 10
}
// 2. Using $crate for absolute paths
#[macro_export]
macro_rules! log_error {
($msg:expr) => {
// Use $crate to refer to the crate's module
$crate::println!("Error: {}", $msg);
};
}
// 3. Hygiene with multiple scopes
macro_rules! with_temp {
($body:block) => {
{
let temp = 42;
$body
}
};
}
fn multi_scope() {
let temp = 100;
// Inner temp doesn't affect outer
let result = with_temp!({
println!("Inside: {}", temp); // Prints 42
temp
});
println!("Outside: {}, result: {}", temp, result); // Prints 100, 42
}
// 4. Breaking hygiene when needed
macro_rules! capture_ident {
($name:ident) => {
// This will capture identifier from caller
let $name = 5;
};
}
fn capture_example() {
// This would shadow outer variable intentionally
capture_ident!(x); // Declares x in this scope
println!("x = {}", x); // Prints 5
}
fn main() {
hygiene_example();
multi_scope();
capture_example();
}

4. Unsafe Rust

Q11: When would you need to use unsafe Rust?

Answer:

// Unsafe Rust is needed for:
// 1. Dereferencing raw pointers
// 2. Calling unsafe functions
// 3. Implementing unsafe traits
// 4. Accessing/modifying mutable statics
// 1. FFI (Foreign Function Interface)
extern "C" {
fn abs(input: i32) -> i32;
fn malloc(size: usize) -> *mut u8;
fn free(ptr: *mut u8);
}
fn ffi_example() {
unsafe {
println!("Absolute value of -3: {}", abs(-3));
let ptr = malloc(1024);
if !ptr.is_null() {
// Use the memory
*ptr = 42;
println!("First byte: {}", *ptr);
free(ptr);
}
}
}
// 2. Performance optimizations
fn fast_copy(src: &[u8], dst: &mut [u8]) {
assert!(src.len() <= dst.len());
unsafe {
// Bypass bounds checking for speed
std::ptr::copy_nonoverlapping(
src.as_ptr(),
dst.as_mut_ptr(),
src.len()
);
}
}
// 3. Implementing data structures
struct MyVec<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> MyVec<T> {
fn new() -> Self {
MyVec {
ptr: std::ptr::null_mut(),
len: 0,
capacity: 0,
}
}
fn push(&mut self, value: T) {
if self.len == self.capacity {
self.grow();
}
unsafe {
std::ptr::write(self.ptr.add(self.len), value);
self.len += 1;
}
}
fn grow(&mut self) {
let new_capacity = (self.capacity * 2).max(1);
let new_ptr = unsafe {
let layout = std::alloc::Layout::array::<T>(new_capacity).unwrap();
if self.capacity == 0 {
std::alloc::alloc(layout) as *mut T
} else {
let old_layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
std::alloc::realloc(
self.ptr as *mut u8,
old_layout,
new_capacity * std::mem::size_of::<T>()
) as *mut T
}
};
self.ptr = new_ptr;
self.capacity = new_capacity;
}
}
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
if self.capacity > 0 {
unsafe {
// Drop all elements
for i in 0..self.len {
std::ptr::drop_in_place(self.ptr.add(i));
}
// Deallocate memory
let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
std::alloc::dealloc(self.ptr as *mut u8, layout);
}
}
}
}

Q12: Explain the rules for using unsafe code.

Answer:

// The 5 unsafe superpowers:
// 1. Dereference a raw pointer
// 2. Call an unsafe function
// 3. Implement an unsafe trait
// 4. Access/modify a mutable static
// 5. Access fields of unions
// Rule 1: Raw pointers must be valid
fn raw_pointer_rules() {
let mut x = 10;
let ptr = &mut x as *mut i32;
unsafe {
// Pointer must be:
// - Non-null
// - Properly aligned
// - Dereferenceable
// - Not aliasing in prohibited ways
*ptr = 20;
}
// WRONG: Dangling pointer
// let dangling;
// {
//     let y = 5;
//     dangling = &y as *const i32;
// }
// unsafe { println!("{}", *dangling); } // Undefined behavior!
}
// Rule 2: Unsafe functions must document safety requirements
/// # Safety
/// `ptr` must be:
/// - Non-null
/// - Properly aligned for T
/// - Point to valid memory for the entire lifetime 'a
unsafe fn dangerous<T>(ptr: *const T) -> &'a T {
&*ptr
}
// Rule 3: Unsafe traits must document invariants
/// # Safety
/// Implementer must ensure that the type is actually `Send`
unsafe trait MySend: Send { }
// Rule 4: Mutable statics require unsafe
static mut COUNTER: u32 = 0;
fn mutable_static() {
unsafe {
COUNTER += 1;
println!("Counter: {}", COUNTER);
}
}
// Rule 5: Union access requires unsafe
union IntOrFloat {
i: i32,
f: f32,
}
fn union_example() {
let u = IntOrFloat { i: 42 };
unsafe {
println!("As int: {}", u.i);
println!("As float: {}", u.f); // Undefined behavior! (not initialized as float)
}
}

Q13: Implement a safe abstraction over unsafe code.

Answer:

use std::ptr::NonNull;
use std::marker::PhantomData;
// Safe wrapper around raw pointer
struct MyBox<T> {
ptr: NonNull<T>,
_marker: PhantomData<T>,
}
impl<T> MyBox<T> {
fn new(value: T) -> Self {
// Allocate memory
let layout = std::alloc::Layout::new::<T>();
let ptr = unsafe {
let ptr = std::alloc::alloc(layout) as *mut T;
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
ptr.write(value);
NonNull::new_unchecked(ptr)
};
MyBox {
ptr,
_marker: PhantomData,
}
}
fn as_ref(&self) -> &T {
unsafe { self.ptr.as_ref() }
}
fn as_mut(&mut self) -> &mut T {
unsafe { self.ptr.as_mut() }
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
// Drop the value
std::ptr::drop_in_place(self.ptr.as_ptr());
// Free the memory
let layout = std::alloc::Layout::new::<T>();
std::alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
}
}
}
// Safe wrapper for a simple lock
struct SpinLock<T> {
locked: std::sync::atomic::AtomicBool,
data: std::cell::UnsafeCell<T>,
}
impl<T> SpinLock<T> {
fn new(data: T) -> Self {
SpinLock {
locked: std::sync::atomic::AtomicBool::new(false),
data: std::cell::UnsafeCell::new(data),
}
}
fn lock(&self) -> SpinLockGuard<'_, T> {
while self.locked.swap(true, std::sync::atomic::Ordering::Acquire) {
std::hint::spin_loop();
}
SpinLockGuard {
lock: self,
}
}
}
struct SpinLockGuard<'a, T> {
lock: &'a SpinLock<T>,
}
impl<'a, T> Drop for SpinLockGuard<'a, T> {
fn drop(&mut self) {
self.lock.locked.store(false, std::sync::atomic::Ordering::Release);
}
}
impl<'a, T> std::ops::Deref for SpinLockGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<'a, T> std::ops::DerefMut for SpinLockGuard<'a, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
fn main() {
// Using safe MyBox
let mut boxed = MyBox::new(42);
println!("Value: {}", boxed.as_ref());
*boxed.as_mut() = 100;
println!("New value: {}", boxed.as_ref());
// Using safe SpinLock
let lock = SpinLock::new(5);
std::thread::scope(|s| {
s.spawn(|| {
let mut guard = lock.lock();
*guard += 1;
});
s.spawn(|| {
let mut guard = lock.lock();
*guard += 2;
});
});
println!("Final value: {}", *lock.lock());
}

5. Performance Optimization

Q14: How do you profile and optimize Rust code?

Answer:

use std::time::Instant;
use std::collections::HashMap;
// 1. Benchmarking with std::time
fn time_function<F, T>(f: F) -> (T, std::time::Duration)
where
F: FnOnce() -> T,
{
let start = Instant::now();
let result = f();
let duration = start.elapsed();
(result, duration)
}
// 2. Using criterion for benchmarks
// In Cargo.toml:
// [dev-dependencies]
// criterion = "0.5"
// 
// [[bench]]
// name = "my_benchmark"
// harness = false
// 3. Common optimizations
struct Optimizer {
data: Vec<i32>,
}
impl Optimizer {
// Bad: Repeated allocations
fn bad_sum_of_squares(&self) -> i32 {
let mut squares = Vec::new();
for &x in &self.data {
squares.push(x * x);
}
squares.iter().sum()
}
// Good: No intermediate allocation
fn good_sum_of_squares(&self) -> i32 {
self.data.iter().map(|&x| x * x).sum()
}
// Bad: Bounds checking on every access
fn bad_binary_search(&self, target: i32) -> Option<usize> {
let mut low = 0;
let mut high = self.data.len() - 1;
while low <= high {
let mid = (low + high) / 2;
if self.data[mid] == target {
return Some(mid);
} else if self.data[mid] < target {
low = mid + 1;
} else {
high = mid - 1;
}
}
None
}
// Good: Using slice pattern to avoid bounds checks
fn good_binary_search(&self, target: i32) -> Option<usize> {
self.data.binary_search(&target).ok()
}
}
// 4. Cache-friendly data structures
#[repr(C)]
struct CacheOptimized {
a: i32,
b: i32,
c: i32,
d: i32,
}
// 5. Using const generics for compile-time optimization
fn process_array<const N: usize>(arr: [i32; N]) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += arr[i];
}
sum
}
// 6. Avoiding clones
#[derive(Clone)]
struct ExpensiveData {
data: Vec<u8>,
}
fn process_data(data: &ExpensiveData) { // Use reference
println!("Processing {} bytes", data.data.len());
}
fn main() {
// Benchmarking
let data = vec![1, 2, 3, 4, 5];
let optimizer = Optimizer { data };
let (result, time) = time_function(|| optimizer.good_sum_of_squares());
println!("Sum: {}, took {:?}", result, time);
// Memory layout optimization
println!("Size of CacheOptimized: {} bytes", 
std::mem::size_of::<CacheOptimized>());
println!("Alignment: {} bytes", 
std::mem::align_of::<CacheOptimized>());
// Const generic
let arr = [1, 2, 3, 4, 5];
println!("Sum: {}", process_array(arr));
// Avoid clones
let expensive = ExpensiveData { data: vec![0; 1000] };
process_data(&expensive); // Borrow instead of clone
println!("Still have data: {} bytes", expensive.data.len());
}

Q15: Explain zero-cost abstractions in Rust.

Answer:

// Zero-cost abstractions mean high-level code compiles to
// equivalent low-level code with no runtime overhead
// 1. Iterators vs manual loops
fn iterator_example(data: &[i32]) -> i32 {
// High-level iterator chain
data.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.sum()
}
fn manual_loop_example(data: &[i32]) -> i32 {
// Equivalent manual loop (same performance)
let mut sum = 0;
for &x in data {
if x % 2 == 0 {
sum += x * x;
}
}
sum
}
// 2. Generic functions
fn generic_min<T: Ord>(a: T, b: T) -> T {
if a < b { a } else { b }
}
// Compiles to specialized version for each type
// 3. Closures
fn closure_example() {
let add_one = |x: i32| x + 1;
let result = add_one(5);
// Closure is inlined, same as writing: 5 + 1
}
// 4. RAII (Resource Acquisition Is Initialization)
struct Guard {
data: String,
}
impl Drop for Guard {
fn drop(&mut self) {
println!("Cleaning up: {}", self.data);
}
}
// Drop is called automatically, zero overhead compared to manual cleanup
// 5. Pattern matching
fn pattern_match(x: Option<i32>) -> i32 {
match x {
Some(v) => v,
None => 0,
}
}
// Compiles to efficient conditional check
// 6. Zero-sized types
struct Empty; // Takes no space
struct Wrapper<T>(T); // Same size as T
fn zero_sized_example() {
println!("Size of Empty: {} bytes", std::mem::size_of::<Empty>());
println!("Size of Wrapper<Empty>: {} bytes", std::mem::size_of::<Wrapper<Empty>>());
}
// 7. Enum optimization
enum Option<T> {
Some(T),
None,
}
// Option<&T> uses pointer null optimization - zero overhead
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// These two compile to nearly identical assembly
let iter_sum = iterator_example(&data);
let loop_sum = manual_loop_example(&data);
println!("Iterator: {}, Manual: {}", iter_sum, loop_sum);
// Generic instantiation
println!("Min i32: {}", generic_min(5, 3));
println!("Min f64: {}", generic_min(5.0, 3.0));
zero_sized_example();
// Option optimization
let x: Option<&i32> = Some(&5);
println!("Size of Option<&i32>: {} bytes", std::mem::size_of_val(&x));
// Same size as a raw pointer!
}

6. Advanced Trait Patterns

Q16: Explain the orphan rule and how to work around it.

Answer:

use std::fmt;
// The orphan rule: You can't implement a foreign trait for a foreign type
// i.e., at least one of trait or type must be local to your crate
// Problem: Can't do this (in your crate)
// impl Display for Vec<String> { }
// Solutions:
// 1. Newtype pattern (wrapper)
struct MyVec(Vec<String>);
impl fmt::Display for MyVec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
// 2. Using a local trait (extension trait)
trait StringExt {
fn repeat_twice(&self) -> String;
}
impl StringExt for String {
fn repeat_twice(&self) -> String {
format!("{}{}", self, self)
}
}
// 3. Using blanket implementations with constraints
trait Printable {
fn print(&self);
}
// Blanket implementation for any type that implements Display
impl<T: fmt::Display> Printable for T {
fn print(&self) {
println!("{}", self);
}
}
// 4. Workaround for specific cases with marker traits
pub trait MyMarker {}
impl MyMarker for String {}
// Can implement foreign trait for types marked with MyMarker
// but not directly for foreign types
// 5. Using deref coercion (for smart pointers)
struct Wrapper<T>(T);
impl<T> std::ops::Deref for Wrapper<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Now Wrapper<String> can be used where &str is expected
// 6. Using associated types
trait Container {
type Item;
fn get(&self) -> &Self::Item;
}
impl<T> Container for Vec<T> {
type Item = T;
fn get(&self) -> &T {
&self[0]
}
}
fn main() {
// Newtype pattern
let my_vec = MyVec(vec!["hello".to_string(), "world".to_string()]);
println!("{}", my_vec);
// Extension trait
let s = "hello".to_string();
println!("{}", s.repeat_twice());
// Blanket implementation
42.print();
"hello".print();
// Deref coercion
let wrapped = Wrapper("hello".to_string());
takes_str(&wrapped); // Works due to deref coercion
}
fn takes_str(s: &str) {
println!("Got: {}", s);
}

Q17: Implement the Iterator trait for a custom type.

Answer:

// 1. Simple iterator
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count <= self.max {
Some(self.count)
} else {
None
}
}
}
// 2. Infinite iterator
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
Some(self.current)
}
}
// 3. Double-ended iterator
struct Range {
start: i32,
end: i32,
}
impl Iterator for Range {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.start <= self.end {
let result = self.start;
self.start += 1;
Some(result)
} else {
None
}
}
}
impl DoubleEndedIterator for Range {
fn next_back(&mut self) -> Option<Self::Item> {
if self.start <= self.end {
let result = self.end;
self.end -= 1;
Some(result)
} else {
None
}
}
}
// 4. Generic iterator with lifetime
struct Iter<'a, T> {
slice: &'a [T],
index: usize,
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.slice.len() {
let result = &self.slice[self.index];
self.index += 1;
Some(result)
} else {
None
}
}
}
// 5. Iterator that returns references to its own data
struct Buffer {
data: Vec<String>,
index: usize,
}
impl Buffer {
fn new(data: Vec<String>) -> Self {
Buffer { data, index: 0 }
}
}
impl Iterator for Buffer {
type Item = String; // Returns owned data
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.data.len() {
let result = self.data[self.index].clone();
self.index += 1;
Some(result)
} else {
None
}
}
}
// 6. Using iterator adapters
fn iterator_adapters() {
let counter = Counter::new(10);
let result: Vec<u32> = counter
.filter(|&x| x % 2 == 0)
.map(|x| x * x)
.collect();
println!("Squares of evens: {:?}", result);
}
fn main() {
// Basic counter
let mut counter = Counter::new(5);
while let Some(x) = counter.next() {
println!("Counter: {}", x);
}
// Fibonacci
let fib: Vec<u64> = Fibonacci::new().take(10).collect();
println!("Fibonacci: {:?}", fib);
// Double-ended iterator
let mut range = Range { start: 1, end: 5 };
println!("From front: {:?}", range.next());
println!("From back: {:?}", range.next_back());
// Generic iterator
let data = vec![1, 2, 3, 4];
let iter = Iter {
slice: &data,
index: 0,
};
for &x in iter {
println!("Value: {}", x);
}
iterator_adapters();
}

7. System Programming

Q18: How do you implement a memory allocator in Rust?

Answer:

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
// 1. Custom allocator that counts allocations
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// Count allocation
ALLOC_COUNT.fetch_add(1, Ordering::SeqCst);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::SeqCst);
// Delegate to system allocator
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// Count deallocation
DEALLOC_COUNT.fetch_add(1, Ordering::SeqCst);
System.dealloc(ptr, layout)
}
}
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
// 2. Simple bump allocator
struct BumpAllocator {
heap_start: usize,
heap_end: usize,
next: AtomicUsize,
}
impl BumpAllocator {
const fn new(heap_start: usize, heap_end: usize) -> Self {
BumpAllocator {
heap_start,
heap_end,
next: AtomicUsize::new(heap_start),
}
}
}
unsafe impl GlobalAlloc for BumpAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// Align the next pointer
let align = layout.align();
let size = layout.size();
let mut current = self.next.load(Ordering::SeqCst);
let aligned = (current + align - 1) & !(align - 1);
if aligned + size <= self.heap_end {
// Allocation fits
self.next.store(aligned + size, Ordering::SeqCst);
aligned as *mut u8
} else {
// Out of memory
std::ptr::null_mut()
}
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// Bump allocator never deallocates
}
}
// 3. Pool allocator for fixed-size objects
struct PoolAllocator {
chunks: Vec<*mut u8>,
free_list: Vec<*mut u8>,
chunk_size: usize,
object_size: usize,
}
impl PoolAllocator {
fn new(object_size: usize, chunk_size: usize) -> Self {
PoolAllocator {
chunks: Vec::new(),
free_list: Vec::new(),
chunk_size,
object_size,
}
}
fn allocate(&mut self) -> *mut u8 {
if let Some(ptr) = self.free_list.pop() {
return ptr;
}
// Allocate new chunk
let layout = Layout::array::<u8>(self.chunk_size).unwrap();
let chunk = unsafe { std::alloc::alloc(layout) };
if chunk.is_null() {
return std::ptr::null_mut();
}
self.chunks.push(chunk);
// Split chunk into objects
let objects = self.chunk_size / self.object_size;
for i in 0..objects {
let ptr = unsafe { chunk.add(i * self.object_size) };
self.free_list.push(ptr);
}
self.free_list.pop().unwrap()
}
fn deallocate(&mut self, ptr: *mut u8) {
self.free_list.push(ptr);
}
}
impl Drop for PoolAllocator {
fn drop(&mut self) {
let layout = Layout::array::<u8>(self.chunk_size).unwrap();
for &chunk in &self.chunks {
unsafe {
std::alloc::dealloc(chunk, layout);
}
}
}
}
fn main() {
// Test counting allocator
let v = vec![1, 2, 3, 4, 5];
println!("Allocations: {}", ALLOC_COUNT.load(Ordering::SeqCst));
println!("Bytes allocated: {}", ALLOC_BYTES.load(Ordering::SeqCst));
drop(v);
println!("Deallocations: {}", DEALLOC_COUNT.load(Ordering::SeqCst));
// Test pool allocator
let mut pool = PoolAllocator::new(32, 1024);
let ptr1 = pool.allocate();
let ptr2 = pool.allocate();
println!("Pool allocated: {:p}, {:p}", ptr1, ptr2);
pool.deallocate(ptr1);
let ptr3 = pool.allocate(); // Reuses ptr1
println!("Reused: {:p}", ptr3);
}

Q19: Explain how to interface with C code in Rust.

Answer:

use std::ffi::{CString, CStr};
use std::os::raw::{c_char, c_int, c_void};
// 1. Declaring external C functions
extern "C" {
fn puts(s: *const c_char) -> c_int;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
fn strlen(s: *const c_char) -> usize;
}
// 2. Creating C-compatible types
#[repr(C)]
struct Point {
x: c_int,
y: c_int,
}
// 3. Exporting Rust functions to C
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
a + b
}
#[no_mangle]
pub extern "C" fn rust_create_point(x: c_int, y: c_int) -> *mut Point {
let point = Box::new(Point { x, y });
Box::into_raw(point)
}
#[no_mangle]
pub extern "C" fn rust_destroy_point(point: *mut Point) {
if !point.is_null() {
unsafe {
drop(Box::from_raw(point));
}
}
}
// 4. Working with C strings
fn c_string_example() {
// Rust -> C
let rust_string = "Hello from Rust";
let c_string = CString::new(rust_string).unwrap();
unsafe {
puts(c_string.as_ptr());
}
// C -> Rust
let c_str_ptr = c_string.as_ptr();
let rust_str = unsafe {
CStr::from_ptr(c_str_ptr).to_str().unwrap()
};
println!("Converted back: {}", rust_str);
// Using strlen
unsafe {
let len = strlen(c_str_ptr);
println!("String length: {}", len);
}
}
// 5. Callbacks between Rust and C
type Callback = extern "C" fn(c_int) -> c_int;
extern "C" fn rust_callback(x: c_int) -> c_int {
println!("Rust callback called with: {}", x);
x * 2
}
extern "C" {
fn register_callback(cb: Callback);
fn trigger_callback(value: c_int) -> c_int;
}
// 6. Struct with callbacks
#[repr(C)]
struct CallbackHandler {
data: *mut c_void,
callback: extern "C" fn(*mut c_void, c_int) -> c_int,
}
extern "C" fn c_callback_handler(data: *mut c_void, value: c_int) -> c_int {
// Convert back to Rust reference
let handler = unsafe { &*(data as *mut CallbackHandler) };
(handler.callback)(handler.data, value)
}
// 7. FFI safety wrapper
struct SafeWrapper {
point: *mut Point,
}
impl SafeWrapper {
fn new(x: i32, y: i32) -> Self {
let point = unsafe {
let ptr = malloc(std::mem::size_of::<Point>()) as *mut Point;
(*ptr).x = x;
(*ptr).y = y;
ptr
};
SafeWrapper { point }
}
fn get(&self) -> (i32, i32) {
unsafe {
((*self.point).x, (*self.point).y)
}
}
}
impl Drop for SafeWrapper {
fn drop(&mut self) {
unsafe {
free(self.point as *mut c_void);
}
}
}
fn main() {
// Basic FFI
c_string_example();
// Using safe wrapper
let wrapper = SafeWrapper::new(10, 20);
println!("Point from C: {:?}", wrapper.get());
// Callback example (if C library supports)
unsafe {
register_callback(rust_callback);
let result = trigger_callback(42);
println!("Callback result: {}", result);
}
// External C function (if linking with C library)
// unsafe {
//     puts(CString::new("Calling C puts!").unwrap().as_ptr());
// }
}

8. WebAssembly

Q20: How do you compile Rust to WebAssembly and interact with JavaScript?

Answer:

// 1. Basic WASM function
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 2. Working with strings
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// 3. Returning complex objects
#[wasm_bindgen]
pub struct Point {
x: i32,
y: i32,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: i32, y: i32) -> Point {
Point { x, y }
}
#[wasm_bindgen(getter)]
pub fn x(&self) -> i32 {
self.x
}
#[wasm_bindgen(setter)]
pub fn set_x(&mut self, x: i32) {
self.x = x;
}
pub fn distance(&self, other: &Point) -> f64 {
let dx = (self.x - other.x) as f64;
let dy = (self.y - other.y) as f64;
(dx * dx + dy * dy).sqrt()
}
}
// 4. Working with arrays
#[wasm_bindgen]
pub fn sum_array(arr: &[i32]) -> i32 {
arr.iter().sum()
}
// 5. Returning arrays
#[wasm_bindgen]
pub fn create_array(len: usize) -> Vec<i32> {
(0..len as i32).collect()
}
// 6. Async functions
#[wasm_bindgen]
pub async fn fetch_data(url: String) -> Result<String, JsValue> {
let response = reqwest::get(&url)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let text = response
.text()
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(text)
}
// 7. Calling JavaScript from Rust
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;
type Date;
#[wasm_bindgen(static_method_of = Date)]
fn now() -> f64;
#[wasm_bindgen(js_namespace = document)]
fn getElementById(id: &str) -> Option<Element>;
type Element;
#[wasm_bindgen(method, js_name = innerHTML)]
fn set_inner_html(this: &Element, html: &str);
}
// 8. DOM manipulation
#[wasm_bindgen]
pub fn update_dom() -> Result<(), JsValue> {
if let Some(element) = getElementById("app") {
let timestamp = Date::now();
element.set_inner_html(&format!(
"Hello from Rust! Timestamp: {}",
timestamp
));
}
Ok(())
}
// 9. Working with closures
use wasm_bindgen::closure::Closure;
#[wasm_bindgen]
pub fn setup_click_handler() -> Result<(), JsValue> {
let window = web_sys::window().expect("no global window");
let document = window.document().expect("no document");
let button = document.get_element_by_id("my-button")
.expect("no button");
let closure = Closure::wrap(Box::new(move || {
log("Button clicked!");
}) as Box<dyn FnMut()>);
button
.dyn_ref::<web_sys::HtmlElement>()
.expect("not an element")
.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget(); // Prevent closure from being dropped
Ok(())
}
// 10. WebAssembly linear memory
#[wasm_bindgen]
pub fn memory_operations(ptr: *mut u8, len: usize) -> u8 {
unsafe {
let slice = std::slice::from_raw_parts(ptr, len);
slice.iter().sum()
}
}
// 11. Working with web_sys
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[wasm_bindgen]
pub fn draw_canvas() -> Result<(), JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id("canvas")
.unwrap()
.dyn_into::<HtmlCanvasElement>()?;
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
context.set_fill_style(&JsValue::from_str("red"));
context.fill_rect(10.0, 10.0, 100.0, 100.0);
Ok(())
}
// 12. Performance-sensitive code
#[wasm_bindgen]
pub fn process_pixels(data: &mut [u8]) {
for chunk in data.chunks_exact_mut(4) {
// Simple grayscale filter
let gray = (chunk[0] as u16 + chunk[1] as u16 + chunk[2] as u16) / 3;
chunk[0] = gray as u8;
chunk[1] = gray as u8;
chunk[2] = gray as u8;
}
}
// In JavaScript, you would use:
// import * as wasm from './pkg/your_crate.js';
// 
// async function init() {
//     await wasm.default();
//     
//     // Call Rust functions
//     console.log(wasm.add(5, 3));
//     console.log(wasm.greet("World"));
//     
//     // Create Rust objects
//     let point = new wasm.Point(10, 20);
//     console.log(point.distance(new wasm.Point(5, 5)));
//     
//     // Work with arrays
//     let arr = new Uint8Array([1, 2, 3, 4]);
//     let sum = wasm.memory_operations(arr.byteOffset, arr.length);
// }

9. Embedded Rust

Q21: How do you write Rust for embedded systems?

Answer:

#![no_std] // No standard library
#![no_main] // No main function
use core::panic::PanicInfo;
use cortex_m_rt::entry;
use cortex_m_semihosting::hprintln;
// 1. No_std environment
// No heap allocation, no threads, no file I/O
// 2. Panic handler for no_std
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
// 3. Entry point
#[entry]
fn main() -> ! {
hprintln!("Hello, embedded world!").unwrap();
// Main loop
loop {
// Do work
}
}
// 4. GPIO manipulation
use embedded_hal::digital::v2::{OutputPin, InputPin};
struct Led<P: OutputPin> {
pin: P,
}
impl<P: OutputPin> Led<P> {
fn new(pin: P) -> Self {
Led { pin }
}
fn on(&mut self) {
self.pin.set_high().ok();
}
fn off(&mut self) {
self.pin.set_low().ok();
}
fn toggle(&mut self) {
self.pin.toggle().ok();
}
}
// 5. Timer usage
use embedded_hal::blocking::delay::{DelayMs, DelayUs};
fn blink_led<D, P>(delay: &mut D, led: &mut Led<P>)
where
D: DelayMs<u32>,
P: OutputPin,
{
loop {
led.on();
delay.delay_ms(500u32);
led.off();
delay.delay_ms(500u32);
}
}
// 6. Reading sensors
use embedded_hal::adc::OneShot;
struct TemperatureSensor<ADC, PIN> {
adc: ADC,
pin: PIN,
}
impl<ADC, PIN, WORD> TemperatureSensor<ADC, PIN>
where
ADC: OneShot<PIN, WORD>,
{
fn new(adc: ADC, pin: PIN) -> Self {
TemperatureSensor { adc, pin }
}
fn read_temperature(&mut self) -> Result<f32, ADC::Error> {
let raw = self.adc.read(&mut self.pin)?;
// Convert raw ADC value to temperature
let voltage = (raw as f32) * 3.3 / 4096.0;
let temperature = (voltage - 0.5) * 100.0;
Ok(temperature)
}
}
// 7. Serial communication
use embedded_hal::serial::{Read, Write};
fn echo_serial<S>(serial: &mut S) -> Result<(), S::Error>
where
S: Read<u8> + Write<u8>,
{
while let Ok(byte) = serial.read() {
serial.write(byte)?;
serial.flush()?;
}
Ok(())
}
// 8. Interrupt handling
use cortex_m::interrupt::{self, Mutex};
use cortex_m::asm;
use core::cell::RefCell;
static SHARED: Mutex<RefCell<Option<Led<GPIO>>>> = Mutex::new(RefCell::new(None));
fn interrupt_handler() {
interrupt::free(|cs| {
if let Some(led) = SHARED.borrow(cs).borrow_mut().as_mut() {
led.toggle();
}
});
}
// 9. Memory-mapped registers
use volatile_register::{RW, RO};
#[repr(C)]
struct UartRegisters {
data: RW<u32>,     // Data register
status: RO<u32>,   // Status register
control: RW<u32>,  // Control register
}
const UART_BASE: usize = 0x4000_1000;
fn uart_example() {
let uart = unsafe { &*(UART_BASE as *const UartRegisters) };
// Read status
let status = uart.status.read();
// Write control
unsafe { uart.control.write(0x01) };
}
// 10. Fixed-point arithmetic (no FPU)
struct Fixed(i32);
impl Fixed {
const SCALE: i32 = 1000;
fn new(value: i32) -> Self {
Fixed(value * Self::SCALE)
}
fn from_raw(raw: i32) -> Self {
Fixed(raw)
}
fn as_i32(&self) -> i32 {
self.0 / Self::SCALE
}
fn as_f32(&self) -> f32 {
self.0 as f32 / Self::SCALE as f32
}
}
impl core::ops::Add for Fixed {
type Output = Self;
fn add(self, other: Self) -> Self {
Fixed(self.0 + other.0)
}
}
// 11. Device-specific configuration
#[repr(u8)]
enum ClockSource {
HSI = 0,
HSE = 1,
PLL = 2,
}
struct SystemConfig {
clock_source: ClockSource,
frequency: u32,
}
// 12. Real-time constraints
struct Deadline {
deadline_us: u32,
}
impl Deadline {
fn new(deadline_us: u32) -> Self {
Deadline { deadline_us }
}
fn is_met(&self, current_time_us: u32) -> bool {
current_time_us <= self.deadline_us
}
fn remaining_us(&self, current_time_us: u32) -> i32 {
self.deadline_us as i32 - current_time_us as i32
}
}
// 13. DMA transfers
struct DmaTransfer<T> {
source: *const T,
destination: *mut T,
count: usize,
}
impl<T> DmaTransfer<T> {
fn start(&self) {
// Configure DMA controller
// Start transfer
}
fn wait(&self) {
// Wait for transfer complete interrupt
}
}

10. Interview Tips and Scenarios

Q22: Common interview scenarios and how to approach them

Answer:

// Scenario 1: Design a thread-safe cache
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
struct Cache<K, V> {
inner: Arc<RwLock<HashMap<K, CacheEntry<V>>>>,
ttl: Duration,
}
struct CacheEntry<V> {
value: V,
expires: Instant,
}
impl<K: Eq + std::hash::Hash + Clone, V: Clone> Cache<K, V> {
fn new(ttl: Duration) -> Self {
Cache {
inner: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
fn get(&self, key: &K) -> Option<V> {
let inner = self.inner.read().unwrap();
if let Some(entry) = inner.get(key) {
if Instant::now() < entry.expires {
return Some(entry.value.clone());
}
}
None
}
fn set(&self, key: K, value: V) {
let mut inner = self.inner.write().unwrap();
inner.insert(key, CacheEntry {
value,
expires: Instant::now() + self.ttl,
});
}
fn cleanup(&self) {
let mut inner = self.inner.write().unwrap();
inner.retain(|_, entry| Instant::now() < entry.expires);
}
}
// Scenario 2: Implement a simple actor system
use std::sync::mpsc;
trait Actor {
type Message;
fn handle(&mut self, msg: Self::Message);
}
struct ActorHandle<Msg> {
sender: mpsc::Sender<Msg>,
}
impl<Msg> ActorHandle<Msg> {
fn send(&self, msg: Msg) -> Result<(), mpsc::SendError<Msg>> {
self.sender.send(msg)
}
}
fn spawn_actor<A, Msg>(mut actor: A) -> ActorHandle<Msg>
where
A: Actor<Message = Msg> + Send + 'static,
Msg: Send + 'static,
{
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for msg in rx {
actor.handle(msg);
}
});
ActorHandle { sender: tx }
}
// Example actor
struct CounterActor {
count: i32,
}
impl Actor for CounterActor {
type Message = CounterMessage;
fn handle(&mut self, msg: Self::Message) {
match msg {
CounterMessage::Increment => {
self.count += 1;
println!("Count: {}", self.count);
}
CounterMessage::Get => {
println!("Current count: {}", self.count);
}
}
}
}
enum CounterMessage {
Increment,
Get,
}
// Scenario 3: Implement a simple state machine
#[derive(Debug, PartialEq)]
enum ConnectionState {
Disconnected,
Connecting,
Connected,
Error,
}
struct Connection {
state: ConnectionState,
retries: u8,
}
impl Connection {
fn new() -> Self {
Connection {
state: ConnectionState::Disconnected,
retries: 0,
}
}
fn connect(&mut self) -> Result<(), &'static str> {
match self.state {
ConnectionState::Disconnected => {
self.state = ConnectionState::Connecting;
self.retries = 0;
Ok(())
}
_ => Err("Invalid state for connect"),
}
}
fn handle_event(&mut self, event: ConnectionEvent) {
match (&self.state, event) {
(ConnectionState::Connecting, ConnectionEvent::Connected) => {
self.state = ConnectionState::Connected;
self.retries = 0;
}
(ConnectionState::Connecting, ConnectionEvent::Failed) => {
if self.retries < 3 {
self.retries += 1;
} else {
self.state = ConnectionState::Error;
}
}
(ConnectionState::Connected, ConnectionEvent::Disconnected) => {
self.state = ConnectionState::Disconnected;
}
_ => {}
}
}
}
enum ConnectionEvent {
Connected,
Disconnected,
Failed,
}
// Scenario 4: Memory-efficient data structure
struct BitSet {
data: Vec<u64>,
size: usize,
}
impl BitSet {
fn new(size: usize) -> Self {
let num_words = (size + 63) / 64;
BitSet {
data: vec![0; num_words],
size,
}
}
fn set(&mut self, index: usize) {
if index < self.size {
let word = index / 64;
let bit = index % 64;
self.data[word] |= 1 << bit;
}
}
fn clear(&mut self, index: usize) {
if index < self.size {
let word = index / 64;
let bit = index % 64;
self.data[word] &= !(1 << bit);
}
}
fn contains(&self, index: usize) -> bool {
if index < self.size {
let word = index / 64;
let bit = index % 64;
(self.data[word] & (1 << bit)) != 0
} else {
false
}
}
fn iter(&self) -> BitSetIter {
BitSetIter {
bitset: self,
current: 0,
}
}
}
struct BitSetIter<'a> {
bitset: &'a BitSet,
current: usize,
}
impl<'a> Iterator for BitSetIter<'a> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
while self.current < self.bitset.size {
if self.bitset.contains(self.current) {
let result = self.current;
self.current += 1;
return Some(result);
}
self.current += 1;
}
None
}
}
// Scenario 5: Interview problem - Implement a rate limiter
use std::collections::VecDeque;
use std::time::{Duration, Instant};
struct RateLimiter {
max_requests: usize,
window: Duration,
requests: VecDeque<Instant>,
}
impl RateLimiter {
fn new(max_requests: usize, window: Duration) -> Self {
RateLimiter {
max_requests,
window,
requests: VecDeque::with_capacity(max_requests + 1),
}
}
fn allow(&mut self) -> bool {
let now = Instant::now();
// Remove old requests
while let Some(&time) = self.requests.front() {
if now - time > self.window {
self.requests.pop_front();
} else {
break;
}
}
// Check if under limit
if self.requests.len() < self.max_requests {
self.requests.push_back(now);
true
} else {
false
}
}
fn remaining(&self) -> usize {
self.max_requests - self.requests.len()
}
fn reset(&mut self) {
self.requests.clear();
}
}

Q23: Problem-Solving Approach

// When faced with a coding problem in an interview:
// 1. Understand the problem
// 2. Ask clarifying questions
// 3. Discuss edge cases
// 4. Outline your approach
// 5. Write code
// 6. Test with examples
// Example: Implement a function that finds the longest palindrome substring
fn longest_palindrome(s: &str) -> &str {
if s.is_empty() {
return "";
}
let bytes = s.as_bytes();
let mut start = 0;
let mut max_len = 1;
// Helper to expand around center
fn expand(bytes: &[u8], mut left: i32, mut right: i32) -> (usize, usize) {
while left >= 0 && right < bytes.len() as i32 && bytes[left as usize] == bytes[right as usize] {
left -= 1;
right += 1;
}
((left + 1) as usize, (right - left - 1) as usize)
}
for i in 0..bytes.len() {
// Odd length palindromes
let (odd_start, odd_len) = expand(bytes, i as i32 - 1, i as i32 + 1);
if odd_len > max_len {
max_len = odd_len;
start = odd_start;
}
// Even length palindromes
if i + 1 < bytes.len() {
let (even_start, even_len) = expand(bytes, i as i32, i as i32 + 1);
if even_len > max_len {
max_len = even_len;
start = even_start;
}
}
}
&s[start..start + max_len]
}
// Test cases
#[test]
fn test_longest_palindrome() {
assert_eq!(longest_palindrome("babad"), "bab");
assert_eq!(longest_palindrome("cbbd"), "bb");
assert_eq!(longest_palindrome("a"), "a");
assert_eq!(longest_palindrome("ac"), "a");
assert_eq!(longest_palindrome(""), "");
}

Q24: System Design with Rust

// Design a simple key-value store
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::Path;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Entry {
key: String,
value: Vec<u8>,
timestamp: u64,
}
struct KeyValueStore {
data: HashMap<String, Entry>,
wal: BufWriter<File>,
path: String,
}
impl KeyValueStore {
fn new(path: &str) -> Result<Self, std::io::Error> {
let mut store = KeyValueStore {
data: HashMap::new(),
wal: BufWriter::new(
OpenOptions::new()
.create(true)
.append(true)
.open(Path::new(path).with_extension("wal"))?
),
path: path.to_string(),
};
// Load from WAL on startup
store.load_from_wal()?;
Ok(store)
}
fn set(&mut self, key: String, value: Vec<u8>) -> Result<(), std::io::Error> {
let entry = Entry {
key: key.clone(),
value: value.clone(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
};
// Write to WAL first
let serialized = bincode::serialize(&entry).unwrap();
let len = serialized.len() as u32;
self.wal.write_all(&len.to_le_bytes())?;
self.wal.write_all(&serialized)?;
self.wal.flush()?;
// Update in-memory store
self.data.insert(key, entry);
Ok(())
}
fn get(&self, key: &str) -> Option<&Vec<u8>> {
self.data.get(key).map(|e| &e.value)
}
fn delete(&mut self, key: &str) -> Result<bool, std::io::Error> {
if self.data.remove(key).is_some() {
// Write tombstone to WAL
let entry = Entry {
key: key.to_string(),
value: Vec::new(),
timestamp: 0, // 0 timestamp indicates tombstone
};
let serialized = bincode::serialize(&entry).unwrap();
let len = serialized.len() as u32;
self.wal.write_all(&len.to_le_bytes())?;
self.wal.write_all(&serialized)?;
self.wal.flush()?;
Ok(true)
} else {
Ok(false)
}
}
fn load_from_wal(&mut self) -> Result<(), std::io::Error> {
let wal_path = Path::new(&self.path).with_extension("wal");
if !wal_path.exists() {
return Ok(());
}
let mut reader = BufReader::new(File::open(wal_path)?);
let mut buffer = Vec::new();
while let Ok(len_bytes) = reader.fill_buf() {
if len_bytes.is_empty() {
break;
}
if len_bytes.len() < 4 {
break;
}
let mut len_arr = [0u8; 4];
len_arr.copy_from_slice(&len_bytes[..4]);
let len = u32::from_le_bytes(len_arr) as usize;
reader.consume(4);
buffer.resize(len, 0);
reader.read_exact(&mut buffer)?;
if let Ok(entry) = bincode::deserialize::<Entry>(&buffer) {
if entry.timestamp == 0 {
// Tombstone
self.data.remove(&entry.key);
} else {
self.data.insert(entry.key.clone(), entry);
}
}
}
Ok(())
}
fn snapshot(&self) -> Result<(), std::io::Error> {
let snapshot_path = Path::new(&self.path).with_extension("snap");
let file = File::create(snapshot_path)?;
let mut writer = BufWriter::new(file);
for entry in self.data.values() {
let serialized = bincode::serialize(entry).unwrap();
let len = serialized.len() as u32;
writer.write_all(&len.to_le_bytes())?;
writer.write_all(&serialized)?;
}
writer.flush()?;
Ok(())
}
}

Conclusion

This comprehensive guide covers the most commonly asked advanced Rust interview questions. Key areas to focus on:

  1. Concurrency: Understand Send/Sync, atomics, mutexes, channels
  2. Async: Know the async model, futures, tokio vs async-std
  3. Unsafe: Be able to explain when and how to use unsafe code
  4. Performance: Understand zero-cost abstractions, profiling, optimization
  5. System Programming: FFI, memory management, embedded systems
  6. WebAssembly: Know the basics of wasm-bindgen and web-sys
  7. Design Patterns: Be able to implement common patterns in Rust

Final Tips

  • Practice coding on platforms like LeetCode using Rust
  • Read open-source Rust code to learn idiomatic patterns
  • Understand the borrow checker deeply - it's what makes Rust unique
  • Be prepared to discuss tradeoffs - no solution is perfect
  • Show your thought process during problem-solving
  • Ask clarifying questions before diving into code

Good luck with your Rust interview!

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper