Files
python-bpf/docs/user-guide/structs.md

11 KiB

BPF Structs

Structs allow you to define custom data types for use in BPF programs. They provide a way to group related fields together and can be used as map values, event payloads, or local variables.

Defining Structs

Use the @bpf and @struct decorators to define a BPF struct:

from pythonbpf import bpf, struct
from ctypes import c_uint64, c_uint32

@bpf
@struct
class Event:
    timestamp: c_uint64
    pid: c_uint32
    cpu: c_uint32

Field Types

Structs support various field types from Python's ctypes module.

Integer Types

from ctypes import (
    c_int8, c_int16, c_int32, c_int64,
    c_uint8, c_uint16, c_uint32, c_uint64
)

@bpf
@struct
class Numbers:
    small_int: c_int8      # -128 to 127
    short_int: c_int16     # -32768 to 32767
    int_val: c_int32       # -2^31 to 2^31-1
    long_int: c_int64      # -2^63 to 2^63-1
    
    byte: c_uint8          # 0 to 255
    word: c_uint16         # 0 to 65535
    dword: c_uint32        # 0 to 2^32-1
    qword: c_uint64        # 0 to 2^64-1

String Types

Fixed-length strings are defined using str(N) where N is the size:

@bpf
@struct
class ProcessInfo:
    name: str(16)      # 16-byte string
    path: str(256)     # 256-byte string
Strings in BPF are fixed-length and null-terminated. The size includes the null terminator.

Pointer Types

from ctypes import c_void_p, c_char_p

@bpf
@struct
class Pointers:
    ptr: c_void_p      # Generic pointer
    str_ptr: c_char_p  # Character pointer

Nested Structs

Structs can contain other structs as fields:

@bpf
@struct
class Address:
    street: str(64)
    city: str(32)
    zip_code: c_uint32

@bpf
@struct
class Person:
    name: str(32)
    age: c_uint32
    address: Address   # Nested struct

Using Structs

As Local Variables

Create and use struct instances within BPF functions:

from pythonbpf import bpf, struct, section
from pythonbpf.helper import pid, ktime, comm
from ctypes import c_void_p, c_int64, c_uint64, c_uint32

@bpf
@struct
class Event:
    timestamp: c_uint64
    pid: c_uint32
    comm: str(16)

@bpf
@section("tracepoint/syscalls/sys_enter_execve")
def capture_event(ctx: c_void_p) -> c_int64:
    # Create an instance
    event = Event()
    
    # Set fields
    event.timestamp = ktime()
    event.pid = pid()
    # Note: comm() requires a buffer parameter to fill
    # comm(event.comm)  # Fills event.comm with process name
    
    # Use the struct
    print(f"Process with PID {event.pid}")
    
    return c_int64(0)

As Map Values

Use structs as values in maps for complex state storage:

from pythonbpf import bpf, struct, map, section
from pythonbpf.maps import HashMap
from ctypes import c_uint32, c_uint64

@bpf
@struct
class ProcessStats:
    syscall_count: c_uint64
    total_time: c_uint64
    max_latency: c_uint64

@bpf
@map
def stats() -> HashMap:
    return HashMap(
        key=c_uint32,
        value=ProcessStats,
        max_entries=1024
    )

@bpf
@section("tracepoint/syscalls/sys_enter_read")
def track_syscalls(ctx: c_void_p) -> c_int64:
    process_id = pid()
    
    # Lookup existing stats
    s = stats.lookup(process_id)
    
    if s:
        # Update existing stats
        s.syscall_count = s.syscall_count + 1
        stats.update(process_id, s)
    else:
        # Create new stats
        new_stats = ProcessStats()
        new_stats.syscall_count = c_uint64(1)
        new_stats.total_time = c_uint64(0)
        new_stats.max_latency = c_uint64(0)
        stats.update(process_id, new_stats)
    
    return c_int64(0)

With Perf Events

