
Rust — Trait Objects, Sized, and Why My DAG Needed `Box<dyn Fn>`
TL;DR — When building a DAG executor, I needed to store arbitrary task closures.
Generics weren’t enough. The solution required trait objects, dynamic dispatch, and'staticbounds.
Introduction
While building a DAG executor, I needed to store arbitrary task closures in a graph structure.
A naïve approach would be:
struct Node<F> {
f: F
}At first glance, this looks fine. But it immediately breaks when we try to store multiple nodes.
2. Why Generics Don't Work
Each closure in Rust has a unique anonymous type.
With generics:
Vec<Node<F>>Each Node<F> is monomorphized for one concrete F. That means F can represent only one concrete type.
But my graph needs:
Vec<Node<different closure types>>That is impossible because Vec<T> requires a single concrete T.
Even if all closures implement Fn(...), they are distinct concrete types. This means:
Generics fail because we need heterogeneity inside a homogeneous container.
We thus need type erasure.
An alternative would be to use an enum with predefined variants.
This works when the set of possible tasks is closed and known in advance.
However, in a general-purpose DAG library, users can provide arbitrary closures.
This makes the set of possible task types open-ended, which requires trait objects rather than enums.
3. Trait Objects and Dynamically Sized Types
The solution is to use trait objects:
Box<dyn Fn(&[Arc<O>]) -> Result<O, E>>Why does this work?
Because dyn Fn erases the concrete closure type.
When we write dyn Fn, we are effectively saying:
"I do not care about the concrete type. I only care that it implements
Fn."
Once the concrete type is erased, the compiler no longer knows:
- The size
- The layout
- The alignment
Therefore, dyn Trait becomes a Dynamically Sized Type (DST).
DSTs cannot live directly on the stack because the compiler must know stack frame sizes at compile time.
So they must be placed behind a pointer.
What Box<dyn Fn> Actually Contains
A Box<dyn Fn> is a fat pointer:
(data_ptr, vtable_ptr)data_ptr→ pointer to the heap allocation containing the concrete closurevtable_ptr→ pointer to a virtual method table that enables dynamic dispatch
This is conceptually similar to how &str is represented as:
(ptr, len)But instead of length metadata, trait objects store a vtable pointer.
Dynamic dispatch happens through this vtable.
In summary:
dyn Fnis unsized because trait objects erase the concrete type, so the compiler no longer knows its layout at compile time. Therefore it must be placed behind a pointer (e.g.,Box) which carries the necessary metadata (vtablepointer) to enable dynamic dispatch.
4. Why 'static, Send and Sync Are Required
My task type is defined as:
pub type TaskFn<O, E> =
dyn Fn(&[Arc<O>]) -> Result<O, E> + Send + Sync + 'static;What 'static Actually Means
T: 'static does not mean "lives forever".
It means:
The type does not contain non-static references.
For example:
let x = 5;
builder.add_task("a", vec![], |_| Ok(x));If x were captured by reference, this closure would contain a reference tied to the current stack frame.
If the task is stored and executed later (possibly in another thread), that reference would become invalid.
Requiring 'static ensures:
- The closure owns its captured data
- Or only references
'staticdata
This prevents stack-local references from escaping.
Why Thread Pools implies 'static
When spawning threads:
std::thread::spawn(move || { ... });The closure must be 'static.
Why?
Because the thread may outlive the current function stack frame.
Rust cannot guarantee when the thread completes.
Therefore:
Anything moved into a thread must not contain references tied to the current stack.
Since my tasks will eventually run in worker threads for parallel execution, 'static is necessary.
It is not about “threads need to know”, it is about preventing captured stack references from escaping.
5. Tradeoffs: Generics vs Trait Objects
Using trait objects means:
- Dynamic dispatch (vtable lookup)
- Slight runtime indirection
But it avoids:
- Monomorphization bloat
- Inability to store heterogeneous closures
In a system where tasks are CPU-heavy, the dynamic dispatch overhead is negligible compared to the work being performed.
The flexibility gain is worth it.
Final Thoughts
Understanding trait objects requires understanding:
- Type erasure
- Dynamically sized types
- Fat pointers
- Lifetime bounds like
'static
These concepts become unavoidable when building systems that store executable logic.
Stay Updated
Get notified when I publish new articles about Web3 development, hackathon experiences, and cryptography insights.
You might also like

Crescent Bench Lab: Measuring ZK Presentations for Real Credentials (JWT + mDL)
A small Rust lab that vendors microsoft/crescent-credentials, generates Crescent test vectors, and benchmarks zksetup/prove/show/verify across several parameters — including proof sizes and selective disclosure variants.

TEE Auction Coprocessor: Replay-Safe Attested Auction Receipt with Gramine SGX — Tutorial
A Rust mini-lab that turns a Vickrey (second-price) auction into a TEE coprocessor: deterministic core, bid commitments, replay protection, and a policy-driven verifier—leaving full DCAP collateral/TCB verification (PCS chain, revocation, freshness rules) for a follow-up.

Baby-Ligero: Three Tiny Tests for a Tiny Circuit — ZK Hack S3M5
A mini Rust lab that implements a baby version of Ligero's three tests — proximity, multiplication, and linear — for a tiny arithmetic circuit, and uses them to see soundness amplification in action.