Skip to content

Commit 5c1071f

Browse files
authored
Merge pull request #4 from pythonbpf/perfbuf
Add userspace utils for Structs and maps, starting with PERF_EVENT_ARRAY
2 parents 5a3937b + f99de99 commit 5c1071f

22 files changed

+1236
-356
lines changed

.github/workflows/pip.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
run: pip install --verbose .[test]
4646

4747
- name: Test import
48-
run: python -c "import pylibbpf; print('Import successful')"
48+
run: python -I -c "import pylibbpf; print('Import successful')"
4949

5050
- name: Test
51-
run: python -m pytest -v
51+
run: python -I -m pytest -v

CMakeLists.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
cmake_minimum_required(VERSION 4.0)
22
project(pylibbpf)
33

4+
set(CMAKE_CXX_STANDARD 20)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
set(CMAKE_CXX_EXTENSIONS OFF)
7+
48
# pybind11
59
include_directories(${CMAKE_SOURCE_DIR}/src)
610
add_subdirectory(pybind11)
711
pybind11_add_module(
812
pylibbpf
13+
# Core
914
src/core/bpf_program.h
1015
src/core/bpf_exception.h
1116
src/core/bpf_map.h
12-
src/bindings/main.cpp
17+
src/core/bpf_object.h
1318
src/core/bpf_program.cpp
14-
src/core/bpf_map.cpp)
19+
src/core/bpf_map.cpp
20+
src/core/bpf_object.cpp
21+
# Maps
22+
src/maps/perf_event_array.h
23+
src/maps/perf_event_array.cpp
24+
# Utils
25+
src/utils/struct_parser.h
26+
src/utils/struct_parser.cpp
27+
# Bindings
28+
src/bindings/main.cpp)
1529

1630
# --- libbpf build rules ---
1731
set(LIBBPF_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libbpf/src)

examples/execve.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import time
22
from ctypes import c_int32, c_int64, c_uint64, c_void_p
33

4-
from pylibbpf import BpfMap
54
from pythonbpf import BPF, bpf, bpfglobal, map, section
65
from pythonbpf.maps import HashMap
76

7+
from pylibbpf import BpfMap
8+
89

910
@bpf
1011
@map

pylibbpf/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import logging
2+
3+
from .ir_to_ctypes import convert_structs_to_ctypes, is_pythonbpf_structs
4+
from .pylibbpf import (
5+
BpfException,
6+
BpfMap,
7+
BpfProgram,
8+
PerfEventArray,
9+
StructParser,
10+
)
11+
from .pylibbpf import (
12+
BpfObject as _BpfObject, # C++ object (internal)
13+
)
14+
from .wrappers import BpfObjectWrapper
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class BpfObject(BpfObjectWrapper):
20+
"""BpfObject with automatic struct conversion"""
21+
22+
def __init__(self, object_path: str, structs=None):
23+
"""Create a BPF object"""
24+
if structs is None:
25+
structs = {}
26+
elif is_pythonbpf_structs(structs):
27+
logger.info(f"Auto-converting {len(structs)} PythonBPF structs to ctypes")
28+
structs = convert_structs_to_ctypes(structs)
29+
30+
# Create C++ BpfObject with converted structs
31+
cpp_obj = _BpfObject(object_path, structs)
32+
33+
# Initialize wrapper
34+
super().__init__(cpp_obj)
35+
36+
37+
__all__ = [
38+
"BpfObject",
39+
"BpfProgram",
40+
"BpfMap",
41+
"PerfEventArray",
42+
"StructParser",
43+
"BpfException",
44+
]
45+
46+
__version__ = "0.0.6"

