Part II — Hands-on networking · Chapter 6

The P4 language

~50 min read5 interactive widgets4 plates

In this chapter

  1. What P4 is, and P414 versus P416
  2. Architectures and targets
  3. The V1Model and standard metadata
  4. The program template
  5. Types, headers and the parser
  6. The select statement
  7. Controls, actions and tables
  8. Match kinds
  9. Deparsing
  10. What P4 cannot do — and why
  11. Check your understanding

1. What P4 is, and P414 versus P416

P4 is a high-level programming language for network data planes. Four properties define it: it allows protocol flexibility, it specifies a packet processing pipeline, it is compiled and loaded into a target platform, and it is open and standardized. The name comes from the founding paper, P4: Programming Protocol-Independent Packet Processors (Bosshart, Daly, Gibb, Izzard, McKeown, Rexford, Schlesinger, Talayco, Vahdat, Varghese, Walker), ACM SIGCOMM CCR, July 2014.

There are two versions of the language, and the distinction matters:

P414P416 (the one studied here)
The original version. Assumed specific device capabilities. Good only for a subset of programmable switches/targets. A more mature and stable language definition. Does not assume device capabilities — they are defined via external libraries and an architecture definition. Good for many targets: switches or NICs, programmable or fixed-function.

The evolution from P414 to P416 is itself an instance of the top-down principle from chapter 5: P414 still baked in assumptions about the hardware; P416 pushes every hardware-specific fact out of the core language and into an architecture description, leaving the language itself protocol- and target-agnostic.

The P416 language elements are a small, deliberate set:

ElementWhat it provides
ParsersState machine, bitfield extraction
ControlsTables, actions, control-flow statements
ExpressionsBasic operations and operators
Data typesBitstrings, headers, structures, arrays
Architecture descriptionProgrammable blocks and their interfaces
Extern librariesSupport for specialized components

2. Architectures and targets

Two P416 terms carry the whole portability story, and confusing them is the single most common mistake:

TermDefinition
P4 TargetAn embodiment of a specific hardware implementation.
P4 ArchitectureProvides an interface to program a target via a set of P4-programmable components, externs and fixed components.

The division of labour is explicit: the P416 language and the P416 core library are community-developed; the extern libraries and the architecture definition are vendor-supplied. A P4 program is written against a specific architecture, which defines which blocks are available, the interfaces of each block, and their capabilities. Example architectures named in the slides are the V1Model, the SimpleSumeSwitch and the Portable Switch Architecture (PSA).

The consequence is the sentence to remember: architectures enable portability of P4 programs across different hardware and software targets. Ask "can I implement this feature with P4?" and the honest answer is it depends on the architecture — the language is flexible enough to express almost any match-action behaviour, but the specific capabilities (ternary vs. LPM vs. exact match, counters, registers) depend on the architecture. Ask "can I run my program on switch X from vendor Y?" and the answer is probably yes, if vendor Y provides a P4 compiler for the architecture you used.

Key idea

The architecture is the contract. It is the thing that lets the community own the language while vendors own the silicon: you write against the architecture, the vendor's compiler maps the architecture to the target. The PSA is the community-developed architecture that describes the common capabilities of a switch — 6 programmable blocks plus 2 fixed-function blocks — including counters, meters, stateful registers and hash functions beyond plain match+action.

3. The V1Model and standard metadata

The course teaches against the V1Model, implemented on top of BMv2's simple_switch target (the software switch used in the labs). Its pipeline has six blocks, and knowing their order is knowing where your code runs:

The V1Model pipeline: parser, checksum verification and ingress match-action, traffic manager, egress match-action and checksum update, deparser Parser Checksum verify+ Ingressmatch-action TrafficManager(fixed function) Egressmatch-action+ Checksum upd. Deparser Everything blue is P4-programmable. The Traffic Manager (queueing, replication/multicast, scheduling) is fixed function. Ingress runs before the buffer; egress runs after it — which is why queue depth is only visible in egress.
Plate 6.1 — The V1Model pipeline. Ingress decides forwarding before the traffic manager; egress can react to queueing state after it. This ordering is exactly what the ECN exercise in chapter 7 exploits.

Between the programmable blocks flows a struct the architecture provides: standard_metadata_t. Three of its fields are the ones you will touch constantly, and their distinction is a favourite exam point:

FieldMeaning
ingress_portThe port on which the packet arrived.
egress_specThe port to which the packet should be sent — you set this in ingress to forward.
egress_portThe port the packet will be sent out of — read-only, and only in the egress pipeline.
struct standard_metadata_t {
    bit<9>  ingress_port;
    bit<9>  egress_spec;
    bit<9>  egress_port;
    bit<32> packet_length;
    bit<32> enq_timestamp;
    bit<19> enq_qdepth;        // enqueue queue depth — used by ECN in ch. 7
    bit<48> ingress_global_timestamp;
    bit<16> mcast_grp;
    bit<1>  checksum_error;
    // ... and more
}