Send struct data to userspace using PerfEventArray:

from pythonbpf import bpf, struct, map, section
from pythonbpf.maps import PerfEventArray
from pythonbpf.helper import pid, ktime, comm
from ctypes import c_void_p, c_int64, c_uint32, c_uint64

@bpf
@struct
class ProcessEvent:
    timestamp: c_uint64
    pid: c_uint32
    ppid: c_uint32
    comm: str(16)

@bpf
@map
def events() -> PerfEventArray:
    return PerfEventArray(key_size=c_uint32, value_size=c_uint32)

@bpf
@section("tracepoint/sched/sched_process_fork")
def trace_fork(ctx: c_void_p) -> c_int64:
    event = ProcessEvent()
    event.timestamp = ktime()
    event.pid = pid()
    # Note: comm() requires a buffer parameter
    # comm(event.comm)  # Fills event.comm with process name
    
    # Send to userspace
    events.output(event)
    
    return c_int64(0)

With Ring Buffers

Ring buffers provide efficient event delivery:

from pythonbpf import bpf, struct, map, section
from pythonbpf.maps import RingBuffer

@bpf
@struct
class FileEvent:
    timestamp: c_uint64
    pid: c_uint32
    filename: str(256)

@bpf
@map
def events() -> RingBuffer:
    return RingBuffer(max_entries=4096)

@bpf
@section("tracepoint/syscalls/sys_enter_openat")
def trace_open(ctx: c_void_p) -> c_int64:
    event = FileEvent()
    event.timestamp = ktime()
    event.pid = pid()
    # event.filename would be populated from ctx
    
    events.output(event)
    
    return c_int64(0)

Field Access and Modification

Reading Fields

Access struct fields using dot notation:

event = Event()
ts = event.timestamp
process_id = event.pid

Writing Fields

Assign values to fields:

event = Event()
event.timestamp = ktime()
event.pid = pid()
# Note: comm() requires a buffer parameter
# comm(event.comm)  # Fills event.comm with process name

String Fields

String fields have special handling:

@bpf
@struct
class Message:
    text: str(64)

@bpf
def example(ctx: c_void_p) -> c_int64:
    msg = Message()
    
    # Assign string value
    msg.text = "Hello from BPF"
    
    # Use helper to get process name (requires buffer)
    # comm(msg.text)  # Fills msg.text with process name
    
    return c_int64(0)

StructType Class

PythonBPF provides a StructType class for working with struct metadata:

from pythonbpf.structs import StructType

# Define a struct
@bpf
@struct
class MyStruct:
    field1: c_uint64
    field2: c_uint32

# Access struct information (from userspace)
# This is typically used internally by the compiler

Complex Examples

Network Packet Event

from pythonbpf import bpf, struct, map, section
from pythonbpf.maps import RingBuffer
from pythonbpf.helper import ktime, XDP_PASS
from ctypes import c_void_p, c_int64, c_uint8, c_uint16, c_uint32, c_uint64

@bpf
@struct
class PacketEvent:
    timestamp: c_uint64
    src_ip: c_uint32
    dst_ip: c_uint32
    src_port: c_uint16
    dst_port: c_uint16
    protocol: c_uint8
    length: c_uint16

@bpf
@map
def packets() -> RingBuffer:
    return RingBuffer(max_entries=8192)

@bpf
@section("xdp")
def capture_packets(ctx: c_void_p) -> c_int64:
    pkt = PacketEvent()
    pkt.timestamp = ktime()
    # Parse packet data from ctx...
    
    packets.output(pkt)
    
    # XDP_PASS
    return XDP_PASS

Process Lifecycle Tracking

@bpf
@struct
class ProcessLifecycle:
    pid: c_uint32
    ppid: c_uint32
    start_time: c_uint64
    exit_time: c_uint64
    exit_code: c_int32
    comm: str(16)

