Getting started
This guide takes you from nothing installed to writing your first jp4 controller. Budget 15 minutes; the longest step is the initial Gradle download. If you already have JDK 21+, Docker, and a checked-out copy of jp4, skip to Run your first example.
Prerequisites
- JDK 21 or newer.
java -versionshould print 21+. The project's CI matrix tests JDK 21 and JDK 25; both are supported. - Docker. The examples and the test suite use a containerised BMv2 device. No native BMv2 install is required.
That's it. jp4 ships its own Gradle wrapper; you do not need to install Gradle separately. The project depends on a small set of grpc-java + protobuf-java jars, which Gradle pulls on the first build.
Add jp4 to your project
jp4 is published as io.github.zhh2001:jp4 on Maven Central. Add it to your build:
// build.gradle.kts
dependencies {
implementation("io.github.zhh2001:jp4:1.0.0")
}Or if you prefer to consume jp4 from a checked-out clone (e.g. while contributing or tracking main), use a Gradle composite build — see how the examples/ directory wires this up via includeBuild("..").
Run your first example
Three commands and the BMv2 logs scroll past:
# 1. Get jp4 and start a BMv2 instance.
git clone https://github.com/zhh2001/jp4.git
cd jp4
docker run --rm -d --name jp4-bmv2 -p 50051:50051 \
p4lang/behavioral-model@sha256:7f28ab029368a1749a100c37ca4eaa6861322abb89885cfebb5c316326a45247 \
simple_switch_grpc \
--no-p4 --device-id 0 --log-console -L info \
-i 0@lo \
-- --grpc-server-addr 0.0.0.0:50051 --cpu-port 255
# 2. Run the L2 learning switch example.
cd examples
./gradlew :simple-l2-switch:run
# 3. Cleanup.
docker rm -f jp4-bmv2The example's output (verbatim from a real run):
[L2] connected as primary on 127.0.0.1:50051, pipeline pushed
[L2] inject src=aa:00:00:00:00:01 dst=ff:ff:ff:ff:ff:ff via simulated ingress 1
[L2] PacketIn src=AA:00:00:00:00:01 dst=FF:FF:FF:FF:FF:FF ingress=1
[L2] LEARN AA:00:00:00:00:01 → port 1 (entry installed)
[L2] inject src=bb:00:00:00:00:02 dst=ff:ff:ff:ff:ff:ff via simulated ingress 2
[L2] PacketIn src=BB:00:00:00:00:02 dst=FF:FF:FF:FF:FF:FF ingress=2
[L2] LEARN BB:00:00:00:00:02 → port 2 (entry installed)
[L2] learned table: {AA:00:00:00:00:01=1, BB:00:00:00:00:02=2}If you see the two LEARN lines and the final learned table: summary with two MAC entries, jp4 is talking to the device correctly.
What just happened
The simple-l2-switch example connects to BMv2 as primary, pushes the simple_l2.p4 pipeline, registers a PacketIn handler, and injects two synthetic Ethernet frames. The data-plane table starts empty, so each frame misses, BMv2 forwards it to the controller, and the handler writes a forwarding entry — that's the LEARN line. Read examples/simple-l2-switch/ for the annotated source.
Write your own controller — minimum viable
The smallest jp4 program that does useful work:
import io.github.zhh2001.jp4.P4Switch;
import io.github.zhh2001.jp4.entity.TableEntry;
import io.github.zhh2001.jp4.match.Match;
import io.github.zhh2001.jp4.pipeline.DeviceConfig;
import io.github.zhh2001.jp4.pipeline.P4Info;
import io.github.zhh2001.jp4.types.Ip4;
import java.nio.file.Path;
public final class HelloJp4 {
public static void main(String[] args) throws Exception {
P4Info p4info = P4Info.fromFile(Path.of("src/main/resources/p4/myprog.p4info.txtpb"));
DeviceConfig dc = DeviceConfig.Bmv2.fromFile(Path.of("src/main/resources/p4/myprog.json"));
try (P4Switch sw = P4Switch.connectAsPrimary("127.0.0.1:50051")
.bindPipeline(p4info, dc)) {
sw.insert(TableEntry.in("MyIngress.ipv4_lpm")
.match("hdr.ipv4.dstAddr", new Match.Lpm(Ip4.of("10.0.0.0").toBytes(), 8))
.action("MyIngress.forward").param("port", 1)
.build());
System.out.println("inserted route 10.0.0.0/8 → port 1");
}
}
}The shape applies to every jp4 controller:
- Load P4Info + device config from disk (or any byte source via
P4Info.fromBytes(...)). - Connect with
P4Switch.connectAsPrimary(address)(or the more flexibleP4Switch.connect(...).asPrimary()/.asSecondary()). - Push the pipeline with
bindPipeline(p4info, dc). The chain returnsP4Switch, so it composes left-to-right. - Operate — insert, modify, delete, read, send, receive.
- Close.
try-with-resourceshandles it;close()is idempotent.
Where to next
- Connection and arbitration — what primary / secondary actually mean, and how to handle mastership loss.
- Tables — the full
TableEntry/Matchbuilder surface, including LPM / ternary / range / optional and the read query. - Packet I/O — three styles for receiving PacketIn, sending PacketOut, and the
controller_packet_metadataround trip. - Error handling — the four exception types and what each one signals.