4. The program template

A V1Model program always has the same skeleton. The V1Switch(...) instantiation at the bottom wires the six blocks together in order — this is the program declaring which piece of code plays which role in the pipeline of the previous section.

#include <core.p4>
#include <v1model.p4>

/* HEADERS */
struct metadata { ... }
struct headers {
  ethernet_t ethernet;
  ipv4_t     ipv4;
}

/* PARSER */
parser MyParser(packet_in packet, out headers hdr,
                inout metadata meta, inout standard_metadata_t smeta) { ... }

control MyVerifyChecksum(inout headers hdr, inout metadata meta) { ... }
control MyIngress(inout headers hdr, inout metadata meta,
                  inout standard_metadata_t std_meta) { ... }
control MyEgress(inout headers hdr, inout metadata meta,
                 inout standard_metadata_t std_meta) { ... }
control MyComputeChecksum(inout headers hdr, inout metadata meta) { ... }
control MyDeparser(packet_out packet, in headers hdr) { ... }

/* SWITCH — bind the blocks into the V1Model pipeline, in order */
V1Switch(
  MyParser(),
  MyVerifyChecksum(),
  MyIngress(),
  MyEgress(),
  MyComputeChecksum(),
  MyDeparser()
) main;

The simplest complete program, the "hello world" of P4, forwards by swapping ports 1 and 2 directly in the ingress control:

5. Types, headers and the parser

P4's types are deliberately spartan, because everything about a packet is bits at fixed offsets:

TypeMeaning
bit<n>Unsigned integer (bitstring) of size n; bit is the same as bit<1>.
int<n>Signed integer of size n (n ≥ 2).
varbit<n>Variable-length bitstring (maximum n) — used for the Modbus payload in chapters 10–11.
headerAn ordered, byte-aligned collection of members; can be valid or invalid, with isValid(), setValid(), setInvalid().
structUnordered collection of members (no alignment restriction).
typedefAn alternative name for a type.
typedef bit<48> macAddr_t;
typedef bit<32> ip4Addr_t;

header ethernet_t {
  macAddr_t dstAddr;
  macAddr_t srcAddr;
  bit<16>   etherType;
}
header ipv4_t {
  bit<4>    version;   bit<4>    ihl;
  bit<8>    diffserv;  bit<16>   totalLen;
  bit<16>   identification; bit<3> flags; bit<13> fragOffset;
  bit<8>    ttl;       bit<8>    protocol; bit<16> hdrChecksum;
  ip4Addr_t srcAddr;   ip4Addr_t dstAddr;
}

The parser as a state machine

Parsers are functions that map packets into headers and metadata, written in a state machine style. Every parser has three predefined states — start, accept, reject — and the programmer may define others. In each state you execute zero or more statements and then transition to another state; loops are permitted. The platform initialises user metadata to 0.

A parser state machine: start extracts ethernet, branches on etherType to parse_ipv4 or accept, and parse_ipv4 extracts ipv4 then accepts; a reject state is also shown start parse_ethernet extract(ethernet) parse_ipv4 extract(ipv4) accept reject etherType = 0x800 default The select statement branches on bits just parsed — here, etherType decides whether an IPv4 header follows.
Plate 6.2 — A parser state machine for the basic router. The select on etherType is the branch; every real protocol stack is a tree of such branches.

6. The select statement

P416 has a select statement to branch inside a parser. The slides describe it precisely: it is similar to case statements in C or Java, but without fall-through behaviour — no break is needed. In parsers it is often necessary to branch based on some of the bits just parsed; for example, etherType determines the format of the rest of the packet. Match patterns can be literals or simple computations such as masks.

state start {
  transition parse_ethernet;
}
state parse_ethernet {
  packet.extract(hdr.ethernet);
  transition select(hdr.ethernet.etherType) {
    0x800:   parse_ipv4;   // it is IPv4, keep parsing
    default: accept;       // not IPv4, we are done
  }
}
Editor's note

The absence of fall-through is not a stylistic detail. A parser must terminate in bounded time on every packet (the line-rate budget of chapter 5), and C-style fall-through invites the kind of accidental control flow that a fixed per-packet cost cannot afford. The parser reads like a case statement but behaves like a decision tree: exactly one branch is taken.

7. Controls, actions and tables

Controls are similar to C functions without loops. They can declare variables, create tables, instantiate externs, and their functionality is specified by the code in the apply statement. They represent everything expressible as a directed acyclic graph: match-action pipelines, deparsers, checksum updates.

Actions

