26 Commits

Author SHA1 Message Date
87908e8713 Remove backslash for multiline conditions in maps 2025-09-30 19:57:58 +05:30
0f3cc434a3 Fix return for unknown map types 2025-09-30 19:55:37 +05:30
d943b78a25 Add __init__ to maps to improve imports 2025-09-30 19:51:28 +05:30
744aa3fbdf Use logger instead of prints in map_pass 2025-09-30 19:44:29 +05:30
9fa362ec6a Remove global map_sym_tab 2025-09-30 19:29:45 +05:30
ca51b7ce01 Fix invalid member func for MapProcessorRegistry 2025-09-30 19:10:25 +05:30
2e005f6eb5 Create MapProcessorRegistry to add more maps 2025-09-30 19:08:56 +05:30
cbc6b93cd8 restructure maps dir, fix imports 2025-09-30 19:01:46 +05:30
9ae1b6ce15 Fix usage of map_params to use BPFMapType 2025-09-30 13:28:18 +05:30
1a1f2cf634 create BPFMapType enum 2025-09-30 12:20:07 +05:30
0d691865bc Add is_map in maps_pass 2025-09-30 11:44:12 +05:30
0fb1cafd20 Revise README for clarity and additional details
Updated README.md to enhance project description, installation instructions, and usage examples.
2025-09-30 03:05:27 +05:30
1adf7d7fcc Merge pull request #5 from pythonbpf/struct_refactor
Struct refactor
2025-09-30 02:01:58 +05:30
3ded17bf8b Fix size calc for ArrayType in structs 2025-09-30 01:59:18 +05:30
715442d7bf fix struct usage in expr_pass 2025-09-30 01:59:17 +05:30
e464a3fdd5 fix struct usage in handle_helper_functions 2025-09-30 01:59:16 +05:30
fed4c179e6 fix struct usage in functions_pass 2025-09-30 01:59:15 +05:30
32c22c3148 fix struct imports 2025-09-30 01:59:14 +05:30
4557b094e1 Use StructType in struct_pass, fix indexing 2025-09-30 01:59:13 +05:30
84500305db Move structs_pass under structs, create StructType 2025-09-30 01:59:12 +05:30
0d21f84529 Remove redundant functions from struct_pass 2025-09-30 01:59:11 +05:30
5bcc02a931 add parser_struct_fields 2025-09-30 01:59:10 +05:30
fe91a176e2 fix structs_proc 2025-09-30 01:59:09 +05:30
083ee21e38 structs_pass cleanup 2025-09-30 01:59:06 +05:30
ea5a1ab2de add jupyter notebook support 2025-09-27 12:24:49 +05:30
de5cc438ab Allow access from struct fields 2025-09-26 23:02:51 +05:30
16 changed files with 898 additions and 210 deletions

3
.gitignore vendored
View File

@ -5,4 +5,5 @@
.vscode/
__pycache__/
*.ll
*.o
*.o
.ipynb_checkpoints/

202
README.md
View File

