kotlin-ebpf-dsl
February 26, 2026 · View on GitHub
A Kotlin DSL for writing eBPF programs with compile-time validation. Write type-safe eBPF in Kotlin, generate production-ready .bpf.c and Kotlin map reader classes.
Why?
Writing eBPF programs in C is error-prone: wrong helper calls, stack overflows, type mismatches, and verifier rejections are only caught at load time. This DSL catches them at build time in your Kotlin tests.
- Type-safe structs with automatic C ABI layout (alignment, padding)
- 3-phase validation — construction-time checks, type checker, semantic analyzer
- Dual codegen — generates both
.bpf.cfiles and KotlinMapReaderclasses - 24 ready-to-use BCC-style tools — biolatency, runqlat, execsnoop, tcpconnect, and more
- 31 BPF helpers with per-program-type availability and GPL enforcement
- 14 program types — tracepoint, kprobe, kretprobe, fentry, fexit, XDP, TC, cgroup, LSM, and more
Quick Start: Use a Built-in Tool
The fastest way to get started — pick a tool, validate, generate:
import dev.ebpf.dsl.tools.*
import dev.ebpf.dsl.api.*
val program = biolatency()
program.validate().throwOnError()
// Generate C and Kotlin
val c = program.generateC() // biolatency.bpf.c
val kt = program.generateKotlin("com.example.bio") // BiolatencyMapReader.kt
// Or emit both to disk
program.emit(OutputConfig(
cDir = "bpf/",
kotlinDir = "src/main/kotlin/",
kotlinPackage = "com.example.bio"
))
See BCC-Style Tools for the full list of 12 ready-to-use tools.
Quick Start: Write a Custom Program
import dev.ebpf.dsl.api.*
import dev.ebpf.dsl.types.*
// 1. Define structs
object CgroupKey : BpfStruct("cgroup_key") {
val cgroupId by u64()
}
object Counter : BpfStruct("counter") {
val count by u64()
}
// 2. Build the program
val program = ebpf("oom_mon") {
license("GPL")
val oomKills by lruHashMap(CgroupKey, Counter, maxEntries = 10240)
tracepoint("oom", "mark_victim") {
val cgroupId = declareVar("cgroup_id", getCurrentCgroupId())
val key = stackVar(CgroupKey) {
it[CgroupKey.cgroupId] = cgroupId
}
val entry = oomKills.lookup(key)
ifNonNull(entry) { e ->
e[Counter.count].atomicAdd(literal(1u, BpfScalar.U64))
}
returnValue(literal(0, BpfScalar.S32))
}
}
// 3. Validate
program.validate().throwOnError()
// 4. Generate
val c = program.generateC()
val kt = program.generateKotlin("com.example.oom")
Generated C
// AUTO-GENERATED by kotlin-ebpf-dsl — do not edit
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "GPL";
struct cgroup_key {
__u64 cgroup_id;
};
struct counter {
__u64 count;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 10240);
__type(key, struct cgroup_key);
__type(value, struct counter);
} oom_kills SEC(".maps");
SEC("tp/oom/mark_victim")
int tp_oom_mark_victim(void *ctx)
{
__u64 cgroup_id = bpf_get_current_cgroup_id();
struct cgroup_key var_0 = {};
var_0.cgroup_id = cgroup_id;
struct counter *entry_1 = bpf_map_lookup_elem(&oom_kills, &var_0);
if (entry_1) {
__sync_fetch_and_add(&entry_1->count, 1ULL);
}
return 0;
}
Generated Kotlin MapReader
// AUTO-GENERATED — type-safe BPF map deserialization
class OomMonMapReader {
object CgroupKeyLayout {
const val SIZE = 8
const val CGROUP_ID_OFFSET = 0
fun encode(cgroupId: Long): ByteArray { /* ... */ }
fun decodeCgroupId(bytes: ByteArray): Long { /* ... */ }
}
data class CounterEntry(val cgroupId: Long, val count: Long)
fun readOomKills(mapFd: Int, bridge: BpfBridge, maxEntries: Int): List<CounterEntry> {
return bridge.mapBatchLookupAndDelete(mapFd, CgroupKeyLayout.SIZE, CounterLayout.SIZE, maxEntries)
.map { (keyBytes, valBytes) ->
CounterEntry(
cgroupId = CgroupKeyLayout.decodeCgroupId(keyBytes),
count = CounterLayout.decodeCount(valBytes)
)
}
}
}
No more manual ByteBuffer parsing — the generated reader handles endianness, offsets, and alignment.
BCC-Style Tools
24 ready-to-use eBPF programs inspired by BCC tools, adapted for per-cgroup (pod-level) aggregation in Kubernetes.
| Tool | Description | Hook Type |
|---|---|---|
execsnoop() | Process exec/exit/fork counting | tracepoint (sched) |
oomkill() | OOM kill event counting | tracepoint (oom) |
runqlat() | CPU run queue latency histogram | tracepoint (sched) |
tcpconnect() | TCP bytes, retransmits, connections, RTT | kprobe + tracepoint |
vfsstat() | VFS read/write/open/fsync counting | kprobe |
biolatency() | Block I/O latency histogram | kprobe (blk-mq) |
hardirqs() | Hardware interrupt latency histogram | tracepoint (irq) |
softirqs() | Software interrupt latency histogram | tracepoint (irq) |
cachestat() | Page cache hit/add/dirty counting | kprobe |
cpudist() | On-CPU time distribution histogram | tracepoint (sched) |
dcstat() | Directory cache (dcache) hit/miss counting | kprobe |
tcpdrop() | TCP packet drop counting | kprobe |
tcplife() | TCP connection duration histogram | tracepoint (sock) |
syscount() | System call counting | raw_tracepoint |
capable() | Security capability check counting | kprobe |
filelife() | File creation/deletion counting | kprobe |
slabtop() | Slab/kmalloc allocation counting | kprobe |
writeback() | Dirty page writeback event counting | tracepoint (writeback) |
bitesize() | Block I/O request size distribution | kprobe (blk-mq) |
drsnoop() | Direct memory reclaim event counting | tracepoint (vmscan) |
signalsnoop() | Signal delivery counting | tracepoint (signal) |
solisten() | Socket listen event counting | kprobe |
pidpersec() | Process creation rate (fork/exec) | tracepoint (sched) |
tcpsynbl() | TCP SYN backlog completion counting | kprobe (tcp) |
Each tool generates:
.bpf.cwith proper SEC annotations, struct definitions, and LRU hash maps- Kotlin
MapReaderclass with type-safeByteBufferdeserialization
Tool Registry
Discover and build tools programmatically via ToolRegistry:
import dev.ebpf.dsl.tools.ToolRegistry
import dev.ebpf.dsl.api.*
// List all available tools
ToolRegistry.all().forEach { tool ->
println("${tool.name}: ${tool.description} [${tool.hookTypes.joinToString()}]")
}
// Build by name
val program = ToolRegistry.byName("runqlat")!!.build()
program.validate().throwOnError()
println(program.generateC())
// Filter by hook type
val kprobeTools = ToolRegistry.byHookType("kprobe")
DSL Reference
Struct Types
object LatHist : BpfStruct("lat_hist") {
val slots by array(BpfScalar.U64, 27) // fixed-size arrays
val count by u64()
val sumNs by u64()
}
Scalars: u8(), u16(), u32(), u64(), s8(), s16(), s32(), s64(), bool()
Map Types
val stats by lruHashMap(Key, Value, maxEntries = 4096)
val events by hashMap(Key, Value, maxEntries = 10240)
val counters by percpuArray(Value, maxEntries = 256)
val ring by ringBuf(maxEntries = 1 shl 20)
Supported: hashMap, lruHashMap, percpuHashMap, array, percpuArray, ringBuf, scalarHashMap, scalarLruHashMap
Program Types
tracepoint("sched", "sched_switch") { /* ... */ }
rawTracepoint("sys_enter") { /* ... */ }
kprobe("vfs_read") { /* ... */ }
kretprobe("vfs_read") { /* ... */ }
fentry("tcp_connect") { /* ... */ }
fexit("tcp_connect") { /* ... */ }
xdp { /* ... */ }
tcClassifier { /* ... */ }
cgroupSkb("ingress") { /* ... */ }
sockOps { /* ... */ }
lsm("bprm_check_security") { /* ... */ }
14 program types: tracepoint, raw_tracepoint, kprobe, kretprobe, fentry, fexit, XDP, TC, cgroup_skb, sockops, socket_filter, LSM, iter, sched_classifier.
Control Flow
// Null-checked map lookup
ifNonNull(map.lookup(key)) { entry ->
entry[Stats.count].atomicAdd(literal(1u, BpfScalar.U64))
}
// Conditionals with else-if / else
ifThen(ts gt literal(0u, BpfScalar.U64)) {
// ...
}.elseIf(ts eq literal(0u, BpfScalar.U64)) {
// ...
}.elseThen {
// ...
}
// Bounded loops (verifier-safe, #pragma unroll)
boundedLoop(literal(16u, BpfScalar.U32)) { i ->
// ...
}
Expressions
val a = declareVar("a", literal(42u, BpfScalar.U64))
val b = declareVar("b", ktimeGetNs())
val sum = a + b
val masked = a and literal(0xFFu, BpfScalar.U64)
val isZero = a eq literal(0u, BpfScalar.U64)
val shifted = a shr literal(3, BpfScalar.U64)
Helpers
31 BPF helpers with per-program-type availability enforcement:
| Category | Helpers |
|---|---|
| Process | getCurrentPidTgid(), getCurrentUidGid(), getCurrentComm(), getCurrentTask(), getCurrentTaskBtf() |
| Cgroup | getCurrentCgroupId() |
| Time | ktimeGetNs() |
| CPU | smpProcessorId() |
| Memory | probeReadKernel(), probeReadUser() |
| Map | mapLookupElem(), mapUpdateElem(), mapDeleteElem() |
| Ring buffer | ringbufOutput(), ringbufReserve(), ringbufSubmit(), ringbufDiscard() |
| Perf | perfEventOutput() |
| XDP | xdpAdjustHead(), xdpAdjustTail(), redirect() |
| TC/Socket | skbLoadBytes(), skbStoreBytes(), csumDiff() |
| Debug | tracePrintk() |
| Signal | sendSignal() |
| Stack | getStack(), getStackId(), tailCall() |
| Sync | spinLock(), spinUnlock() |
The DSL enforces helper availability per program type — calling getCurrentCgroupId() inside an XDP program is a validation error.
Validation
The 3-phase validation pipeline catches:
| Phase | What it checks |
|---|---|
| Construction | Map name length, maxEntries > 0, array keys = U32, ringbuf power-of-2 |
| Type Checker | Helper availability per program type, GPL requirements, expression types |
| Semantic Analyzer | Stack overflow (> 512 bytes), unreachable code, unchecked division, anti-patterns (large HASH without LRU) |
val result = program.validate()
// Check programmatically
result.errors.forEach { println("${it.code}: ${it.message}") }
result.warnings.forEach { println("${it.code}: ${it.message}") }
// Or fail fast
result.throwOnError()
Building
./gradlew build
Requires JDK 21+.
Running Tests
./gradlew test
242 tests covering types, IR, maps, programs, DSL builders, validation, codegen, BCC-style tools, and end-to-end integration.
Usage as Composite Build
Include kotlin-ebpf-dsl in your Gradle project as a composite build:
// settings.gradle.kts
val ebpfDslPath: String by settings
includeBuild(ebpfDslPath) {
dependencySubstitution {
substitute(module("dev.ebpf:kotlin-ebpf-dsl")).using(project(":"))
}
}
// build.gradle.kts
val bpfGenerator by sourceSets.creating
dependencies {
"bpfGeneratorImplementation"("dev.ebpf:kotlin-ebpf-dsl:0.1.0-SNAPSHOT")
}
tasks.register<JavaExec>("generateBpf") {
classpath = bpfGenerator.runtimeClasspath
mainClass.set("com.example.bpf.GenerateBpfKt")
}
See kpod-metrics for a full integration example with a 5-stage Docker build.
Project Structure
src/main/kotlin/dev/ebpf/dsl/
types/ BpfScalar, BpfStruct, BpfArrayType, StructField
ir/ BpfExpr, BpfStmt, Op, Variable (AST nodes)
maps/ MapType (17 types), MapDecl, MapCapabilities
programs/ ProgramType (14 types), BpfHelper, HelperRegistry (31 helpers)
api/ ebpf() builder, ProgramBodyBuilder, ExprHandle, MapHandle
validation/ TypeChecker, SemanticAnalyzer, Diagnostic
codegen/ CCodeGenerator, KotlinCodeGenerator
tools/ ToolRegistry, BCC-style programs (24 tools), CommonStructs
License
MIT