Actions are also very similar to C functions, and their parameters have a direction. This is the concept that unifies the data plane and the control plane, so it is worth stating carefully:

// directionless dstAddr and port are filled in by the control plane
// when it installs a table entry
action ipv4_forward(macAddr_t dstAddr, bit<9> port) {
  standard_metadata.egress_spec = port;
  hdr.ethernet.dstAddr = dstAddr;
  hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
}
action drop() { mark_to_drop(); }

Tables

The table is the fundamental unit of a match-action pipeline. A table specifies what data to match on and the match kind, a list of possible actions, and optional properties (size, default action, static entries). Each table contains one or more entries (rules), and each entry has a specific key to match on, a single action executed on a match, and action data (possibly empty).

table ipv4_lpm {
  key = {
    hdr.ipv4.dstAddr: lpm;      // longest-prefix match
  }
  actions = {
    ipv4_forward;
    drop;
    NoAction;
  }
  size = 1024;
  default_action = NoAction();
}

The division of labour is the crucial part, and it is the same control/data split from chapter 3, now made concrete: the data-plane P4 program defines the format of the table (key fields, actions, action data) and performs the lookup and executes the chosen action; the control plane populates the entries with specific information, based on configuration, automatic discovery, or protocol calculations.

A control applies its tables in its apply block:

control MyIngress(inout headers hdr, inout metadata meta,
                  inout standard_metadata_t std_meta) {
  table ipv4_lpm { /* ... as above ... */ }
  apply {
    if (hdr.ipv4.isValid()) {
      ipv4_lpm.apply();
    }
  }
}
Careful — two FAQ facts worth memorising

From the lab FAQ: you cannot apply a table multiple times in a P4 program (except via resubmit/recirculate), and you cannot modify table entries from your P4 program (except for direct counters) — entries are the control plane's job, or you use registers. These are not arbitrary restrictions; they are what keeps the per-packet cost constant.

8. Match kinds

The type match_kind is special in P4. The standard library core.p4 defines three, the V1Model architecture adds two more, and other architectures may define their own — a perfect small illustration of the "architecture defines capabilities" principle from section 2.

Defined inMatch kinds
core.p4 (standard)exact, ternary, lpm (longest-prefix match)
v1model.p4 (architecture)range, selector
some other architecturee.g. regexp, fuzzy

The key must equal the value exactly. Used for things like "packets arriving on ingress port 1", where there is nothing to generalise. The Modbus security table in chapter 11 matches egress_spec exactly.

The key is matched against a value and a mask — only the masked bits must agree. This is the TCAM-backed matching of chapter 5's ACLs, used when rules overlap and priority decides. The simple_switch_CLI syntax uses value&&&mask.

