Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Whistler

Whistler is a Common Lisp compiler that produces eBPF bytecode. You write s-expressions, the compiler emits valid ELF object files, and your kernel loads them directly. There is no LLVM, no kernel headers, no CFFI. The entire compiler is self-contained SBCL code.

(defmap pkt-count :type :array
  :key-size 4 :value-size 8 :max-entries 1)

(defprog count-packets (:type :xdp :license "GPL")
  (incf (getmap pkt-count 0))
  XDP_PASS)

This compiles to 11 eBPF instructions and a valid BPF ELF object file.

Why Whistler

eBPF programs are small, verifier-constrained, and pattern-driven. They parse headers, validate bounds, look up state in maps, and emit events. These recurring patterns make eBPF a natural fit for a high-level language with real metaprogramming -- not string concatenation or preprocessor macros, but a compiler that understands the domain.

Real metaprogramming

Whistler programs are Common Lisp. CL macros are hygienic, composable, and operate on the AST at compile time. A with-tcp macro that parses Ethernet, IP, and TCP headers with bounds checks is not a preprocessor trick -- it is a function that generates verified code and participates in the compiler pipeline.

No C toolchain

The compiler is approximately 7,000 lines of SBCL. It includes its own ELF writer, BTF encoder, and peephole optimizer. You do not need clang, LLVM, libelf, or kernel headers installed.

Compiler-aware abstractions

Struct accessors, protocol helpers, and map operations are part of the language. The compiler optimizes them intentionally rather than recovering patterns after C lowering. (incf (getmap m k)) compiles to an atomic add on the map value -- the compiler knows the idiom.

Automatic CO-RE

Struct identity is preserved through the compilation pipeline. When you use import-kernel-struct to access kernel data structures, the compiler emits CO-RE relocations automatically. No manual BPF_CORE_READ calls.

Interactive REPL development

Compile, load, attach, inspect maps, iterate -- all from one Lisp image. Change a BPF program, recompile, and reload without leaving your REPL session.

Comparison

WhistlerC + clangBCC (Python)Aya (Rust)bpftrace
Toolchain size~3 MB (SBCL)~200 MB~100 MB~500 MB~50 MB
MetaprogrammingFull CL macros#definePython stringsproc_macronone
Output formatELF .oELF .oJIT loadedELF .oJIT loaded
Self-contained compileryesno (needs LLVM)no (needs kernel headers)no (needs LLVM)no
Interactive developmentREPLnoyesnoyes
Code quality vs clang -O2matches or beatsbaselinen/acomparablen/a

Whistler matches clang -O2 instruction counts on real programs. On the Cilium nodeport-lb4 load balancer (a complex production BPF program), Whistler produces 76 instructions to clang's 75.

The userspace loader

whistler/loader is a pure Common Lisp BPF loader. No libbpf, no CFFI. It parses .bpf.o files, creates maps via bpf() syscalls, loads programs into the kernel, attaches them (kprobe, uprobe, tracepoint, XDP, TC, cgroup), and consumes ring buffers -- all from SBCL.

You can also use inline BPF sessions that compile and load in the same form:

(whistler/loader:with-bpf-session ()
  ;; Kernel side -- compiled to eBPF at macroexpand time
  (bpf:map counter :type :hash
    :key-size 4 :value-size 8 :max-entries 1024)
  (bpf:prog trace (:type :kprobe
                    :section "kprobe/__x64_sys_execve"
                    :license "GPL")
    (incf (getmap counter 0))
    0)

  ;; Userspace side -- normal CL at runtime
  (bpf:attach trace "__x64_sys_execve")
  (loop (sleep 1)
        (format t "count: ~d~%" (bpf:map-ref counter 0))))

One file, one language, no intermediate artifacts.