pylibbpf/ir_to_ctypes.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import ctypes
2+
import logging
3+
from typing import Dict, Type
4+
5+
from llvmlite import ir
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def ir_type_to_ctypes(ir_type):
11+
"""Convert LLVM IR type to ctypes type."""
12+
if isinstance(ir_type, ir.IntType):
13+
width = ir_type.width
14+
type_map = {
15+
8: ctypes.c_uint8,
16+
16: ctypes.c_uint16,
17+
32: ctypes.c_uint32,
18+
64: ctypes.c_uint64,
19+
}
20+
if width not in type_map:
21+
raise ValueError(f"Unsupported integer width: {width}")
22+
return type_map[width]
23+
24+
elif isinstance(ir_type, ir.ArrayType):
25+
count = ir_type.count
26+
element_type_ir = ir_type.element
27+
28+
if isinstance(element_type_ir, ir.IntType) and element_type_ir.width == 8:
29+
# Use c_char for string fields (will have .decode())
30+
return ctypes.c_char * count
31+
else:
32+
element_type = ir_type_to_ctypes(element_type_ir)
33+
return element_type * count
34+
elif isinstance(ir_type, ir.PointerType):
35+
return ctypes.c_void_p
36+
37+
else:
38+
raise TypeError(f"Unsupported IR type: {ir_type}")
39+
40+
41+
def _make_repr(struct_name: str, fields: list):
42+
"""Create a __repr__ function for a struct"""
43+
44+
def __repr__(self):
45+
field_strs = []
46+
for field_name, _ in fields:
47+
value = getattr(self, field_name)
48+
field_strs.append(f"{field_name}={value}")
49+
return f"<{struct_name} {' '.join(field_strs)}>"
50+
51+
return __repr__
52+
53+
54+
def convert_structs_to_ctypes(structs_sym_tab) -> Dict[str, Type[ctypes.Structure]]:
55+
"""Convert PythonBPF's structs_sym_tab to ctypes.Structure classes."""
56+
if not structs_sym_tab:
57+
return {}
58+
59+
ctypes_structs = {}
60+
61+
for struct_name, struct_type_obj in structs_sym_tab.items():
62+
try:
63+
fields = []
64+
for field_name, field_ir_type in struct_type_obj.fields.items():
65+
field_ctypes = ir_type_to_ctypes(field_ir_type)
66+
fields.append((field_name, field_ctypes))
67+
68+
repr_func = _make_repr(struct_name, fields)
69+
70+
struct_class = type(
71+
struct_name,
72+
(ctypes.Structure,),
73+
{
74+
"_fields_": fields,
75+
"__module__": "pylibbpf.ir_to_ctypes",
76+
"__doc__": f"Auto-generated ctypes structure for {struct_name}",
77+
"__repr__": repr_func,
78+
},
79+
)
80+
81+
ctypes_structs[struct_name] = struct_class
82+
# Pretty print field info
83+
field_info = ", ".join(f"{name}: {typ.__name__}" for name, typ in fields)
84+
logger.debug(f" {struct_name}({field_info})")
85+
except Exception as e:
86+
logger.error(f"Failed to convert struct '{struct_name}': {e}")
87+
raise
88+
logger.info(f"Converted struct '{struct_name}' to ctypes")
89+
return ctypes_structs
90+
91+
92+
def is_pythonbpf_structs(structs) -> bool:
93+
"""Check if structs dict is from PythonBPF."""
94+
if not isinstance(structs, dict) or not structs:
95+
return False
96+
97+
first_value = next(iter(structs.values()))
98+
return (
99+
hasattr(first_value, "ir_type")
100+
and hasattr(first_value, "fields")
101+
and hasattr(first_value, "size")
102+
)
103+
104+
105+
__all__ = ["convert_structs_to_ctypes", "is_pythonbpf_structs"]

pylibbpf/py.typed

Whitespace-only changes.

pylibbpf/wrappers.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from typing import Callable, Optional
2+
3+
4+
class PerfEventArrayHelper:
5+
"""Fluent wrapper for PERF_EVENT_ARRAY maps."""
6+
7+
def __init__(self, bpf_map):
8+
self._map = bpf_map
9+
self._perf_buffer = None
10+
11+
def open_perf_buffer(
12+
self,
13+
callback: Callable,
14+
struct_name: str = "",
15+
page_cnt: int = 8,
16+
lost_callback: Optional[Callable] = None,
17+
):
18+
"""Open perf buffer with auto-deserialization."""
19+
from .pylibbpf import PerfEventArray
20+
21+
if struct_name:
22+
self._perf_buffer = PerfEventArray(
23+
self._map,
24+
page_cnt,
25+
callback,
26+
struct_name,
27+
lost_callback or (lambda cpu, cnt: None),
28+
)
29+
else:
30+
self._perf_buffer = PerfEventArray(
31+
self._map, page_cnt, callback, lost_callback or (lambda cpu, cnt: None)
32+
)
33+
34+
return self
35+
36+
def poll(self, timeout_ms: int = -1) -> int:
37+
if not self._perf_buffer:
38+
raise RuntimeError("Call open_perf_buffer() first")
39+
return self._perf_buffer.poll(timeout_ms)
40+
41+
def consume(self) -> int:
42+
if not self._perf_buffer:
43+
raise RuntimeError("Call open_perf_buffer() first")
44+
return self._perf_buffer.consume()
45+
46+
def __getattr__(self, name):
47+
return getattr(self._map, name)
48+
49+
50+
class BpfObjectWrapper:
51+
"""Smart wrapper that returns map-specific helpers."""
52+
53+
BPF_MAP_TYPE_PERF_EVENT_ARRAY = 4
54+
BPF_MAP_TYPE_RINGBUF = 27
55+
56+
def __init__(self, bpf_object):
57+
self._obj = bpf_object
58+
self._map_helpers = {}
59+
60+
def __getitem__(self, name: str):
61+
"""Return appropriate helper based on map type."""
62+
if name in self._map_helpers:
63+
return self._map_helpers[name]
64+
65+
map_obj = self._obj[name]
66+
map_type = map_obj.get_type()
67+
68+
if map_type == self.BPF_MAP_TYPE_PERF_EVENT_ARRAY:
69+
helper = PerfEventArrayHelper(map_obj)
70+
else:
71+
helper = map_obj
72+
73+
self._map_helpers[name] = helper
74+
return helper
75+
76+
def __getattr__(self, name):
77+
return getattr(self._obj, name)

