eBPF Language Battle: Full-Stack Comparison of C, Rust, and Zig
The previous articles on OOM tracing all used C for eBPF kernel-space programs. This is natural — C is eBPF’s “native language,” with the verifier, CO-RE, and libbpf toolchain all designed around C. But if you’ve followed the eBPF ecosystem, you’ll notice a clear trend: more and more people are writing eBPF in languages other than C. Rust’s Aya framework is already used in production by the Solana validator and Kubernetes Gateway API; meanwhile, Zig is trying to bring a new development experience with comptime, explicit allocation, and first-class C interop.
This article shifts perspective: instead of focusing on a specific scenario, we’re doing a full-stack comparison — for the same eBPF program, how would you write it in C, Rust, and Zig? How do kernel-space programs compile? How do user-space loaders work? Where’s the real technical dividing line among the three?
Timeline note: This article is based on the toolchain state as of June 2026. Zig’s BPF CO-RE support remains a Post-1.0 goal, and Aya’s compiler-level CO-RE relocation is also in progress — these are evolving rapidly, and some conclusions may change with version updates.
eBPF Full-Stack Development Model
Regardless of the language, a complete eBPF tool consists of two parts: the BPF program running in kernel space, and the control program running in user space. Understanding this model is prerequisite for comparing the three languages.
flowchart LR
subgraph 用户态["用户态 User Space"]
LD["Loader<br/>打开 .o、加载、attach"]
POLL["控制循环<br/>读 maps / ringbuf"]
end
subgraph 内核态["内核态 Kernel"]
HOOK["Hook 点<br/>kprobe / tracepoint / xdp"]
BPF["BPF 程序<br/>verifier 校验后运行"]
MAP["Maps / RingBuf<br/>内核 ↔ 用户数据通道"]
end
LD -- "bpf 系统调用" --> BPF
BPF -- "挂载到" --> HOOK
HOOK -- "事件触发执行" --> BPF
BPF -- "读写" --> MAP
MAP -- "用户态读取" --> POLL
classDef user fill:#3b82f6,color:#fff
classDef kern fill:#f59e0b,color:#fff
class LD,POLL user
class HOOK,BPF,MAP kernTwo pieces of code, two targets, one ELF file binding them together:
- Kernel-space program compiles to a BPF ELF object (
.o), containing bytecode, map definitions, and BTF information. It must pass verifier validation before running. - User-space loader is responsible for loading this
.ointo the kernel, handling CO-RE relocation, attaching to specific hook points, then reading maps/ringbuf to retrieve data.
The difference among the three languages is essentially what language to use for these two pieces of code, and what toolchain to glue them together.
C + libbpf: Unshakable Baseline
C is the de facto standard for eBPF. Kernel docs, Cilium, bcc, Tetragon, BPF tools in all major distributions — all C + libbpf. Later arrivals take it as their reference.
The typical development paradigm is libbpf-bootstrap: compile kernel-space programs with Clang, access kernel structures through the BPF_CORE_READ macro (this is CO-RE), then use bpftool gen skeleton to generate type-safe loader skeletons.
Kernel-space program (C + libbpf):
| |
User-space loader (libbpf skeleton):
| |
vmlinux.h is all kernel structure definitions generated by bpftool btf dump from the target kernel. Combined with BPF_CORE_READ, one program can run on different kernel versions without recompilation — this is CO-RE (Compile Once, Run Everywhere). The C path’s CO-RE is compiler-native and most complete, which is why it remains unshakable to this day.
Rust + Aya: Strongest Challenger for Memory Safety
Aya is the eBPF framework in the Rust ecosystem. As of June 2026, aya-rs/aya has 4.6k stars, with the latest version at v0.14.0. Its biggest selling point is pure Rust, no C toolchain dependencies: kernel-space programs use aya-ebpf (compiled with #![no_std]) to BPF bytecode, user-space programs use aya to load, all without Clang, libbpf, or kernel headers.
Kernel-space program (Aya, #![no_std]):
| |
User-space loader (Aya):
| |
Aya’s real production strength isn’t in the code itself, but in its user roster: the Solana validator’s XDP fast packet retransmission (agave-xdp), Kubernetes SIGs’ L4 load balancer blixt, Red Hat’s bpfman, and Deepfence’s ebpfguard. This is currently the only non-C solution validated at large production scale.
Aya also brings things the C path lacks: type-safe map access, tokio-native async (AsyncFd directly interfaces with ringbuf), and the ownership system blocking a class of errors at compile time.
Zig + zbpf: Explorer with Potential and Limitations
Zig is the youngest attempt of the three. The Zig toolchain itself supports BPF targets — zig cc -target bpfel can compile BPF ELF objects, and Zig’s source code src/target.zig explicitly lists bpfel/bpfeb as LLVM-supported target architectures. But the official platform support page lists BPF as “Additional Platforms,” not within the Tier rating system, meaning available but correctness isn’t guaranteed.
| |
The most mature in the community is tw4452852/zbpf (275 stars). It doesn’t go the old @cImport(libbpf) route, instead implementing a pure Zig user-space loader: built-in ELF parsing, BTF parsing, CO-RE relocation, skeleton code generation, zero external dependencies (no libelf, libbpf, libz).
Kernel-space program (Zig + zbpf, schematic):
| |
User-space loader (Zig + zbpf):
| |
Zig’s appeal lies in language features: comptime for compile-time type checking, explicit allocators making memory sources crystal clear, @cImport making C interop nearly zero-cost. One build command zig build zbpf -Dbpf=probe.zig -Dmain=main.zig packages kernel-space and user-space into a single binary, cross-compile with zero config.
But writing eBPF in Zig has one unavoidable hard deficiency.
CO-RE: Technical Dividing Line Among Three Languages
This is the most critical part of the article. CO-RE isn’t a binary choice of “have or not,” but a three-tier “at which layer to implement,” determining whether your program can run across kernel versions.
The essence of CO-RE is: when BPF programs access kernel structure fields, they don’t hardcode offsets but encode them as BTF relocation records, which the user-space loader rewrites instructions based on the target kernel’s actual layout at load time. To do this, the compiler must emit a special LLVM intrinsic (llvm.preserve.struct.access.index etc.).
| Language | CO-RE Implementation Layer | Mechanism | Maturity |
|---|---|---|---|
| C (libbpf) | Compiler-native | Clang emits BTF relocation intrinsics, BPF_CORE_READ macro | Production-grade |
| Rust (Aya) | User-space relocation | aya-obj parses ELF for relocation; Rust compiler-side BTF relo not yet complete | Usable |
| Zig (zbpf) | User-space workaround | zbpf implements all 16 relocations at ELF layer; Zig compiler doesn’t emit intrinsics | Experimental |
C’s advantage is native: Clang directly understands CO-RE, BPF_CORE_READ is a first-class citizen, cross-kernel operation is compiler-guaranteed.
Aya is in transition: According to the 2026 FOSDEM talk, Rust can already produce correct BTF for all types, but compiler-side BTF relocation intrinsics aren’t complete yet. Currently Aya does relocation in user-space (aya-obj) to compensate, so it’s usable but not yet compiler-native guaranteed. This route is officially in progress.
Zig is stuck at the hardest step: Codeberg issue #35324 explicitly states the Zig compiler won’t emit intrinsics required for CO-RE, so “cannot realistically be used to write eBPF programs.” zmitchell is implementing these intrinsics in Air.zig, but Andrew Kelley marked it as a Post-1.0 milestone — meaning it won’t be considered until after 1.0, with no current timeline. zbpf’s CO-RE is an ELF-layer manual workaround, usable but not a compiler-guaranteed equivalent; accessing complex nested structures may still be limited.
This layer difference is the fundamental reason Zig can’t yet stand alongside C and Aya as equals.
Full-Stack Comparison
| Dimension | C (libbpf) | Rust (Aya) | Zig (zbpf) |
|---|---|---|---|
| Kernel-space language | C | Rust(#![no_std]) | Zig(freestanding) |
| User-space language | C / Go | Rust | Zig |
| CO-RE / BTF | Compiler-native, complete | BTF complete, compiler relo in progress | ELF layer workaround, no compiler-native |
| C toolchain dependencies | Needs Clang | Not needed | Still need Clang backend when compiling BPF |
| Memory safety | No guarantee | Ownership system compile-time guarantee | Explicit allocation, partial runtime checks |
| Async runtime | Manual epoll | tokio / async-std native | No native async |
| Ecosystem size | Largest(Cilium、bcc、Tetragon) | Large(4.6k stars,7+ production users) | Small(275 stars,1 main maintainer) |
| Production cases | Industry standard | Solana、K8s、Red Hat、Deepfence | No public production cases |
| Skeleton generation | bpftool gen skeleton | aya-tool / xtask | zbpf gen-skeleton |
| Kernel version compatibility | ≥ 4.18(CO-RE) | ≥ 5.8(ringbuf) | Depends on features |
| Compile speed | Slow(Clang + kernel headers) | Fast(pure Rust) | Fast(Zig toolchain) |
| Learning curve | Medium(C + libbpf complex) | Medium(Rust + Aya concepts) | Medium(Zig simple but thin ecosystem) |
Maturity Assessment and Practical Selection
Honestly:
- C + libbpf: Production-ready, industry standard. CO-RE most complete, ecosystem thickest, most documentation. Without special reasons for new projects, it’s still the default choice. The cost is tolerating C’s memory unsafety and heavier toolchain.
- Rust + Aya: Production-usable, the only non-C solution today that can replace C for writing production eBPF. If your team already uses Rust, or needs memory safety + async, Aya is the clear choice. The only caveat is compiler-level CO-RE is still on the way; complex scenarios need testing.
- Zig + zbpf: Experimental. Technical exploration is imaginative (pure Zig loader, zero dependencies, comptime type safety), but missing compiler-level CO-RE means no guarantee of cross-kernel operation, and the ecosystem is too small (single maintainer). Suitable for learning, technical research, contributing to the community, but not recommended for production.
A pragmatic judgment: if you’re choosing a non-C language for eBPF today, there’s only one answer — Rust + Aya. Zig’s story is attractive, but its key piece (CO-RE) is still on the Post-1.0 roadmap, not a production option at least short-term. However, zbpf’s pure Zig loader implementation is worth every eBPF enthusiast reading — it reimplements libbpf’s core in one language,本身就是对 eBPF 工具链最好的深度学习.
Summary
This article outlined three eBPF development paths from the language ecosystem perspective:
- C + libbpf remains the baseline, compiler-native CO-RE is its moat
- Rust + Aya is the only production-usable non-C solution, memory safety + async is real value
- Zig + zbpf is an explorer with potential, but stuck at compiler-level CO-RE, currently only suitable for experimentation
The true dividing line among the three languages isn’t syntax, but at which layer CO-RE is implemented — compiler-native, user-space relocation, or ELF layer workaround. Understanding this, you can see the maturity ceiling of each project.
The eBPF language battle is just beginning. Later I’ll continue exploring in this direction — might actually write a tool with zbpf, or dissect Aya’s CO-RE relocation implementation. This series will keep going.