A Nix-driven workbench for developing DPDK applications in the D programming language.
Features • Quick start • D + DPDK • MicroVM • Layout
A flake-parts-based Nix flake that pins the DPDK and LDC
toolchains, demonstrates how to call DPDK from D without hand-written
bindings or dstep by leaning on LDC's ImportC, and ships a disposable
NixOS microvm — preconfigured with hugepages and a virtio NIC — so you can
actually run your DPDK code without root, host-side hugepage reservations,
or per-distro package juggling. A small upstream C example tags along as a
toolchain sanity check.
Call DPDK from D, directly, via ImportC.
examples/helloworld-d/ shows the pattern. A
four-line dpdk_c.c #includes the DPDK headers; LDC's ImportC
preprocesses it and makes every declaration callable from main.d. No
hand-written extern(C) blocks, no dstep step, no binding maintenance:
import dpdk_c;
import core.stdc.stdio : printf;
extern(C) int lcore_hello(void* arg) {
printf("hello from core %u\n", rte_lcore_id());
return 0;
}
extern(C) int main(int argc, char** argv) {
if (rte_eal_init(argc, argv) < 0)
rte_exit(1, "Cannot init EAL\n");
// …
}C macros that ImportC can't surface (e.g. RTE_LCORE_FOREACH_WORKER) get
spelled out as their underlying rte_get_next_lcore expansion. The Nix
derivation drops fortify/fortify3 from NIX_HARDENING_ENABLE because
ImportC can't parse glibc's __builtin___*_chk fortified inlines —
otherwise everything is stock.
A disposable NixOS microvm for actually running DPDK.
DPDK wants hugepages, and most setups want root. On a dev host that's a pain — system reboots, capability juggling, the works. The flake exposes two QEMU/KVM guests built with microvm.nix:
nix run .#dpdk-smoke— boots, runs the C and D helloworlds via a systemd oneshot, prints the output to serial, then powers off cleanly. Drop-in CI smoke test.nix run .#dpdk-guest— boots interactively with serial autologin to root, motd cheat-sheet, anddpdk-testpmd/dpdk-devbind.pyready to go.
Both guests 9p-mount the host's /nix/store into the VM, so microvm.nix
skips building an erofs image of the closure — rebuilds are seconds, not
minutes. The guest reserves 1 GB of 2 MB hugepages at boot, gets one
virtio-net NIC, and runs on stock QEMU+KVM (no host hugepages required).
$ nix run .#dpdk-smoke
…
[ 12.40] dpdk-smoke-start[969]: hello from core 1
[ 12.40] dpdk-smoke-start[969]: hello from core 0
[ 13.04] dpdk-smoke-start[979]: hello from core 1
[ 13.04] dpdk-smoke-start[979]: hello from core 0
[ 13.05] dpdk-smoke-start[967]: === smoke done ===Reproducible toolchain via Nix.
direnv allow (or nix develop) and you have DPDK 25.07, LDC 1.41,
dub, dtools, pkg-config, gcc, numactl, and gnumake — no version
drift across machines or distros.
$ pkg-config --modversion libdpdk
25.07.0
$ ldc2 --version | head -1
LDC - the LLVM D compiler (1.41.0):Upstream helloworld in C, byte-for-byte (toolchain reference).
examples/helloworld-c/ is a verbatim copy of
DPDK's examples/helloworld/, including the pkg-config libdpdk-driven
Makefile. It's not the point of the project — the D port is — but it's a
useful reference and a sanity check that the toolchain matches upstream.
$ make -C examples/helloworld-c
$ ldd examples/helloworld-c/build/helloworld | grep librte_eal
librte_eal.so.25 => /nix/store/…-dpdk-25.07/lib/librte_eal.so.25Get the toolchain.
$ git clone <this repo>
$ cd dpdk-d-devshell
$ direnv allow # or, if you don't use direnv:
$ nix developBuild the D example.
$ make -C examples/helloworld-dOr as a Nix package:
$ nix build .#helloworld-d
$ ./result/bin/dpdk-helloworld-d --help | head -2
EAL: Detected CPU lcores: 32
EAL: Detected NUMA nodes: 1Run it in the VM.
$ nix run .#dpdk-smoke # one-shot: boot, run helloworlds, halt
$ nix run .#dpdk-guest # interactive shell on serial consoleExit code 0 plus the two hello from core N pairs from the smoke run
means the toolchain is healthy end-to-end.
The full D main.d lives in
examples/helloworld-d/main.d; the ImportC
shim is examples/helloworld-d/dpdk_c.c.
Three ideas worth carrying into a real project:
- Keep
dpdk_c.cthin. It exists so ImportC has a single translation unit to chew on; expose only the headers your D code actually touches. If a header explodes (DPDK does heavy macro work), pull in a smaller one and put a hand-rolled trampoline next to it. - Inline C macros at the call site. ImportC doesn't surface
preprocessor macros to D. Expand them in D —
RTE_LCORE_FOREACH_WORKERbecomes awhileloop overrte_get_next_lcore. - Disable
_FORTIFY_SOURCEfor ImportC's preprocessor pass — seenix/pkgs/helloworld-d/default.nixfor thehardeningDisabletrick, or theNIX_HARDENING_ENABLEfilter inexamples/helloworld-d/Makefileif you're building outside Nix.
The guests live in nix/microvm/:
guest.nix— shared base: QEMU/KVM hypervisor, 4 vCPU, 3 GB RAM (avoiding QEMU's exactly-2-GB hang), 1 GB of 2 MB hugepages, one virtio-net interface, helloworld binaries baked into the closure, host/nix/storeshared in via 9p.interactive.nix— serial autologin and amotdcheat-sheet.smoke.nix— systemd oneshot that runs the binaries then powers off.default.nix— flake-parts module wiring both guests asnixosConfigurationsand aspackages.dpdk-{guest,smoke}.
VFIO PCI pass-through is intentionally out of scope here; the guest is meant
to validate the toolchain and exercise software vdevs (net_null0,
net_pcap0), not to run wire-rate.
If you'd rather skip the VM and run DPDK directly on a NixOS host, reserve
hugepages via boot.kernelParams:
boot.kernelParams = [
"default_hugepagesz=2M"
"hugepagesz=2M"
"hugepages=512" # 1 GB of 2 MB pages
];For real workloads, prefer 1 GB pages (hugepagesz=1G hugepages=N) and add
intel_iommu=on iommu=pt (or amd_iommu=on) for VFIO PCI pass-through.
.
├── examples/
│ ├── helloworld-c/ # verbatim upstream DPDK helloworld (reference)
│ └── helloworld-d/ # the D port via LDC ImportC
├── nix/
│ ├── pkgs/ # Nix derivations: cpu-checker, helloworld-{c,d}
│ └── microvm/ # flake-parts module: dpdk-guest, dpdk-smoke
├── flake.nix
└── flake.lock