@ -1,77 +1,183 @@
# Python-BPF
<p align="center">
<a href="https://www.python.org/downloads/release/python-3080/"><img src="https://img.shields.io/badge/python-3.8-blue.svg"></a>
<a href="https://pypi.org/project/pythonbpf"><img src="https://badge.fury.io/py/pythonbpf.svg"></a>
</p>
This is an LLVM IR generator for eBPF programs in Python. We use llvmlite to generate LLVM IR from pure Python. This is then compiled to LLVM object files, which can be loaded into the kernel for execution. We do not rely on BCC to do our compilation.
Python-BPF is an LLVM IR generator for eBPF programs written in Python. It uses [llvmlite](https://github.com/numba/llvmlite) to generate LLVM IR and then compiles to LLVM object files. These object files can be loaded into the kernel for execution. Unlike BCC, Python-BPF performs compilation without relying on its infrastructure.
# DO NOT USE IN PRODUCTION. IN DEVELOPMENT.
> **Note**: This project is under active development and not ready for production use.
## Video Demo
[Video demo for code under demo/](https://youtu.be/eMyLW8iWbks)
---
## Slide Deck
[Slide deck explaining the project](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
## Overview
## Installation
- Have `clang` installed.
- `pip install pythonbpf`
* Generate eBPF programs directly from Python.
* Compile to LLVM object files for kernel execution.
* Built with `llvmlite` for IR generation.
* Supports maps, helpers, and global definitions for BPF.
* Companion project: [pylibbpf](https://github.com/pythonbpf/pylibbpf), which provides the bindings required for object loading and execution.
---
## Installation
Dependencies:
* `clang`
* Python ≥ 3.8
Install via pip:
```bash
pip install pythonbpf pylibbpf
```
---
## Example Usage
## Usage
```python
# pythonbpf_example.py
from pythonbpf import bpf, map, bpfglobal, section, compile
from pythonbpf.helpers import bpf_ktime_get_ns
import time
from pythonbpf import bpf, map, section, bpfglobal, BPF
from pythonbpf.helpers import pid
from pythonbpf.maps import HashMap
from pylibbpf import *
from ctypes import c_void_p, c_int64, c_uint64, c_int32
import matplotlib.pyplot as plt
from ctypes import c_void_p, c_int64, c_int32, c_uint64
# This program attaches an eBPF tracepoint to sys_enter_clone,
# counts per-PID clone syscalls, stores them in a hash map,
# and then plots the distribution as a histogram using matplotlib.
# It provides a quick view of process creation activity over 10 seconds.
@bpf
@map
def last() -> HashMap:
return HashMap(key=c_uint64, value=c_uint64, max_entries=1)
def hist() -> HashMap:
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
@bpf
@section("tracepoint/syscalls/sys_enter_execve")
def hello(ctx: c_void_p) -> c_int32:
print("entered")
return c_int32(0)
@bpf
@section("tracepoint/syscalls/sys_exit_execve")
def hello_again(ctx: c_void_p) -> c_int64:
print("exited")
key = 0
tsp = last().lookup(key)
print(tsp)
ts = bpf_ktime_get_ns()
@section("tracepoint/syscalls/sys_enter_clone")
def hello(ctx: c_void_p) -> c_int64:
process_id = pid()
one = 1
prev = hist().lookup(process_id)
if prev:
previous_value = prev + 1
print(f"count: {previous_value} with {process_id}")
hist().update(process_id, previous_value)
return c_int64(0)
else:
hist().update(process_id, one)
return c_int64(0)
@bpf
@bpfglobal
def LICENSE() -> str:
return "GPL"
def some_normal_function():
print("normal function")
# compiles and dumps object file in the same directory
compile()
b = BPF()
b.load_and_attach()
hist = BpfMap(b, hist)
print("Recording")
time.sleep(10)
counts = list(hist.values())
plt.hist(counts, bins=20)
plt.xlabel("Clone calls per PID")
plt.ylabel("Frequency")
plt.title("Syscall clone counts")
plt.show()
```
- Run `python pythonbpf_example.py` to get the compiled object file that can be then loaded into the kernel.
---
## Architecture
Python-BPF provides a complete pipeline to write, compile, and load eBPF programs in Python:
1. **Python Source Code**
* Users write BPF programs in Python using decorators like `@bpf`, `@map`, `@section`, and `@bpfglobal`.
* Maps (hash maps), helpers (e.g., `ktime`, `deref`), and tracepoints are defined using Python constructs, preserving a syntax close to standard Python.
2. **AST Generation**
* The Python `ast` module parses the source code into an Abstract Syntax Tree (AST).
* Decorators and type annotations are captured to determine BPF maps, tracepoints, and global variables.
3. **LLVM IR Emission**
* The AST is transformed into LLVM Intermediate Representation (IR) using `llvmlite`.
* IR captures BPF maps, control flow, assignments, and calls to helper functions.
* Debug information is emitted for easier inspection.
4. **LLVM Object File Compilation**
* The LLVM IR (`.ll`) is compiled into a BPF target object file (`.o`) using `llc -march=bpf -O2`.
* This produces a kernel-loadable ELF object file containing the BPF bytecode.
5. **libbpf Integration (via pylibbpf)**
* The compiled object file can be loaded into the kernel using `pylibbpf`.
* Maps, tracepoints, and program sections are initialized, and helper functions are resolved.
* Programs are attached to kernel hooks (e.g., syscalls) for execution.
6. **Execution in Kernel**
* The kernel executes the loaded eBPF program.
* Hash maps, helpers, and global variables behave as defined in the Python source.
* Output can be read via BPF maps, helper functions, or trace printing.
This architecture eliminates the need for embedding C code in Python, allowing full Python tooling support while generating true BPF object files ready for kernel execution.
---
## Development
- Make a virtual environment and activate it using `python3 -m venv .venv && source .venv/bin/activate`.
- Run `make install` to install the required dependencies.
- Run `make` to see the compilation output of the example.
- Run `check.sh` to check if generated object file passes through the verifier inside the examples directory.
- Run `make` in the `examples/c-form` directory to modify the example C BPF program to check the actual LLVM IR generated by clang.
### Development Notes
- Run ` ./check.sh check execve2.o;` in examples folder to check if the object code passes the verifier.
- Run ` ./check.sh run execve2.o;` in examples folder to run the object code using `bpftool`.
1. Create a virtual environment and activate it:
```bash
python3 -m venv .venv
source .venv/bin/activate
```
2. Install dependencies:
```bash
make install
```
3. Build and test examples:
```bash
make
```
4. Verify an object file with the kernel verifier:
```bash
./check.sh check execve2.o
```
5. Run an object file using `bpftool`:
```bash
./check.sh run execve2.o
```
6. Explore LLVM IR output from clang in `examples/c-form` by running `make`.
---
## Resources
* [Video demonstration](https://youtu.be/eMyLW8iWbks)
* [Slide deck](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
---
## Authors
- [@r41k0u](https://github.com/r41k0u)
- [@varun-r-mallya](https://github.com/varun-r-mallya)
* [@r41k0u](https://github.com/r41k0u)
* [@varun-r-mallya](https://github.com/varun-r-mallya)
---

397
demo/clone-matplotlib.ipynb Normal file

File diff suppressed because one or more lines are too long

47
examples/c-form/ex8.bpf.c Normal file
View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <linux/blkdev.h>
#define __TARGET_ARCH_aarch64
#define u64 unsigned long long
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, struct request *);
__type(value, u64);
} start SEC(".maps");
SEC("kprobe/blk_start_request")
int BPF_KPROBE(trace_start_req, struct request *req)
{
u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start, &req, &ts, BPF_ANY);
return 0;
}
SEC("kprobe/blk_mq_start_request")
int BPF_KPROBE(trace_start_mq, struct request *req)
{
u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start, &req, &ts, BPF_ANY);
return 0;
}
SEC("kprobe/blk_account_io_completion")
int BPF_KPROBE(trace_completion, struct request *req)
{
u64 *tsp, delta;
tsp = bpf_map_lookup_elem(&start, &req);
if (tsp) {
delta = bpf_ktime_get_ns() - *tsp;
bpf_printk("%d %x %d\n", req->__data_len,
req->cmd_flags, delta / 1000);
bpf_map_delete_elem(&start, &req);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";

View File

@ -26,10 +26,10 @@ def hello(ctx: c_void_p) -> c_int32:
ts = ktime()
process_id = pid()
strobj = "hellohellohello"
dataobj.pid = process_id
dataobj.ts = ts
dataobj.pid = pid()
dataobj.ts = ktime()
# dataobj.comm = strobj
print(f"clone called at {ts} by pid {process_id}, comm {strobj}")
print(f"clone called at {dataobj.ts} by pid {dataobj.pid}, comm {strobj}")
events.output(dataobj)
return c_int32(0)

View File

@ -3,7 +3,7 @@ from llvmlite import ir
from .expr_pass import eval_expr
def bpf_ktime_get_ns_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, local_var_metadata=None):
def bpf_ktime_get_ns_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
"""
Emit LLVM IR for bpf_ktime_get_ns helper function call.
"""
@ -63,7 +63,7 @@ def bpf_map_lookup_elem_emitter(call, map_ptr, module, builder, func, local_sym_
return result, ir.PointerType()
def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, local_var_metadata=None):
def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
if not hasattr(func, "_fmt_counter"):
func._fmt_counter = 0
@ -101,10 +101,42 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None,
else:
raise NotImplementedError(
"Only integer and pointer types are supported in formatted values.")
print("Formatted value variable:", var_ptr, var_type)
else:
raise ValueError(
f"Variable {value.value.id} not found in local symbol table.")
elif isinstance(value.value, ast.Attribute):
# object field access from struct
if isinstance(value.value.value, ast.Name) and local_sym_tab and value.value.value.id in local_sym_tab:
var_name = value.value.value.id
field_name = value.value.attr
if local_var_metadata and var_name in local_var_metadata:
var_type = local_var_metadata[var_name]
if var_type in struct_sym_tab:
struct_info = struct_sym_tab[var_type]
if field_name in struct_info.fields:
field_type = struct_info.field_type(
field_name)
if isinstance(field_type, ir.IntType):
fmt_parts.append("%lld")
exprs.append(value.value)
elif field_type == ir.PointerType(ir.IntType(8)):
fmt_parts.append("%s")
exprs.append(value.value)
else:
raise NotImplementedError(
"Only integer and pointer types are supported in formatted values.")
else:
raise ValueError(
f"Field {field_name} not found in struct {var_type}.")
else:
raise ValueError(
f"Struct type {var_type} for variable {var_name} not found in struct symbol table.")
else:
raise ValueError(
f"Metadata for variable {var_name} not found in local variable metadata.")
else:
raise ValueError(
f"Variable {value.value.value.id} not found in local symbol table.")
else:
raise NotImplementedError(
"Only simple variable names are supported in formatted values.")
@ -119,12 +151,12 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None,
fmt_gvar = ir.GlobalVariable(
module, ir.ArrayType(ir.IntType(8), len(fmt_str)), name=fmt_name)
fmt_gvar.global_constant = True
fmt_gvar.initializer = ir.Constant(
fmt_gvar.initializer = ir.Constant( # type: ignore
ir.ArrayType(ir.IntType(8), len(fmt_str)),
bytearray(fmt_str.encode("utf8"))
)
fmt_gvar.linkage = "internal"
fmt_gvar.align = 1
fmt_gvar.align = 1 # type: ignore
fmt_ptr = builder.bitcast(fmt_gvar, ir.PointerType())
@ -136,8 +168,9 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None,
"Warning: bpf_printk supports up to 3 arguments, extra arguments will be ignored.")
for expr in exprs[:3]:
print(f"{ast.dump(expr)}")
val, _ = eval_expr(func, module, builder,
expr, local_sym_tab, None)
expr, local_sym_tab, None, struct_sym_tab, local_var_metadata)
if val:
if isinstance(val.type, ir.PointerType):
val = builder.ptrtoint(val, ir.IntType(64))
@ -339,7 +372,7 @@ def bpf_map_delete_elem_emitter(call, map_ptr, module, builder, func, local_sym_
return result, None
def bpf_get_current_pid_tgid_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, local_var_metadata=None):
def bpf_get_current_pid_tgid_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
"""
Emit LLVM IR for bpf_get_current_pid_tgid helper function call.
"""
@ -375,7 +408,7 @@ def bpf_perf_event_output_handler(call, map_ptr, module, builder, func, local_sy
data_type = local_var_metadata[data_name]
if data_type in struct_sym_tab:
struct_info = struct_sym_tab[data_type]
size_val = ir.Constant(ir.IntType(64), struct_info["size"])
size_val = ir.Constant(ir.IntType(64), struct_info.size)
else:
raise ValueError(
f"Struct type {data_type} for variable {data_name} not found in struct symbol table.")
@ -420,11 +453,12 @@ helper_func_list = {
def handle_helper_call(call, module, builder, func, local_sym_tab=None, map_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
print(local_var_metadata)
if isinstance(call.func, ast.Name):
func_name = call.func.id
if func_name in helper_func_list:
# it is not a map method call
return helper_func_list[func_name](call, None, module, builder, func, local_sym_tab)
return helper_func_list[func_name](call, None, module, builder, func, local_sym_tab, struct_sym_tab, local_var_metadata)
else:
raise NotImplementedError(
f"Function {func_name} is not implemented as a helper function.")

View File

@ -2,14 +2,15 @@ import ast
from llvmlite import ir
from .license_pass import license_processing
from .functions_pass import func_proc
from .maps_pass import maps_proc
from .structs_pass import structs_proc
from pythonbpf.maps import maps_proc
from .structs.structs_pass import structs_proc
from .globals_pass import globals_processing
import os
import subprocess
import inspect
from pathlib import Path
from pylibbpf import BpfProgram
import tempfile
def find_bpf_chunks(tree):
@ -122,14 +123,17 @@ def compile():
def BPF() -> BpfProgram:
caller_frame = inspect.stack()[1]
caller_file = Path(caller_frame.filename).resolve()
ll_file = Path("/tmp") / caller_file.with_suffix(".ll").name
o_file = Path("/tmp") / caller_file.with_suffix(".o").name
compile_to_ir(str(caller_file), str(ll_file))
src = inspect.getsource(caller_frame.frame)
with tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".py") as f, \
tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".ll") as inter, \
tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".o") as obj_file:
f.write(src)
f.flush()
source = f.name
compile_to_ir(source, str(inter.name))
subprocess.run([
"llc", "-march=bpf", "-filetype=obj", "-O2",
str(inter.name), "-o", str(obj_file.name)
], check=True)
subprocess.run([
"llc", "-march=bpf", "-filetype=obj", "-O2",
str(ll_file), "-o", str(o_file)
], check=True)
return BpfProgram(str(o_file))
return BpfProgram(str(obj_file.name))

View File

@ -4,6 +4,7 @@ from llvmlite import ir
def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab=None, local_var_metadata=None):
print(f"Evaluating expression: {ast.dump(expr)}")
print(local_var_metadata)
if isinstance(expr, ast.Name):
if expr.id in local_sym_tab:
var = local_sym_tab[expr.id][0]
@ -66,6 +67,23 @@ def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_s
if method_name in helper_func_list:
return handle_helper_call(
expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
elif isinstance(expr, ast.Attribute):
if isinstance(expr.value, ast.Name):
var_name = expr.value.id
attr_name = expr.attr
if var_name in local_sym_tab:
var_ptr, var_type = local_sym_tab[var_name]
print(f"Loading attribute "
f"{attr_name} from variable {var_name}")
print(f"Variable type: {var_type}, Variable ptr: {var_ptr}")
print(local_var_metadata)
if local_var_metadata and var_name in local_var_metadata:
metadata = structs_sym_tab[local_var_metadata[var_name]]
if attr_name in metadata.fields:
gep = metadata.gep(builder, var_ptr, attr_name)
val = builder.load(gep)
field_type = metadata.field_type(attr_name)
return val, field_type
print("Unsupported expression evaluation")
return None
@ -73,6 +91,7 @@ def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_s
def handle_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata):
"""Handle expression statements in the function body."""
print(f"Handling expression: {ast.dump(expr)}")
print(local_var_metadata)
call = expr.value
if isinstance(call, ast.Call):
eval_expr(func, module, builder, call, local_sym_tab,

View File

@ -49,21 +49,17 @@ def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, struc
struct_type = local_var_metadata[var_name]
struct_info = structs_sym_tab[struct_type]
if field_name in struct_info["fields"]:
field_idx = struct_info["fields"][field_name]
struct_ptr = local_sym_tab[var_name][0]
field_ptr = builder.gep(
struct_ptr, [ir.Constant(ir.IntType(32), 0),
ir.Constant(ir.IntType(32), field_idx)],
inbounds=True)
if field_name in struct_info.fields:
field_ptr = struct_info.gep(
builder, local_sym_tab[var_name][0], field_name)
val = eval_expr(func, module, builder, rval,
local_sym_tab, map_sym_tab, structs_sym_tab)
if isinstance(struct_info["field_types"][field_idx], ir.ArrayType) and val[1] == ir.PointerType(ir.IntType(8)):
if isinstance(struct_info.field_type(field_name), ir.ArrayType) and val[1] == ir.PointerType(ir.IntType(8)):
# TODO: Figure it out, not a priority rn
# Special case for string assignment to char array
#str_len = struct_info["field_types"][field_idx].count
#assign_string_to_array(builder, field_ptr, val[0], str_len)
#print(f"Assigned to struct field {var_name}.{field_name}")
# str_len = struct_info["field_types"][field_idx].count
# assign_string_to_array(builder, field_ptr, val[0], str_len)
# print(f"Assigned to struct field {var_name}.{field_name}")
pass
if val is None:
print("Failed to evaluate struct field assignment")
@ -138,7 +134,7 @@ def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, struc
print(f"Dereferenced and assigned to {var_name}")
elif call_type in structs_sym_tab and len(rval.args) == 0:
struct_info = structs_sym_tab[call_type]
ir_type = struct_info["type"]
ir_type = struct_info.ir_type
# var = builder.alloca(ir_type, name=var_name)
# Null init
builder.store(ir.Constant(ir_type, None),
@ -282,6 +278,7 @@ def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_s
def process_stmt(func, module, builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab, did_return, ret_type=ir.IntType(64)):
print(f"Processing statement: {ast.dump(stmt)}")
if isinstance(stmt, ast.Expr):
print(local_var_metadata)
handle_expr(func, module, builder, stmt, local_sym_tab,
map_sym_tab, structs_sym_tab, local_var_metadata)
elif isinstance(stmt, ast.Assign):
@ -363,7 +360,7 @@ def allocate_mem(module, builder, body, func, ret_type, map_sym_tab, local_sym_t
f"Pre-allocated variable {var_name} for deref")
elif call_type in structs_sym_tab:
struct_info = structs_sym_tab[call_type]
ir_type = struct_info["type"]
ir_type = struct_info.ir_type
var = builder.alloca(ir_type, name=var_name)
local_var_metadata[var_name] = call_type
print(
@ -547,6 +544,8 @@ def infer_return_type(func_node: ast.FunctionDef):
return found_type or "None"
# For string assignment to fixed-size arrays
def assign_string_to_array(builder, target_array_ptr, source_string_ptr, array_length):
"""
Copy a string (i8*) to a fixed-size array ([N x i8]*)
@ -555,36 +554,39 @@ def assign_string_to_array(builder, target_array_ptr, source_string_ptr, array_l
entry_block = builder.block
copy_block = builder.append_basic_block("copy_char")
end_block = builder.append_basic_block("copy_end")
# Create loop counter
i = builder.alloca(ir.IntType(32))
builder.store(ir.Constant(ir.IntType(32), 0), i)
# Start the loop
builder.branch(copy_block)
# Copy loop
builder.position_at_end(copy_block)
idx = builder.load(i)
in_bounds = builder.icmp_unsigned('<', idx, ir.Constant(ir.IntType(32), array_length))
in_bounds = builder.icmp_unsigned(
'<', idx, ir.Constant(ir.IntType(32), array_length))
builder.cbranch(in_bounds, copy_block, end_block)
with builder.if_then(in_bounds):
# Load character from source
src_ptr = builder.gep(source_string_ptr, [idx])
char = builder.load(src_ptr)
# Store character in target
dst_ptr = builder.gep(target_array_ptr, [ir.Constant(ir.IntType(32), 0), idx])
dst_ptr = builder.gep(
target_array_ptr, [ir.Constant(ir.IntType(32), 0), idx])
builder.store(char, dst_ptr)
# Increment counter
next_idx = builder.add(idx, ir.Constant(ir.IntType(32), 1))
builder.store(next_idx, i)
builder.position_at_end(end_block)
# Ensure null termination
last_idx = ir.Constant(ir.IntType(32), array_length - 1)
null_ptr = builder.gep(target_array_ptr, [ir.Constant(ir.IntType(32), 0), last_idx])
null_ptr = builder.gep(
target_array_ptr, [ir.Constant(ir.IntType(32), 0), last_idx])
builder.store(ir.Constant(ir.IntType(8), 0), null_ptr)

View File

@ -0,0 +1,2 @@
from .maps import HashMap, PerfEventArray
from .maps_pass import maps_proc

View File

@ -1,37 +1,37 @@
import ast
from llvmlite import ir
from .type_deducer import ctypes_to_ir
from . import dwarf_constants as dc
from pythonbpf import dwarf_constants as dc
from enum import Enum
from .maps_utils import MapProcessorRegistry
import logging
map_sym_tab = {}
logger = logging.getLogger(__name__)
def maps_proc(tree, module, chunks):
""" Process all functions decorated with @map to find BPF maps """
map_sym_tab = {}
for func_node in chunks:
# Check if this function is a map
is_map = False
for decorator in func_node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "map":
is_map = True
break
if is_map:
if is_map(func_node):
print(f"Found BPF map: {func_node.name}")
process_bpf_map(func_node, module)
continue
map_sym_tab[func_node.name] = process_bpf_map(func_node, module)
return map_sym_tab
BPF_MAP_MAPPINGS = {
"HASH": 1, # BPF_MAP_TYPE_HASH
"PERF_EVENT_ARRAY": 4, # BPF_MAP_TYPE_PERF_EVENT_ARRAY
}
def is_map(func_node):
return any(
isinstance(decorator, ast.Name) and decorator.id == "map"
for decorator in func_node.decorator_list
)
class BPFMapType(Enum):
HASH = 1
PERF_EVENT_ARRAY = 4
def create_bpf_map(module, map_name, map_params):
"""Create a BPF map in the module with the given parameters and debug info"""
map_type_str = map_params.get("type", "HASH")
map_type = BPF_MAP_MAPPINGS.get(map_type_str)
"""Create a BPF map in the module with given parameters and debug info"""
# Create the anonymous struct type for BPF map
map_struct_type = ir.LiteralStructType(
@ -42,15 +42,14 @@ def create_bpf_map(module, map_name, map_params):
map_global.linkage = 'dso_local'
map_global.global_constant = False
map_global.initializer = ir.Constant(
map_struct_type, None) # type: ignore
map_struct_type, None)
map_global.section = ".maps"
map_global.align = 8 # type: ignore
map_global.align = 8
# Generate debug info for BTF
create_map_debug_info(module, map_global, map_name, map_params)
print(f"Created BPF map: {map_name}")
map_sym_tab[map_name] = map_global
logger.info(f"Created BPF map: {map_name} with params {map_params}")
return map_global
@ -75,7 +74,7 @@ def create_map_debug_info(module, map_global, map_name, map_params):
# Create array type for map type field (array of 1 unsigned int)
array_subrange = module.add_debug_info(
"DISubrange", {"count": BPF_MAP_MAPPINGS[map_params.get("type", "HASH")]})
"DISubrange", {"count": map_params.get("type", BPFMapType.HASH).value})
array_type = module.add_debug_info("DICompositeType", {
"tag": dc.DW_TAG_array_type,
"baseType": uint_type,
@ -183,9 +182,11 @@ def create_map_debug_info(module, map_global, map_name, map_params):
return global_var_expr
@MapProcessorRegistry.register("HashMap")
def process_hash_map(map_name, rval, module):
print(f"Creating HashMap map: {map_name}")
map_params: dict[str, object] = {"type": "HASH"}
"""Process a BPF_HASH map declaration"""
logger.info(f"Processing HashMap: {map_name}")
map_params = {"type": BPFMapType.HASH}
# Assuming order: key_type, value_type, max_entries
if len(rval.args) >= 1 and isinstance(rval.args[0], ast.Name):
@ -202,18 +203,21 @@ def process_hash_map(map_name, rval, module):
map_params["key"] = keyword.value.id
elif keyword.arg == "value" and isinstance(keyword.value, ast.Name):
map_params["value"] = keyword.value.id
elif keyword.arg == "max_entries" and isinstance(keyword.value, ast.Constant):
elif (keyword.arg == "max_entries" and
isinstance(keyword.value, ast.Constant)):
const_val = keyword.value.value
if isinstance(const_val, (int, str)):
map_params["max_entries"] = const_val
print(f"Map parameters: {map_params}")
logger.info(f"Map parameters: {map_params}")
return create_bpf_map(module, map_name, map_params)
@MapProcessorRegistry.register("PerfEventArray")
def process_perf_event_map(map_name, rval, module):
print(f"Creating PerfEventArray map: {map_name}")
map_params = {"type": "PERF_EVENT_ARRAY"}
"""Process a BPF_PERF_EVENT_ARRAY map declaration"""
logger.info(f"Processing PerfEventArray: {map_name}")
map_params = {"type": BPFMapType.PERF_EVENT_ARRAY}
if len(rval.args) >= 1 and isinstance(rval.args[0], ast.Name):
map_params["key_size"] = rval.args[0].id
@ -223,21 +227,18 @@ def process_perf_event_map(map_name, rval, module):
for keyword in rval.keywords:
if keyword.arg == "key_size" and isinstance(keyword.value, ast.Name):
map_params["key_size"] = keyword.value.id
elif keyword.arg == "value_size" and isinstance(keyword.value, ast.Name):
elif (keyword.arg == "value_size" and
isinstance(keyword.value, ast.Name)):
map_params["value_size"] = keyword.value.id
print(f"Map parameters: {map_params}")
logger.info(f"Map parameters: {map_params}")
return create_bpf_map(module, map_name, map_params)
def process_bpf_map(func_node, module):
"""Process a BPF map (a function decorated with @map)"""
map_name = func_node.name
print(f"Processing BPF map: {map_name}")
BPF_MAP_TYPES = {"HashMap": process_hash_map, # BPF_MAP_TYPE_HASH
"PerfEventArray": process_perf_event_map, # BPF_MAP_TYPE_PERF_EVENT_ARRAY
}
logger.info(f"Processing BPF map: {map_name}")
# For now, assume single return statement
return_stmt = None
@ -250,13 +251,13 @@ def process_bpf_map(func_node, module):
rval = return_stmt.value
# Handle only HashMap maps
if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name):
if rval.func.id in BPF_MAP_TYPES:
handler = BPF_MAP_TYPES[rval.func.id]
handler(map_name, rval, module)
handler = MapProcessorRegistry.get_processor(rval.func.id)
if handler:
return handler(map_name, rval, module)
else:
print(f"Unknown map type {rval.func.id}, defaulting to HashMap")
process_hash_map(map_name, rval, module)
logger.warning(f"Unknown map type "
f"{rval.func.id}, defaulting to HashMap")
return process_hash_map(map_name, rval, module)
else:
raise ValueError("Function under @map must return a map")

View File

@ -0,0 +1,16 @@
class MapProcessorRegistry:
"""Registry for map processor functions"""
_processors = {}
@classmethod
def register(cls, map_type_name):
"""Decorator to register a processor function for a map type"""
def decorator(func):
cls._processors[map_type_name] = func
return func
return decorator
@classmethod
def get_processor(cls, map_type_name):
"""Get the processor function for a map type"""
return cls._processors.get(map_type_name)

View File

@ -0,0 +1,31 @@
from llvmlite import ir
class StructType:
def __init__(self, ir_type, fields, size):
self.ir_type = ir_type
self.fields = fields
self.size = size
def field_idx(self, field_name):
return list(self.fields.keys()).index(field_name)
def field_type(self, field_name):
return self.fields[field_name]
def gep(self, builder, ptr, field_name):
idx = self.field_idx(field_name)
return builder.gep(ptr, [ir.Constant(ir.IntType(32), 0),
ir.Constant(ir.IntType(32), idx)],
inbounds=True)
def field_size(self, field_name):
fld = self.fields[field_name]
if isinstance(fld, ir.ArrayType):
return fld.count * (fld.element.width // 8)
elif isinstance(fld, ir.IntType):
return fld.width // 8
elif isinstance(fld, ir.PointerType):
return 8
raise TypeError(f"Unsupported field type: {fld}")

View File

@ -0,0 +1,97 @@
import ast
import logging
from llvmlite import ir
from pythonbpf.type_deducer import ctypes_to_ir
from .struct_type import StructType
logger = logging.getLogger(__name__)
# TODO: Shall we allow the following syntax:
# struct MyStruct:
# field1: int
# field2: str(32)
# Where int is mapped to c_uint64?
# Shall we just int64, int32 and uint32 similarly?
def structs_proc(tree, module, chunks):
""" Process all class definitions to find BPF structs """
structs_sym_tab = {}
for cls_node in chunks:
if is_bpf_struct(cls_node):
print(f"Found BPF struct: {cls_node.name}")
struct_info = process_bpf_struct(cls_node, module)
structs_sym_tab[cls_node.name] = struct_info
return structs_sym_tab
def is_bpf_struct(cls_node):
return any(
isinstance(decorator, ast.Name) and decorator.id == "struct"
for decorator in cls_node.decorator_list
)
def process_bpf_struct(cls_node, module):
""" Process a single BPF struct definition """
fields = parse_struct_fields(cls_node)
field_types = list(fields.values())
total_size = calc_struct_size(field_types)
struct_type = ir.LiteralStructType(field_types)
logger.info(f"Created struct {cls_node.name} with fields {fields.keys()}")
return StructType(struct_type, fields, total_size)
def parse_struct_fields(cls_node):
""" Parse fields of a struct class node """
fields = {}
for item in cls_node.body:
if isinstance(item, ast.AnnAssign) and \
isinstance(item.target, ast.Name):
fields[item.target.id] = get_type_from_ann(item.annotation)
else:
logger.error(f"Unsupported struct field: {ast.dump(item)}")
raise TypeError(f"Unsupported field in {ast.dump(cls_node)}")
return fields
def get_type_from_ann(annotation):
""" Convert an AST annotation node to an LLVM IR type for struct fields"""
if isinstance(annotation, ast.Call) and \
isinstance(annotation.func, ast.Name):
if annotation.func.id == "str":
# Char array
# Assumes constant integer argument
length = annotation.args[0].value
return ir.ArrayType(ir.IntType(8), length)
elif isinstance(annotation, ast.Name):
# Int type, written as c_int64, c_uint32, etc.
return ctypes_to_ir(annotation.id)
raise TypeError(f"Unsupported annotation type: {ast.dump(annotation)}")
def calc_struct_size(field_types):
""" Calculate total size of the struct with alignment and padding """
curr_offset = 0
for ftype in field_types:
if isinstance(ftype, ir.IntType):
fsize = ftype.width // 8
alignment = fsize
elif isinstance(ftype, ir.ArrayType):
fsize = ftype.count * (ftype.element.width // 8)
alignment = ftype.element.width // 8
elif isinstance(ftype, ir.PointerType):
# We won't encounter this rn, but for the future
fsize = 8
alignment = 8
else:
raise TypeError(f"Unsupported field type: {ftype}")
padding = (alignment - (curr_offset % alignment)) % alignment
curr_offset += padding + fsize
final_padding = (8 - (curr_offset % 8)) % 8
return curr_offset + final_padding

View File

@ -1,69 +0,0 @@
import ast
from llvmlite import ir
from .type_deducer import ctypes_to_ir
from . import dwarf_constants as dc
structs_sym_tab = {}
def structs_proc(tree, module, chunks):
for cls_node in chunks:
# Check if this class is a struct
is_struct = False
for decorator in cls_node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "struct":
is_struct = True
break
if is_struct:
print(f"Found BPF struct: {cls_node.name}")
process_bpf_struct(cls_node, module)
continue
return structs_sym_tab
def process_bpf_struct(cls_node, module):
struct_name = cls_node.name
field_names = []
field_types = []
for item in cls_node.body:
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
print(f"Field: {item.target.id}, Type: "
f"{ast.dump(item.annotation)}")
field_names.append(item.target.id)
if isinstance(item.annotation, ast.Call) and isinstance(item.annotation.func, ast.Name) and item.annotation.func.id == "str":
# This is a char array with fixed length
# TODO: For now assuming str is always called with constant
field_types.append(ir.ArrayType(
ir.IntType(8), item.annotation.args[0].value))
else:
field_types.append(ctypes_to_ir(item.annotation.id))
curr_offset = 0
for ftype in field_types:
if isinstance(ftype, ir.IntType):
fsize = ftype.width // 8
alignment = fsize
elif isinstance(ftype, ir.ArrayType):
fsize = ftype.count * (ftype.element.width // 8)
alignment = ftype.element.width // 8
elif isinstance(ftype, ir.PointerType):
fsize = 8
alignment = 8
else:
print(f"Unsupported field type in struct {struct_name}")
return
padding = (alignment - (curr_offset % alignment)) % alignment
curr_offset += padding
curr_offset += fsize
final_padding = (8 - (curr_offset % 8)) % 8
total_size = curr_offset + final_padding
struct_type = ir.LiteralStructType(field_types)
structs_sym_tab[struct_name] = {
"type": struct_type,
"fields": {name: idx for idx, name in enumerate(field_names)},
"size": total_size,
"field_types": field_types,
}
print(f"Created struct {struct_name} with fields {field_names}")