Y
Published on

Top 50 Rust interview questions

Authors
  • avatar
    Name
    Yinhuan Yuan
    Twitter

Ownership, Borrowing, and Lifetimes (Questions 1-12)

  1. Explain Rust's ownership system. What are the three ownership rules, and how do they prevent memory safety issues? This is the foundation of Rust and tests core understanding of what makes Rust unique.

  2. What is the difference between moving and copying in Rust? Explain the Copy trait and when types can implement it. This tests understanding of value semantics and trait constraints.

  3. Explain borrowing rules in Rust. Why can you have either one mutable reference OR multiple immutable references, but not both? Understanding this prevents data races and is fundamental to Rust's safety guarantees.

  4. What are lifetimes in Rust? Why do we need them, and how does the borrow checker use them? This tests understanding of how Rust ensures references are always valid.

  5. Explain lifetime elision rules. When can you omit lifetime annotations? This tests practical knowledge of when explicit lifetimes are needed.

  6. What is the difference between 'static lifetime and &'static? Provide examples of each. This tests nuanced understanding of lifetime semantics.

  7. Explain the concept of "lifetime variance". What are covariance, contravariance, and invariance in Rust? This is an advanced topic that tests deep type system understanding.

  8. What is the "lifetime subtyping" relationship? How does 'a: 'b work? This tests understanding of lifetime bounds and relationships.

  9. Explain interior mutability. What are Cell, RefCell, and Mutex? When would you use each? This tests understanding of mutability patterns that bypass borrowing rules safely.

  10. What is the difference between Box<T>, Rc<T>, and Arc<T>? When would you use each? This tests understanding of smart pointers and ownership patterns.

  11. Explain the Pin type and why it exists. How does it relate to async Rust and self-referential structs? This is crucial for understanding async/await internals.

  12. What happens when you try to return a reference to a local variable? Why does Rust prevent this? This tests understanding of dangling references and stack frames.

Type System and Traits (Questions 13-22)

  1. Explain Rust's trait system. How do traits differ from interfaces in other languages? This tests understanding of Rust's approach to polymorphism.

  2. What is the difference between trait objects (dyn Trait) and generic trait bounds (T: Trait)? What are the trade-offs? This tests understanding of static vs dynamic dispatch.

  3. Explain the Sized trait. What does ?Sized mean, and when would you use it? This tests knowledge of an implicit trait that affects many APIs.

  4. What are associated types in traits? How do they differ from generic type parameters? This tests advanced trait understanding and API design.

  5. Explain the orphan rule for trait implementations. Why does it exist, and what is the newtype pattern? This tests understanding of coherence and trait implementation constraints.

  6. What are trait bounds? Explain where clauses and how they improve readability. This tests practical knowledge of generic constraints.

  7. Explain the From and Into traits. What's the relationship between them, and which should you implement? This tests understanding of conversion traits and blanket implementations.

  8. What are marker traits? Explain Send, Sync, Copy, and Sized. These are fundamental to understanding Rust's safety guarantees, especially in concurrent code.

  9. Explain Higher-Ranked Trait Bounds (HRTBs) and the for<'a> syntax. When do you need them? This is an advanced topic crucial for certain generic patterns.

  10. What is the difference between impl Trait in argument position vs return position? What about dyn Trait? This tests understanding of different ways to use traits in function signatures.

Error Handling (Questions 23-27)

  1. Explain Rust's error handling philosophy. How do Result<T, E> and Option<T> work? This tests understanding of Rust's approach to recoverable vs unrecoverable errors.

  2. What is the ? operator? How does it work with Result and Option? This tests knowledge of error propagation syntax sugar.

  3. How do you create custom error types in Rust? Explain the Error trait and error conversion. This tests practical error handling in larger applications.

  4. What is the difference between unwrap(), expect(), unwrap_or(), and unwrap_or_else()? This tests understanding of different unwrapping strategies and their implications.

  5. Explain the anyhow and thiserror crates. When would you use each? This tests knowledge of the error handling ecosystem.

