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
- Use descriptive field names - Makes code self-documenting
- Order fields by size - Reduces padding and memory usage
- Use appropriate sizes - Don't use
c_uint64whenc_uint32suffices - Document complex structs - Add comments explaining field purposes
- Keep structs focused - Each struct should represent one logical entity
- 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}
mapsfor storing struct data - Explore {doc}
helpersfor populating struct fields - See {doc}
compilationto understand how structs are compiled