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.c files and Kotlin MapReader classes
  • 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.

ToolDescriptionHook Type
execsnoop()Process exec/exit/fork countingtracepoint (sched)
oomkill()OOM kill event countingtracepoint (oom)
runqlat()CPU run queue latency histogramtracepoint (sched)
tcpconnect()TCP bytes, retransmits, connections, RTTkprobe + tracepoint
vfsstat()VFS read/write/open/fsync countingkprobe
biolatency()Block I/O latency histogramkprobe (blk-mq)
hardirqs()Hardware interrupt latency histogramtracepoint (irq)
softirqs()Software interrupt latency histogramtracepoint (irq)
cachestat()Page cache hit/add/dirty countingkprobe
cpudist()On-CPU time distribution histogramtracepoint (sched)
dcstat()Directory cache (dcache) hit/miss countingkprobe
tcpdrop()TCP packet drop countingkprobe
tcplife()TCP connection duration histogramtracepoint (sock)
syscount()System call countingraw_tracepoint
capable()Security capability check countingkprobe
filelife()File creation/deletion countingkprobe
slabtop()Slab/kmalloc allocation countingkprobe
writeback()Dirty page writeback event countingtracepoint (writeback)
bitesize()Block I/O request size distributionkprobe (blk-mq)
drsnoop()Direct memory reclaim event countingtracepoint (vmscan)
signalsnoop()Signal delivery countingtracepoint (signal)
solisten()Socket listen event countingkprobe
pidpersec()Process creation rate (fork/exec)tracepoint (sched)
tcpsynbl()TCP SYN backlog completion countingkprobe (tcp)

Each tool generates:

  • .bpf.c with proper SEC annotations, struct definitions, and LRU hash maps
  • Kotlin MapReader class with type-safe ByteBuffer deserialization

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:

CategoryHelpers
ProcessgetCurrentPidTgid(), getCurrentUidGid(), getCurrentComm(), getCurrentTask(), getCurrentTaskBtf()
CgroupgetCurrentCgroupId()
TimektimeGetNs()
CPUsmpProcessorId()
MemoryprobeReadKernel(), probeReadUser()
MapmapLookupElem(), mapUpdateElem(), mapDeleteElem()
Ring bufferringbufOutput(), ringbufReserve(), ringbufSubmit(), ringbufDiscard()
PerfperfEventOutput()
XDPxdpAdjustHead(), xdpAdjustTail(), redirect()
TC/SocketskbLoadBytes(), skbStoreBytes(), csumDiff()
DebugtracePrintk()
SignalsendSignal()
StackgetStack(), getStackId(), tailCall()
SyncspinLock(), 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:

PhaseWhat it checks
ConstructionMap name length, maxEntries > 0, array keys = U32, ringbuf power-of-2
Type CheckerHelper availability per program type, GPL requirements, expression types
Semantic AnalyzerStack 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