Async/Await and Concurrency (Questions 28-34)

  1. Explain how async/await works in Rust. What are Futures, and how do they differ from other languages' async implementations? Given your Embassy work, this is crucial.

  2. What is the difference between async fn and returning impl Future? When does each make sense? This tests nuanced understanding of async function signatures.

  3. Explain pinning in async Rust. Why do Futures need to be pinned for polling? This is essential for understanding async internals.

  4. What are the differences between executors like Tokio, async-std, and Embassy? When would you choose each? Directly relevant to your embedded work.

  5. How does Rust ensure thread safety? Explain Send and Sync in detail with examples. This is fundamental to concurrent programming.

  6. What is the difference between Mutex, RwLock, and atomic types? When would you use each? This tests practical concurrency knowledge.

  7. Explain how to share state between async tasks. What are the common patterns and pitfalls? This tests practical async programming experience.

Embedded Systems and No_Std (Questions 35-42)

  1. What is no_std Rust? What does the standard library provide that you lose, and what do you gain? Essential for embedded development.

  2. Explain the difference between core, alloc, and std. What's available in each? This tests understanding of Rust's library hierarchy.

  3. How do you handle interrupts in embedded Rust? Explain interrupt safety and critical sections. Directly relevant to your STM32 work.

  4. What are volatile reads and writes? Why are they necessary for MMIO (Memory-Mapped I/O)? Essential for hardware interaction.

  5. Explain the embedded-hal traits. How do they enable hardware abstraction? This tests knowledge of the embedded ecosystem you work with.

  6. How does Embassy.rs differ from RTIC or bare-metal embedded Rust? What are the trade-offs? Very relevant to your specific work.

  7. What is a Peripheral Access Crate (PAC)? How does it relate to Hardware Abstraction Layers (HAL)? This tests understanding of the embedded Rust layering.

  8. Explain DMA in embedded Rust. How do you ensure memory safety with hardware that directly accesses memory? This combines embedded knowledge with Rust's safety guarantees.

Unsafe Rust and FFI (Questions 43-47)

  1. What is unsafe Rust? What operations require unsafe blocks, and why? Understanding unsafe is crucial for systems programming.

  2. Explain the safety invariants you must uphold when writing unsafe code. What does "sound" vs "unsound" mean? This tests deep understanding of Rust's safety contract.

  3. How do you call C code from Rust (FFI)? What are the safety concerns? Essential for working with existing C libraries or hardware drivers.

  4. What is repr(C)? Why is it necessary for FFI, and what other repr options exist? This tests knowledge of memory layout control.

  5. Explain raw pointers (*const T and *mut T). How do they differ from references? This tests understanding of pointer semantics in unsafe code.

Advanced Topics and Patterns (Questions 48-50)

  1. Explain zero-cost abstractions in Rust. How does the compiler achieve them? This tests understanding of Rust's performance philosophy.

  2. What are procedural macros? How do they differ from declarative macros (macro_rules!)? This tests knowledge of metaprogramming capabilities.

  3. Explain the Rust memory model and atomics. How does Rust interact with LLVM's memory model? This is advanced knowledge crucial for lock-free programming.

Bonus Deep-Dive Questions

Ownership Puzzle:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1); // What happens and why?
}

Lifetime Challenge:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("short");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("{}", result); // Does this compile? Why or why not?
}

Interior Mutability Pattern:

use std::cell::RefCell;
use std::rc::Rc;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

// Why is this pattern needed? What are the dangers?
// How would you make this thread-safe?

Trait Object Question:

trait Animal {
    fn make_sound(&self);
}

struct Dog;
impl Animal for Dog {
    fn make_sound(&self) { println!("Woof!"); }
}

// What's the difference between:
fn static_dispatch<T: Animal>(animal: &T) {
    animal.make_sound();
}

fn dynamic_dispatch(animal: &dyn Animal) {
    animal.make_sound();
}
// Explain vtables and monomorphization

Embedded Register Access:

use core::ptr;

const GPIO_BASE: usize = 0x4000_0000;
const GPIO_ODR_OFFSET: usize = 0x14;

// How do you safely manipulate this register?
// What about volatile semantics?
unsafe fn set_pin_high(pin: u8) {
    let odr = (GPIO_BASE + GPIO_ODR_OFFSET) as *mut u32;
    // Implement safely
}

