mirror of
https://github.com/varun-r-mallya/Python-BPF.git
synced 2026-02-07 21:50:55 +00:00
Add comprehensive Sphinx documentation structure and content
Co-authored-by: r41k0u <76248539+r41k0u@users.noreply.github.com>
This commit is contained in:
542
docs/user-guide/structs.md
Normal file
542
docs/user-guide/structs.md
Normal file
@ -0,0 +1,542 @@
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
@bpf
|
||||
@struct
|
||||
class ProcessInfo:
|
||||
name: str(16) # 16-byte string
|
||||
path: str(256) # 256-byte string
|
||||
```
|
||||
|
||||
```{note}
|
||||
Strings in BPF are fixed-length and null-terminated. The size includes the null terminator.
|
||||
```
|
||||
|
||||
### Pointer Types
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```python
|
||||
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()
|
||||
event.comm = comm()
|
||||
|
||||
# Use the struct
|
||||
print(f"Process {event.comm} with PID {event.pid}")
|
||||
|
||||
return c_int64(0)
|
||||
```
|
||||
|
||||
### As Map Values
|
||||
|
||||
Use structs as values in maps for complex state storage:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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()
|
||||
event.comm = comm()
|
||||
|
||||
# Send to userspace
|
||||
events.output(event)
|
||||
|
||||
return c_int64(0)
|
||||
```
|
||||
|
||||
### With Ring Buffers
|
||||
|
||||
Ring buffers provide efficient event delivery:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
event = Event()
|
||||
ts = event.timestamp
|
||||
process_id = event.pid
|
||||
```
|
||||
|
||||
### Writing Fields
|
||||
|
||||
Assign values to fields:
|
||||
|
||||
```python
|
||||
event = Event()
|
||||
event.timestamp = ktime()
|
||||
event.pid = pid()
|
||||
event.comm = comm()
|
||||
```
|
||||
|
||||
### String Fields
|
||||
|
||||
String fields have special handling:
|
||||
|
||||
```python
|
||||
@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
|
||||
msg.text = comm()
|
||||
|
||||
return c_int64(0)
|
||||
```
|
||||
|
||||
## StructType Class
|
||||
|
||||
PythonBPF provides a `StructType` class for working with struct metadata:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
from pythonbpf import bpf, struct, map, section
|
||||
from pythonbpf.maps import RingBuffer
|
||||
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_uint32:
|
||||
pkt = PacketEvent()
|
||||
pkt.timestamp = ktime()
|
||||
# Parse packet data from ctx...
|
||||
|
||||
packets.output(pkt)
|
||||
|
||||
# XDP_PASS
|
||||
return c_uint32(2)
|
||||
```
|
||||
|
||||
### Process Lifecycle Tracking
|
||||
|
||||
```python
|
||||
@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()
|
||||
info.comm = comm()
|
||||
|
||||
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
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```python
|
||||
@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
|
||||
```
|
||||
|
||||
```{tip}
|
||||
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
|
||||
|
||||
```python
|
||||
@bpf
|
||||
@struct
|
||||
class TimestampedEvent:
|
||||
timestamp: c_uint64 # Always first for sorting
|
||||
# ... other fields
|
||||
```
|
||||
|
||||
### Identification Pattern
|
||||
|
||||
```python
|
||||
@bpf
|
||||
@struct
|
||||
class Identifiable:
|
||||
pid: c_uint32
|
||||
tid: c_uint32
|
||||
cpu: c_uint32
|
||||
# ... additional fields
|
||||
```
|
||||
|
||||
### Stats Aggregation Pattern
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user