@StructOps: implementing kernel struct_ops in Java¶
Blog series: Part 15 — Custom scheduler with sched_ext struct_ops · Part 19 — Scheduler cookbook
Javadoc: @StructOps
A @StructOps interface bundles the kernel's C-level callback table for a
bpf_struct_ops kind (sched-ext, TCP congestion control, qdisc, HID) into
a Java interface. Extending a class with the interface, overriding the
callbacks you care about, and loading via BPFProgram.load(YourClass.class)
compiles a struct_ops BPF program, attaches it, and registers it with the
kernel — all in one step.
Supported kinds¶
| Java interface | Kernel struct | Kernel since |
|---|---|---|
SchedExtOps |
sched_ext_ops |
6.12 |
TcpCongestionControl |
tcp_congestion_ops |
5.6 |
QdiscOps |
Qdisc_ops |
6.10 |
HidBpfOps |
hid_bpf_ops |
6.11 |
hello-ebpf's kernel floor is 6.14, so all four are available without compile-time gating.
The annotation¶
@StructOps(
value = "tcp_congestion_ops", // required — the kernel struct name
sectionPrefix = "struct_ops/", // optional — override for non-standard prefixes
instanceName = "" // optional — override the emitted map name
)
public interface TcpCongestionControl { … }
Values default to sensible sched-ext / TCP defaults; you rarely need to override.
Method-to-field mapping¶
- Java method name → C field name via camelCase → snake_case (
congAvoid→cong_avoid). - Return type and arg types validated against the BTF layout at compile time.
- Un-overridden default methods are omitted from the emitted struct — the kernel accepts NULL for optional slots.
- A
String name()method emits.name = "…"as a literal initializer, not a synthesized program.
@Sleepable¶
Some struct_ops methods are declared sleepable in their BTF metadata
(e.g. sched-ext's sched_init). Mark the Java override with
@Sleepable and the plugin emits SEC("struct_ops.s/<field>") instead
of SEC("struct_ops/<field>").
@BPF public abstract class MyScheduler extends BPFProgram implements SchedExtOps {
@Override @Sleepable public int schedInit() { return 0; }
}
Property placeholders (sched-ext only)¶
Sched-ext historically supports __property_<name> placeholders for the
flags, timeout_ms, and name fields — resolved at load time from
@PropertyDefinition annotations. These are preserved verbatim by the
struct-ops synthesizer; you don't need to declare them anywhere.
Runtime attach¶
BPFProgram.load(YourClass.class) handles the full lifecycle:
- Compile and load the BPF object file (existing behaviour).
- For each
@StructOpsinterface implemented by the class, callbpf_map__attach_struct_ops(…). - Populate
prog.structOpsInfo()with(kernelName, mapName, mapFd, bpfLinkId)entries — useful for diagnostics. - On
close(), all attached links are detached automatically.
If the running kernel doesn't advertise the struct_ops kind (via
Features.hasStructOps(name)), load throws
BPFLoadError.UnsupportedKernel(name, since) before touching the BPF
object.
Canonical examples¶
| Kind | Sample |
|---|---|
SchedExtOps |
MinimalScheduler.java |
TcpCongestionControl |
HelloCubicSample.java |
QdiscOps |
QdiscOpsSmokeTest.java |
HidBpfOps |
HidBpfOpsSmokeTest.java |
Escape hatches¶
- Hand-writing a
SEC("struct_ops/…")method directly on a@BPFclass (via@BPFFunction(section = "…", headerTemplate = "…")) still works. The plugin only intercepts methods that are@Overrides of a@StructOpsinterface — a manually annotated method sitting alongside is left untouched. - To emit a struct_ops kind not yet covered by a marker interface,
declare your own interface annotated
@StructOps("your_kind"). You must also add ayour_kind.jsonlayout underbpf-compiler-plugin/src/main/resources/struct-ops-layouts/for BTF validation — seestruct-ops-layouts/README.mdfor the schema.
TCP Congestion Control — worked example¶
The snippet below shows a minimal BIC/Cubic-inspired congestion controller.
It overrides ssthresh (reduce window on loss), congAvoid (additive
increase), and name (kernel registration name) — enough to be loaded and
recognised by the kernel.
@BPF(license = "GPL")
public abstract class MyCubic extends BPFProgram implements TcpCongestionControl {
// camelCase → snake_case: congAvoid → cong_avoid
@Override
public void congAvoid(Ptr<tcp_sock> tp, int ack, int acked) {
// Read current congestion window via CO-RE
int cwnd = tp.val().snd_cwnd;
int ssthresh = tp.val().snd_ssthresh;
if (cwnd < ssthresh) {
// Slow start: double each RTT
tp.val().snd_cwnd = cwnd + acked;
} else {
// Congestion avoidance: AIMD increase
tp.val().snd_cwnd = cwnd + 1;
}
}
@Override
public int ssthresh(Ptr<tcp_sock> tp) {
// Reduce window by half on loss (cubic-style floor at 2)
int cwnd = tp.val().snd_cwnd;
return Math.max(cwnd >> 1, 2);
}
// name() emits .name = "my_cc" — not compiled as a BPF program
@Override
public String name() { return "my_cc"; }
}
Key points:
- The arg is named
tp, notctx. TheBPF_PROGtracing macro that libbpf expands internally reserves the namectx; using it causes a compile error. See Common pitfalls below. tp.val().snd_cwnduses CO-RE field access; the compiler rewrites it to aBPF_CORE_READcall automatically.name()returns a string literal. The synthesiser emits.name = "my_cc"directly — do not annotatename()with@BPFFunction.- Sleepable callbacks (none in TCP CC, but common in sched-ext) need
@Sleepable; see the@Sleepablesection above and timers.md for background on sleepable contexts.
Common pitfalls¶
-
Args named
ctxcollide with theBPF_PROGmacro. libbpf'sBPF_PROGtracing macro injects a local variable namedctxfor every struct_ops program. If your Java method declares a parameter namedctx, clang sees a redeclaration and the build fails. Rename totp,tsk,hctx,sk, or any other non-ctxidentifier. -
Qdisc_opsrequires all five handlers. The kernel'sbpf_qdisc_reg()rejects registrations that leave any ofenqueue,dequeue,reset,destroy, orinitas NULL, returningENOENT. Override all five even if the body is a no-op. -
Forgetting
@Sleepableon a sleepable slot. If the kernel BTF marks a callback as sleepable (e.g.sched_ext_ops.init) but the emitted section header saysSEC("struct_ops/init")instead ofSEC("struct_ops.s/init"), the verifier rejects the program at load time. Add@Sleepableto the Java override; the plugin then emits the correct.s/suffix. See sched-ext/index.md for a sched-ext example. -
name()must not be annotated with@BPFFunction. The method maps to a literal.name = "…"field initializer in the kernel struct, not to a compiled BPF program. Annotating it with@BPFFunctionconfuses the synthesiser and produces an invalid object.
Further reading¶
- sched_ext — the most common
@StructOpsconsumer - Samples —
HelloCubicSample,QdiscOps,HidBpfOps - Diagnostics — verifier rejections and struct_ops error messages
- BPF struct_ops — docs.ebpf.io