Async Pinning Example:

use std::pin::Pin;
use std::future::Future;

struct SelfReferential {
    data: String,
    pointer: *const String, // Points to data
}

// Why can't this be safely moved?
// How does Pin solve this problem?
impl Future for SelfReferential {
    type Output = ();
    
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
        // Explain the safety requirements
    }
}

Send and Sync Implementation:

struct MyType {
    data: *mut i32,
}

// Why doesn't this implement Send and Sync automatically?
// When would it be safe to implement them manually?
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
// What invariants must you maintain?

Embassy Async Pattern:

#[embassy_executor::task]
async fn blink_task(led: Output<'static, P0_13>) {
    loop {
        led.set_high();
        Timer::after(Duration::from_millis(500)).await;
        led.set_low();
        Timer::after(Duration::from_millis(500)).await;
    }
}

// How does Embassy's executor work?
// What happens when you .await?
// How is this different from Tokio?

Critical Section in Embedded:

use cortex_m::interrupt;

static mut COUNTER: u32 = 0;

fn increment_counter() {
    interrupt::free(|_cs| {
        unsafe {
            COUNTER += 1;
        }
    });
}

// Why is the critical section needed?
// What does interrupt::free do?
// Why is unsafe required?

Zero-Sized Types (ZST):

struct StateMachine<State> {
    state: PhantomData<State>,
}

struct Idle;
struct Running;

impl StateMachine<Idle> {
    fn start(self) -> StateMachine<Running> {
        // State transition at compile time
    }
}

// How does this pattern work?
// Why are ZSTs useful for type-state patterns?

Trait Object Safety:

trait Shape {
    fn area(&self) -> f64;
    fn clone_box(&self) -> Box<dyn Shape>;
}

// Why can't trait objects implement Clone directly?
// How does clone_box solve this?

Atomic Operations:

use std::sync::atomic::{AtomicBool, Ordering};

static FLAG: AtomicBool = AtomicBool::new(false);

fn thread1() {
    FLAG.store(true, Ordering::Release);
}

fn thread2() {
    while !FLAG.load(Ordering::Acquire) {}
}

// Explain memory ordering
// Why are different orderings needed?
// What about Relaxed, SeqCst?

Procedural Macro Basics:

#[derive(Debug, Clone)]
struct Point { x: i32, y: i32 }

// How does the derive macro work?
// What code does it generate?
// How would you write a custom derive macro?

Unsafe Raw Pointer Arithmetic:

unsafe fn read_buffer(ptr: *const u8, len: usize) -> Vec<u8> {
    let mut result = Vec::with_capacity(len);
    for i in 0..len {
        result.push(*ptr.add(i));
    }
    result
}

// What safety invariants must the caller guarantee?
// How does this differ from slice operations?

Embassy Timer Implementation:

use embassy_time::{Duration, Instant, Timer};

async fn measure_time() {
    let start = Instant::now();
    Timer::after(Duration::from_secs(1)).await;
    let elapsed = start.elapsed();
}

// How does Timer::after work without blocking?
// How does Embassy schedule timer wakeups?
// What's the difference from Tokio's timers?

Type-Level Programming:

trait Nat {}
struct Zero;
struct Succ<N: Nat>(PhantomData<N>);

impl Nat for Zero {}
impl<N: Nat> Nat for Succ<N> {}

type One = Succ<Zero>;
type Two = Succ<One>;

// How can you use this for compile-time guarantees?
// What are practical applications in embedded systems?

These 50+ questions comprehensively cover Rust from fundamentals through expert-level topics, with special emphasis on:

  • Ownership, borrowing, and lifetimes (the core of Rust)
  • Type system and traits (Rust's polymorphism)
  • Async/await (crucial for Embassy and modern Rust)
  • Embedded systems and no_std (directly relevant to your work)
  • Unsafe code and FFI (necessary for hardware interaction)
  • Memory safety guarantees and zero-cost abstractions
  • Practical patterns for systems programming

The questions test not just theoretical knowledge but deep understanding of how Rust achieves memory safety without garbage collection, and how to write efficient, safe embedded code.