Longest-prefix match: the entry with the longest matching address prefix wins. This is exactly how a router chooses a route (chapter 4's "longest IP prefix"), which is why the routing table ipv4_lpm uses it.

9. Deparsing

The deparser assembles the headers back into a well-formed packet. It is expressed as a control function — no new construct is needed — and it uses the packet_out extern from core.p4, whose emit(hdr) serialises a header only if it is valid.

control MyDeparser(packet_out packet, in headers hdr) {
  apply {
    packet.emit(hdr.ethernet);
    packet.emit(hdr.ipv4);   // emitted only if still valid
  }
}

The design advantage stated in the slides is subtle but important: making deparsing explicit decouples it from parsing. You are not obliged to emit headers in the order you parsed them, or to emit everything you parsed; a header you setInvalid() during processing simply vanishes from the output. This is precisely the mechanism the tunnelling exercise in chapter 7 uses to push and pop a tunnel header — validity in, validity out.

Walk one packet through the full V1Model pipeline below, from parse to deparse.

10. What P4 cannot do — and why

The most instructive slides in the P4 deck are the ones about limitations, because the limits are not incidental — they are the price of running at line rate. The core P4 language is very small: highly portable, but very limited in expressivity.

What is missing from the language

Floating point; pointers and references; data structures and recursive data types; dynamic memory management; loops and iterators (except the parser state machine); recursion; threads. The slides give the reason in one arrow: ⇒ constant work per byte of header. Everything absent is something whose cost cannot be bounded per packet.

What cannot be done in pure P4

Cannot be done in pure P4How it is done instead
Multicast, broadcast, queueing, scheduling, multiplexingBy the target device, controlled by P4 metadata.
Persistent state across packets (per-flow state)External objects: registers, counters, meters (chapter 7).
Communication to the control planeExternal objects: learning providers, digests (chapter 8).
Payload processing, e.g. encryptionExterns — this is exactly the AES/HMAC extern of the industrial lab (chapter 11).
Packet generationThe control plane, or external objects.
TimersNot in P4 itself.
TCP-style reassembly, trailers, inter-packet operations (fragmentation/reassembly)Not currently done.
For the exam

Two questions to expect. Why can't P4 do X? — because X's cost is not bounded per byte of header, and the data plane's whole promise is constant per-packet work. If P4 can't do X, how is X done? — through the target (multicast, queueing), through externs (persistent state via registers/counters/meters, payload crypto), or through the control plane (packet generation, table population). Section 6.2's answer — the architecture defines capabilities — is the same answer wearing a different hat.

The closing argument of the deck answers "why P416?" and it reads as a list of software-engineering virtues rather than networking ones: clearly defined semantics (you can describe what your data plane does), expressive (a wide range of architectures through a standard methodology), high-level and target-independent (the compiler manages resources and the hardware), type-safe (enforces good design and eliminates "stupid" bugs), agile (high-speed devices become as flexible as any software), and insight (freely mixing packet headers and intermediate results).

Check your understanding

What is P4, and what distinguishes P416 from P414?

P4 is a high-level language for network data planes: protocol-flexible, specifying a packet-processing pipeline, compiled and loaded into a target, open and standardized. P414 assumed specific device capabilities and suited only a subset of targets. P416 does not assume device capabilities — they are defined via external libraries and an architecture definition — so it is good for many targets, programmable or fixed-function.

Distinguish a P4 target from a P4 architecture.

A target is an embodiment of a specific hardware implementation. An architecture provides an interface to program a target via P4-programmable components, externs and fixed components, and defines which blocks are available, their interfaces and their capabilities. The language and core library are community-developed; extern libraries and the architecture definition are vendor-supplied. Architectures enable portability across targets.

"Can I implement feature X in P4?" What is the correct answer?

It depends on the architecture. The language is flexible enough to express almost any match-action behaviour, but specific capabilities — ternary vs. LPM vs. exact match, counters, registers — depend on the architecture. And "can I run my program on switch X from vendor Y?" is probably yes, if vendor Y provides a P4 compiler for the architecture you used.

Distinguish ingress_port, egress_spec and egress_port.

ingress_port is the port the packet arrived on; egress_spec is the port it should be sent to, which you set in ingress to forward; egress_port is the port it will leave from, read-only and only in the egress pipeline. Forwarding is "set egress_spec"; the traffic manager turns egress_spec into egress_port.

List the V1Model blocks in order and name the fixed-function one.

Parser, checksum verification + ingress match-action, Traffic Manager, egress match-action + checksum update, deparser. The Traffic Manager (queueing, replication/multicast, scheduling) is the fixed-function block; everything else is P4-programmable. Ingress runs before the buffer, egress after it.

What is the select statement, and what makes it unlike a C switch?

It branches inside a parser based on bits just parsed (for example etherType deciding whether IPv4 follows). Unlike a C switch it has no fall-through — no break is needed. Patterns can be literals or simple computations such as masks. Exactly one branch is taken.

Explain directional versus directionless action parameters.

Directional parameters come from the data plane; actions called directly use only these. Directionless parameters come from the control plane; actions used in tables typically use these (and may also use directional ones). This is how a single action body serves both planes: ipv4_forward(dstAddr, port) gets its port filled in by the control plane when it installs a table entry.

What does a table specify, and who fills its entries?

A table specifies the match key and match kind, the list of possible actions, and optional properties (size, default action, static entries). Each entry has a key, a single action, and action data. The P4 program defines the table format and performs the lookup and action; the control plane populates the entries, based on configuration, discovery or protocol calculations.

Two things you cannot do with a table in your P4 program?

You cannot apply a table multiple times (except via resubmit/recirculate), and you cannot modify table entries from the P4 program (except for direct counters) — entry population is the control plane's job, or you use registers. Both restrictions preserve constant per-packet cost.

Name the match kinds and where each is defined.

core.p4 defines exact, ternary and lpm; v1model.p4 adds range and selector; other architectures may add their own (e.g. regexp, fuzzy). lpm is how a router chooses a route; ternary is value+mask, TCAM-backed, for overlapping ACL rules; exact is equality.

What does the deparser do, and why is decoupling it from parsing useful?

The deparser reassembles headers into a well-formed packet, as a control using packet.emit(hdr), which serialises a header only if it is valid. Decoupling deparsing from parsing means you can emit headers in a different order, omit some, or add new ones by managing their validity bits — exactly the mechanism used to push and pop a tunnel header.

Why can't pure P4 do encryption, persistent state or multicast — and how are they done?

Because their cost is not bounded per byte of header, which would break line-rate processing (the language also lacks loops, recursion, floating point and dynamic memory for the same reason). Instead: payload processing like encryption is done via externs; persistent per-flow state via registers, counters and meters; multicast, queueing and scheduling by the target device controlled through P4 metadata; packet generation and table population by the control plane.