vs. ndnd (Go)
📝 Note: ndnd and ndn-rs share a goal of being simpler and more modern than NFD, but they make fundamentally different language-level tradeoffs. This comparison focuses on how Go’s garbage collector and goroutine model differ from Rust’s ownership and async model for the specific workload of NDN packet forwarding.
This page compares ndn-rs with ndnd, an NDN forwarder written in Go. ndnd was created as a simpler, more modern alternative to NFD, leveraging Go’s concurrency primitives and garbage collector. Understanding the differences clarifies what ndn-rs gains from Rust’s ownership model and what tradeoffs that entails.
Comparison Table
| Aspect | ndnd (Go) | ndn-rs (Rust) | Why ndn-rs chose differently |
|---|---|---|---|
| Architecture | Single binary. ndnd is a standalone forwarder; applications communicate over a Unix socket or TCP. | Embeddable library. The forwarder engine is a library crate that can be embedded in-process, run as a standalone router, or compiled for bare-metal targets. | A single-binary forwarder forces IPC on every packet exchange between application and forwarder. Embedding the engine eliminates this overhead entirely and allows co-optimization of application and forwarding logic. |
| Pipeline model | Convention-based. Go functions pass packet structs through channels; the order of processing steps is enforced by code structure and developer discipline. | Ownership-based. PacketContext is moved by value through PipelineStage trait objects. Short-circuits consume the context, making use-after-hand-off a compile error. | Go’s garbage collector means any goroutine can hold a reference to a packet indefinitely. There is no compile-time guarantee that a forwarded packet is not also being read elsewhere. Rust’s ownership model makes the pipeline’s data flow explicit and compiler-checked. |
| PIT concurrency | sync.Mutex. ndnd protects the PIT with a standard Go mutex. | DashMap (sharded concurrent hash map). The PIT is split into independent shards, each with its own lock. | A single mutex serializes all PIT operations. Under load, goroutines queue up waiting for the lock. DashMap distributes contention across shards, allowing multiple cores to process PIT lookups in parallel. |
| Strategy system | Interface-based. Strategies implement a Go interface. Adding or changing a strategy requires recompilation. | Trait + WASM. Built-in strategies implement the Strategy trait; external strategies can be hot-loaded as WASM modules at runtime via ndn-strategy-wasm. Strategies compose via StrategyFilter wrappers. | Go interfaces are similar to Rust traits for dispatch, but lack composability (no generic wrappers without reflection). WASM hot-loading allows deploying new forwarding logic to running routers in production without downtime or recompilation. |
| Simulation | None built-in. Testing ndnd at scale requires deploying multiple instances, often using Mini-NDN (Mininet-based). | In-process simulation. ndn-sim provides SimFace and SimLink for building arbitrary topologies in a single process, with deterministic event replay and tracing. | Network simulation with real OS processes is slow, non-deterministic, and hard to debug. In-process simulation with simulated links enables fast, reproducible integration tests and research experiments without any external tooling. |
| Embedded targets | Not supported. Go’s runtime (garbage collector, goroutine scheduler) requires an OS with virtual memory. | Same crate, no_std. ndn-tlv and ndn-packet compile without the standard library; ndn-embedded provides a minimal forwarder for bare-metal microcontrollers. | NDN’s value proposition includes IoT and edge devices. A forwarder that cannot run on a microcontroller leaves that space to separate, incompatible implementations. Rust’s no_std support lets the same packet library run everywhere. |
| Memory model | Garbage collected. The Go runtime traces live objects and reclaims memory periodically. GC pauses are short but non-deterministic. | Ownership + reference counting. Memory is freed deterministically when the last owner drops. Arc provides shared ownership where needed (names, CS entries). No GC pauses. | GC pauses are problematic for a forwarder where latency matters. A 100-microsecond GC pause during a PIT lookup is a 100-microsecond latency spike for every pending Interest. Deterministic deallocation means predictable, low-tail-latency forwarding. |
| Packet lifetime | GC-managed. A packet struct lives as long as any goroutine holds a reference. The programmer does not think about when memory is freed. | Explicit. Bytes (reference-counted buffer) and Arc<Name> make sharing explicit. When the last reference is dropped, memory is freed immediately. | Explicit lifetime management is more work for the programmer but provides two benefits: (1) memory usage is predictable and bounded, critical for routers under load; (2) zero-copy sharing via Bytes::clone() (reference count increment, not data copy) is opt-in and visible in the code. |
| Error handling | Multiple returns (value, error). Errors are values; forgetting to check an error is a silent bug (though errcheck linters help). | Result<T, E>. The compiler forces every error to be handled. Ignoring a Result is a warning; discarding it silently requires an explicit let _ =. | In a forwarder, silently swallowed errors (e.g., a failed PIT insert) can cause hard-to-diagnose forwarding failures. Rust’s Result type makes error handling mandatory, catching these bugs at compile time. |
| Build and deploy | go build produces a single static binary. Fast compilation, simple deployment. | cargo build produces a single binary. Compilation is slower than Go but produces faster code. Cross-compilation to embedded targets is straightforward via Cargo. | Go’s fast compilation is a genuine advantage for developer iteration speed. ndn-rs accepts slower builds in exchange for zero-cost abstractions, no GC overhead, and the safety guarantees described above. |
Go’s GC vs. Rust’s Ownership for NDN
The choice between garbage collection and ownership-based memory management has particular implications for NDN packet processing:
PIT entry lifetime. A PIT entry must live exactly as long as the Interest is pending – typically milliseconds to seconds. In Go, the GC will eventually reclaim expired entries, but “eventually” may mean the entry lingers in memory for multiple GC cycles after expiry. In ndn-rs, dropping a PIT entry frees its memory immediately, and the hierarchical timing wheel ensures expired entries are drained on a 1 ms tick.
Content Store pressure. The CS is the largest memory consumer in a forwarder. In Go, cached Data packets are opaque to the GC – it must trace through them to determine liveness, adding GC overhead proportional to CS size. In ndn-rs, the CS stores Bytes (reference-counted buffers). Eviction drops the reference count; if no pipeline is currently sending that data, the buffer is freed instantly. The GC never needs to scan cached data because there is no GC.
Name sharing. An NDN name may appear simultaneously in the PIT, FIB, CS, and multiple pipeline contexts. In Go, the GC handles this transparently – all references are equal. In ndn-rs, Arc<Name> makes sharing explicit: cloning an Arc is a reference count increment (one atomic operation), and the name is freed when the last Arc is dropped. The cost is identical to Go’s approach at runtime, but the programmer can see exactly where names are shared.
Zero-copy forwarding. When a Data packet satisfies multiple PIT entries, ndnd typically copies the packet for each downstream face. In ndn-rs, Bytes::clone() creates a new handle to the same underlying buffer (one atomic increment). Multiple faces receive the same data without any copy – a pattern that is natural with reference counting but requires careful manual management in GC’d languages where “sharing” and “copying” look the same.
Where ndnd Has the Advantage
- Simplicity. ndnd’s codebase is smaller and easier for newcomers to read. Go’s lack of generics complexity (traits, lifetimes, associated types) means less to learn.
- Compilation speed. Go compiles significantly faster than Rust, improving developer iteration time.
- Goroutine ergonomics. Go’s goroutines and channels make concurrent code straightforward to write. Rust’s async model requires more explicit annotation (
async,.await,Sendbounds) and familiarity with pinning. - Community overlap. ndnd is actively maintained by NDN project contributors, ensuring close alignment with evolving NDN specifications.