Lesson 5 — Rust: lifetimes, Box, Arc, dyn Trait
Question
Reading Reth source requires deeper Rust than dapp work. Lifetimes (<'a>), Box<dyn Trait>, Arc<dyn Trait + Send + Sync> — what they mean, when each is right.
Principle (minimum model)
- Lifetimes (
<'a>). Compile-time guarantee that references outlive what they point to. Most uses are inferred; rarely-written. Box<dyn Trait>. Owned trait object on the heap. Used when you need polymorphism but ownership.Arc<dyn Trait>. Shared trait object across tasks. Add+ Send + Syncfor Tokio. Most Reth components areArc<dyn ComponentTrait + Send + Sync>.&dyn Trait/&mut dyn Trait. Borrowed; cheaper than Box. Use when you don't need ownership.impl Trait(sugar). Sugar for "anonymous generic".fn foo() -> impl Future<...>returns a Future without naming the concrete type.- Generics vs
dyn Traittrade-off. Generics monomorphise (compile-time); each concrete type generates its own code.dyndispatches at runtime via vtable; smaller binary; slightly slower. - Reth conventions. Components passed as
Arc<dyn ... + Send + Sync>; per-call hot-path uses generics; mid-frequency usesdyn.
Worked example + steps
Rust: lifetimes, Box, Arc, dyn Trait
Open any ExEx source file. The first ten lines will throw Arc<>, 'static, dyn Trait, and Box<> at you. Four "advanced but actually simple" Rust features — and you need every one to read the code Reth ships with. This lesson tests you, not teaches you — if you stumble on the predict prompts, the gap is real and worth closing.
1. Lifetimes 'a
A lifetime annotation tells the compiler how long a borrow lives.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
'ais a label for "some lifetime"- Both inputs and the output share the same
'a→ "the returned reference lives at least as long as both inputs" - Often the compiler infers them — most signatures don't need explicit lifetimes
'static
'static means "lives for the entire program." String literals are &'static str:
let s: &'static str = "hello";
Long-running tasks (like ExEx) often require 'static bounds because they can outlive any local scope.
2. Box<T> — heap allocation
When you want a value on the heap rather than the stack, wrap it in Box<T>:
let boxed: Box<i64> = Box::new(42);
println!("{}", *boxed); // 42
Common reasons:
- Recursive types (linked lists) need a fixed-size pointer
- Holding a dynamically-sized value (
dyn Trait) - Move large values cheaply instead of copying
3. Rc<T> and Arc<T> — shared ownership
Rust's "one owner" rule sometimes gets in the way: you want multiple parts of the program to hold the same value.
| Type | Use |
|---|---|
Rc<T> | single-threaded reference counting |
Arc<T> | thread-safe (atomic reference counting) |
use std::sync::Arc;
let shared = Arc::new(String::from("hello"));
let clone1 = Arc::clone(&shared); // refcount += 1
let clone2 = Arc::clone(&shared); // refcount += 1
// Send to another thread — Arc is Send
std::thread::spawn(move || println!("{}", clone1));
Reth and ExEx code is full of Arc<...> — multiple async tasks need to read the same component.
4. Mutex / RwLock — shared mutability
Arc<T> alone is read-only. To mutate shared state, wrap in Mutex or RwLock:
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
std::thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
});
| Type | Use |
|---|---|
Mutex | exclusive read/write |
RwLock | many readers OR one writer |
5. dyn Trait — dynamic dispatch
A trait object — like a TypeScript / Java interface, but you opt into runtime method resolution explicitly.
trait Greet {
fn greet(&self);
}
struct En;
struct Ja;
impl Greet for En { fn greet(&self) { println!("Hello"); } }
impl Greet for Ja { fn greet(&self) { println!("Hello"); } }
let g: Box<dyn Greet> = if std::env::var("LANG").unwrap_or_default().starts_with("ja") {
Box::new(Ja)
} else {
Box::new(En)
};
g.greet();
impl Trait vs dyn Trait
| Form | Meaning |
|---|---|
impl Trait | static dispatch (concrete type at compile time) |
dyn Trait | dynamic dispatch (resolve at runtime, needs Box or & ) |
impl is faster, but dyn lets you have heterogeneous collections like Vec<Box<dyn Trait>>.
6. What you'll see in ExEx code
async fn my_exex<Node: FullNodeComponents>(
mut ctx: ExExContext<Node>,
) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.recv().await {
// ...
}
Ok(())
}
Node: FullNodeComponents— trait boundExExContext<Node>— generic over the node bundle- Internally uses
Arc<...>for shared components - Lifetime annotations are elided but
'staticis required
Cheat sheet
| Feature | One-liner |
|---|---|
'a / 'static | how long borrows live |
Box<T> | heap allocation |
Rc<T> / Arc<T> | shared ownership (Arc is thread-safe) |
Mutex<T> | safely mutate shared data |
dyn Trait | runtime method dispatch |
Final check: close this tab. Write the ExEx
my_exexsignature from memory. If you can't, you don't yet own the vocabulary — open it back up. The next lesson reads ExEx in detail; you'll need each of these without the cheat sheet.
Summary (3 lines)
- Lifetimes (
<'a>) + Box<dyn Trait> + Arc<dyn Trait + Send + Sync> + &dyn Trait + impl Trait. Each has a specific use case. - Reth conventions: components as Arc<dyn ... + Send + Sync>; hot-path generics; mid-frequency dyn.
- Reading Reth needs this fluency. Next: ExEx buildup.