pyproject.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ requires = [
44
"wheel",
55
"ninja",
66
"cmake>=4.0",
7+
"pybind11>=2.10",
78
]
89
build-backend = "setuptools.build_meta"
910

1011
[project]
1112
name = "pylibbpf"
12-
version = "0.0.5"
13+
version = "0.0.6"
1314
description = "Python Bindings for Libbpf"
1415
authors = [
1516
{ name = "r41k0u", email = "[email protected]" },
@@ -32,14 +33,17 @@ classifiers = [
3233
"Topic :: Software Development :: Libraries :: Python Modules",
3334
"Topic :: System :: Operating System Kernels :: Linux",
3435
]
36+
dependencies = [
37+
"llvmlite>=0.40.0",
38+
]
3539

3640
[project.optional-dependencies]
3741
test = ["pytest>=6.0"]
3842

3943
[project.urls]
40-
Homepage = "https://github.com/varun-r-mallya/pylibbpf"
41-
Repository = "https://github.com/varun-r-mallya/pylibbpf"
42-
Issues = "https://github.com/varun-r-mallya/pylibbpf/issues"
44+
Homepage = "https://github.com/pythonbpf/pylibbpf"
45+
Repository = "https://github.com/pythonbpf/pylibbpf"
46+
Issues = "https://github.com/pythonbpf/pylibbpf/issues"
4347

4448
[tool.mypy]
4549
files = "setup.py"

setup.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
from pathlib import Path
55

6-
from setuptools import Extension, setup
6+
from setuptools import Extension, find_packages, setup
77
from setuptools.command.build_ext import build_ext
88

99
# Convert distutils Windows platform specifiers to CMake -A arguments
@@ -129,8 +129,11 @@ def build_extension(self, ext: CMakeExtension) -> None:
129129
description="Python Bindings for Libbpf",
130130
long_description=long_description,
131131
long_description_content_type="text/markdown",
132-
url="https://github.com/varun-r-mallya/pylibbpf",
133-
ext_modules=[CMakeExtension("pylibbpf")],
132+
url="https://github.com/pythonbpf/pylibbpf",
133+
packages=find_packages(where="."),
134+
package_dir={"": "."},
135+
py_modules=[], # Empty since we use packages
136+
ext_modules=[CMakeExtension("pylibbpf.pylibbpf")],
134137
cmdclass={"build_ext": CMakeBuild},
135138
zip_safe=False,
136139
classifiers=[
@@ -147,6 +150,16 @@ def build_extension(self, ext: CMakeExtension) -> None:
147150
"Topic :: Software Development :: Libraries :: Python Modules",
148151
"Topic :: System :: Operating System Kernels :: Linux",
149152
],
153+
install_requires=[
154+
"llvmlite>=0.40.0", # Required for struct conversion
155+
],
150156
extras_require={"test": ["pytest>=6.0"]},
151157
python_requires=">=3.8",
158+
package_data={
159+
"pylibbpf": [
160+
"*.py",
161+
"py.typed", # For type hints
162+
],
163+
},
164+
include_package_data=True,
152165
)

0 commit comments

Comments
 (0)