@bpf
@map
def process_info() -> HashMap:
    return HashMap(
        key=c_uint32,
        value=ProcessLifecycle,
        max_entries=4096
    )

@bpf
@section("tracepoint/sched/sched_process_fork")
def track_fork(ctx: c_void_p) -> c_int64:
    process_id = pid()
    
    info = ProcessLifecycle()
    info.pid = process_id
    info.start_time = ktime()
    # Note: comm() requires a buffer parameter
    # comm(info.comm)  # Fills info.comm with process name
    
    process_info.update(process_id, info)
    
    return c_int64(0)

@bpf
@section("tracepoint/sched/sched_process_exit")
def track_exit(ctx: c_void_p) -> c_int64:
    process_id = pid()
    
    info = process_info.lookup(process_id)
    if info:
        info.exit_time = ktime()
        process_info.update(process_id, info)
    
    return c_int64(0)

Aggregated Statistics

@bpf
@struct
class FileStats:
    read_count: c_uint64
    write_count: c_uint64
    total_bytes_read: c_uint64
    total_bytes_written: c_uint64
    last_access: c_uint64

@bpf
@map
def file_stats() -> HashMap:
    return HashMap(
        key=str(256),  # Filename as key
        value=FileStats,
        max_entries=1024
    )

Memory Layout

Structs in BPF follow C struct layout rules:

  • Fields are laid out in order
  • Padding may be added for alignment
  • Size is rounded up to alignment

Example:

@bpf
@struct
class Aligned:
    a: c_uint8   # 1 byte
                 # 3 bytes padding
    b: c_uint32  # 4 bytes
    c: c_uint64  # 8 bytes
    # Total: 16 bytes
For optimal memory usage, order fields from largest to smallest to minimize padding.

Best Practices

  1. Use descriptive field names - Makes code self-documenting
  2. Order fields by size - Reduces padding and memory usage
  3. Use appropriate sizes - Don't use c_uint64 when c_uint32 suffices
  4. Document complex structs - Add comments explaining field purposes
  5. Keep structs focused - Each struct should represent one logical entity
  6. Use fixed-size strings - Always specify string lengths explicitly

Common Patterns

Timestamp + Data Pattern

@bpf
@struct
class TimestampedEvent:
    timestamp: c_uint64  # Always first for sorting
    # ... other fields

Identification Pattern

@bpf
@struct
class Identifiable:
    pid: c_uint32
    tid: c_uint32
    cpu: c_uint32
    # ... additional fields

Stats Aggregation Pattern

@bpf
@struct
class Statistics:
    count: c_uint64
    sum: c_uint64
    min: c_uint64
    max: c_uint64
    avg: c_uint64  # Computed in userspace

Troubleshooting

Struct Size Issues

If you encounter size-related errors:

  • Check for excessive padding
  • Verify field types are correct
  • Consider reordering fields

Initialization Problems

If fields aren't initialized correctly:

  • Always initialize all fields explicitly
  • Set default values where appropriate
  • Use helper functions for dynamic values

Type Mismatch Errors

If you get type errors:

  • Ensure field types match assignments
  • Check that imported types are from ctypes
  • Verify nested struct definitions

Reading Struct Data in Userspace

After capturing struct data, read it in Python:

import ctypes
from pylibbpf import BpfMap

# Define matching Python class
class Event(ctypes.Structure):
    _fields_ = [
        ("timestamp", ctypes.c_uint64),
        ("pid", ctypes.c_uint32),
        ("comm", ctypes.c_char * 16),
    ]

# Read from map
map_obj = BpfMap(b, stats)
for key, value_bytes in map_obj.items():
    value = Event.from_buffer_copy(value_bytes)
    print(f"PID: {value.pid}, Comm: {value.comm.decode()}")

Next Steps

  • Learn about {doc}maps for storing struct data
  • Explore {doc}helpers for populating struct fields
  • See {doc}compilation to understand how structs are compiled