1 Commits

Author SHA1 Message Date
97436ee92a add libxdp submodule
Signed-off-by: varun-r-mallya <varunrmallya@gmail.com>
2025-09-28 10:57:19 +05:30
27 changed files with 448 additions and 1406 deletions

View File

@ -15,8 +15,8 @@ jobs:
name: Format name: Format
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- uses: pre-commit/action@v3.0.1 - uses: pre-commit/action@v3.0.1

View File

@ -1,38 +1,29 @@
name: Pip name: Pip
on: on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch: workflow_dispatch:
pull_request:
push:
branches:
- master
- main
jobs: jobs:
test: build:
name: Test on Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ['3.12', '3.13'] platform: [ubuntu-latest]
python-version: ["3.8", "3.12"]
runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v6
with: with:
submodules: recursive submodules: true
- name: Verify submodules - uses: actions/setup-python@v5
run: |
echo "Checking submodule status:"
git submodule status
echo "Checking libbpf directory:"
ls -la libbpf/
echo "Checking libbpf/src:"
ls -la libbpf/src/ || echo "libbpf/src not found!"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -42,72 +33,19 @@ jobs:
sudo apt-get install -y \ sudo apt-get install -y \
libbpf-dev \ libbpf-dev \
libelf-dev \ libelf-dev \
zlib1g-dev \ linux-headers-generic \
build-essential \ build-essential \
clang \
cmake \ cmake \
ninja-build \ ninja-build
pkg-config \
git \
make
- name: Install Python build dependencies - name: Add requirements
run: | run: python -m pip install --upgrade pip wheel setuptools
python -m pip install --upgrade pip
pip install --upgrade "setuptools>=77.0.0" wheel
pip install cmake ninja pybind11
- name: Check build requirements - name: Build and install
run: | run: pip install --verbose .[test]
echo "Python version:"
python --version
echo "CMake version:"
cmake --version
echo "Ninja version:"
ninja --version
echo "Setuptools version:"
python -c "import setuptools; print(setuptools.__version__)"
- name: Build and install pylibbpf (verbose) - name: Test import
run: | run: python -c "import pylibbpf; print('Import successful')"
pip install -v -e . 2>&1 | tee build.log
continue-on-error: false
- name: Check build output - name: Test
run: | run: python -m pytest -v
echo "Build directory contents:"
find build -type f -name "*.so" 2>/dev/null || echo "No .so files found in build/"
echo ""
echo "Looking for pylibbpf extension:"
find . -name "pylibbpf*.so" -o -name "pylibbpf*.pyd"
echo ""
echo "Site-packages contents:"
python -c "import site; print(site.getsitepackages())"
ls -la $(python -c "import site; print(site.getsitepackages()[0])")/pylibbpf/ || echo "pylibbpf not in site-packages"
- name: Try importing extension directly
run: |
python -c "
import sys
print('Python path:', sys.path)
try:
from pylibbpf import pylibbpf
print('Successfully imported pylibbpf.pylibbpf')
print('pylibbpf.pylibbpf members:', dir(pylibbpf))
except ImportError as e:
print(f'Failed to import pylibbpf.pylibbpf: {e}')
"
- name: Verify extension loaded
run: |
python -c "import pylibbpf; print('Members:', dir(pylibbpf)); assert hasattr(pylibbpf, 'BpfObject'), 'BpfObject not found!'; print('✓ OK')"
- name: Install test dependencies
if: success()
run: |
pip install pytest pytest-cov
- name: Run tests
if: success()
run: |
python -I -m pytest -v -s --cov=pylibbpf --cov-report=term-missing

View File

@ -15,9 +15,9 @@ jobs:
name: Build SDist name: Build SDist
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: true
- name: Build SDist - name: Build SDist
run: pipx run build --sdist run: pipx run build --sdist
@ -25,7 +25,7 @@ jobs:
- name: Check metadata - name: Check metadata
run: pipx run twine check dist/* run: pipx run twine check dist/*
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v4
with: with:
name: cibw-sdist name: cibw-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
@ -39,33 +39,31 @@ jobs:
arch: [x86_64] arch: [x86_64]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: true
- name: Build wheels - name: Build wheels
uses: pypa/cibuildwheel@v3.3 uses: pypa/cibuildwheel@v2.17
env: env:
CIBW_PLATFORM: linux CIBW_PLATFORM: linux
CIBW_ARCHS_LINUX: ${{ matrix.arch }} CIBW_ARCHS_LINUX: ${{ matrix.arch }}
CIBW_BUILD: "cp312-* cp313-*" CIBW_BUILD: "cp38-* cp311-*"
CIBW_SKIP: "*-musllinux*" CIBW_SKIP: "*-musllinux*"
CIBW_TEST_COMMAND: "python -c 'import pylibbpf; assert hasattr(pylibbpf, \"BpfObject\"), \"BpfObject not found\"; print(f\"pylibbpf {pylibbpf.__version__} OK\")'" CIBW_TEST_COMMAND: "python -c 'import pylibbpf; print(f\"pylibbpf {pylibbpf.__version__} imported successfully\")'"
CIBW_TEST_SKIP: "*-linux_aarch64" CIBW_TEST_SKIP: "*-linux_aarch64"
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
CIBW_BEFORE_ALL_LINUX: | CIBW_BEFORE_ALL_LINUX: |
dnf install -y elfutils-libelf-devel zlib-devel make gcc gcc-c++ git dnf install -y elfutils-libelf-devel zlib-devel
CIBW_BEFORE_BUILD: >
pip install --upgrade "setuptools>=77.0.0" wheel cmake ninja pybind11
- name: Verify clean directory - name: Verify clean directory
run: git diff --exit-code run: git diff --exit-code
shell: bash shell: bash
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: cibw-wheels-linux-${{ matrix.arch }} name: cibw-wheels-linux-${{ matrix.arch }}
path: wheelhouse/*.whl path: wheelhouse/*.whl
@ -82,7 +80,7 @@ jobs:
steps: steps:
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v4
with: with:
pattern: cibw-* pattern: cibw-*
path: dist path: dist

3
.gitmodules vendored
View File

@ -5,3 +5,6 @@
[submodule "libbpf"] [submodule "libbpf"]
path = libbpf path = libbpf
url = https://github.com/libbpf/libbpf.git url = https://github.com/libbpf/libbpf.git
[submodule "xdp-tools"]
path = xdp-tools
url = https://github.com/xdp-project/xdp-tools/

View File

@ -1,36 +1,17 @@
cmake_minimum_required(VERSION 4.0) cmake_minimum_required(VERSION 4.0)
project(pylibbpf) project(pylibbpf)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
# pybind11 # pybind11
include_directories(${CMAKE_SOURCE_DIR}/src) include_directories(${CMAKE_SOURCE_DIR}/src)
add_subdirectory(pybind11) add_subdirectory(pybind11)
pybind11_add_module( pybind11_add_module(
pylibbpf pylibbpf
# Core
src/core/bpf_program.h src/core/bpf_program.h
src/core/bpf_exception.h src/core/bpf_exception.h
src/core/bpf_map.h src/core/bpf_map.h
src/core/bpf_object.h src/bindings/main.cpp
src/core/bpf_program.cpp src/core/bpf_program.cpp
src/core/bpf_map.cpp src/core/bpf_map.cpp)
src/core/bpf_object.cpp
# Maps
src/maps/perf_event_array.h
src/maps/perf_event_array.cpp
# Utils
src/utils/struct_parser.h
src/utils/struct_parser.cpp
# Bindings
src/bindings/main.cpp)
# --- libbpf build rules --- # --- libbpf build rules ---
set(LIBBPF_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libbpf/src) set(LIBBPF_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libbpf/src)

View File

@ -1,43 +1,24 @@
<picture> # Py-libbpf
<source
media="(prefers-color-scheme: light)"
srcset="https://github.com/user-attachments/assets/dbd56f5b-4512-4c82-a404-30bce0ee5207"
width="450"
alt="pylibbpf light mode">
<img
src="https://github.com/user-attachments/assets/9e873d60-a834-4411-bc0c-361b14502f8b"
width="450"
alt="pylibbpf dark mode">
</picture>
<p align="center"> <p align="center">
<!-- PyPI --> <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://www.python.org/downloads/release/python-3120/"><img src="https://img.shields.io/badge/python-3.12-blue.svg"></a>
<a href="https://pypi.org/project/pylibbpf"><img src="https://badge.fury.io/py/pylibbpf.svg"></a> <a href="https://pypi.org/project/pylibbpf"><img src="https://badge.fury.io/py/pylibbpf.svg"></a>
<!-- <a href="https://pypi.org/project/pythonbpf/"><img src="https://img.shields.io/pypi/status/pythonbpf" alt="PyPI Status"></a> -->
<a href="https://pepy.tech/project/pylibbpf"><img src="https://pepy.tech/badge/pylibbpf" alt="Downloads"></a>
<!-- Build & CI -->
<a href="https://github.com/pythonbpf/pylibbpf/actions"><img src="https://github.com/pythonbpf/pylibbpf/actions/workflows/wheels.yml/badge.svg" alt="Build Status"></a>
<!-- Meta -->
<a href="https://github.com/pythonbpf/pylibbpf/blob/master/LICENSE"><img src="https://img.shields.io/github/license/pythonbpf/pylibbpf" alt="License"></a>
</p> </p>
This library provides Python bindings for libbpf on Linux to make loading of eBPF object files easier. This is meant to
be used along with `pythonbpf`, the eBPF Python DSL compiler. This library makes it possible to attach these programs to
events in the kernel right from inside Python.
This library provides Python bindings for **libbpf** on Linux, making it easier to load eBPF object files. It is designed to be used together with [PythonBPF](https://github.com/pythonbpf/python-bpf), the eBPF compiler for Python. With these bindings, you can attach eBPF programs to kernel events directly from Python. # IN DEVELOPMENT. DO NOT USE.
All programs written with this are to be run with a `sudo` Python interpreter.
> **Note**: This project is under active development and not ready for production use. ## Prerequisites
## Dependencies
* A compiler with C++11 support * A compiler with C++11 support
* Pip 10+ or CMake >= 4.1 * Pip 10+ or CMake >= 4.1
* Ninja or Pip 10+ * Ninja or Pip 10+
## Installation ## Installation
`pip install pylibbpf`
## Development Just clone this repository and pip install. Note the `--recursive` option which is
Clone this repository and pip install. Note the `--recursive` option which is
needed for the pybind11 submodule: needed for the pybind11 submodule:
```bash ```bash
@ -49,5 +30,11 @@ pip install .
With the `setup.py` file included in this example, the `pip install` command will With the `setup.py` file included in this example, the `pip install` command will
invoke CMake and build the pybind11 module as specified in `CMakeLists.txt`. invoke CMake and build the pybind11 module as specified in `CMakeLists.txt`.
## Development
Do this before running to make sure Python can manipulate bpf programs without sudo
```bash
sudo setcap cap_bpf,cap_sys_admin+ep /usr/bin/python3.12
```
## Building the documentation ## Building the documentation
The documentation here is still boilerplate. The documentation here is still boilerplate.

View File

@ -1,11 +1,10 @@
import time import time
from ctypes import c_int32, c_int64, c_uint64, c_void_p from ctypes import c_int32, c_int64, c_uint64, c_void_p
from pylibbpf import BpfMap
from pythonbpf import BPF, bpf, bpfglobal, map, section from pythonbpf import BPF, bpf, bpfglobal, map, section
from pythonbpf.maps import HashMap from pythonbpf.maps import HashMap
from pylibbpf import BpfMap
@bpf @bpf
@map @map

View File

@ -1,46 +0,0 @@
import logging
from .ir_to_ctypes import convert_structs_to_ctypes, is_pythonbpf_structs
from .pylibbpf import (
BpfException,
BpfMap,
BpfProgram,
PerfEventArray,
StructParser,
)
from .pylibbpf import (
BpfObject as _BpfObject, # C++ object (internal)
)
from .wrappers import BpfObjectWrapper
logger = logging.getLogger(__name__)
class BpfObject(BpfObjectWrapper):
"""BpfObject with automatic struct conversion"""
def __init__(self, object_path: str, structs=None):
"""Create a BPF object"""
if structs is None:
structs = {}
elif is_pythonbpf_structs(structs):
logger.info(f"Auto-converting {len(structs)} PythonBPF structs to ctypes")
structs = convert_structs_to_ctypes(structs)
# Create C++ BpfObject with converted structs
cpp_obj = _BpfObject(object_path, structs)
# Initialize wrapper
super().__init__(cpp_obj)
__all__ = [
"BpfObject",
"BpfProgram",
"BpfMap",
"PerfEventArray",
"StructParser",
"BpfException",
]
__version__ = "0.0.7"

View File

@ -1,104 +0,0 @@
import ctypes
import logging
from llvmlite import ir
logger = logging.getLogger(__name__)
def ir_type_to_ctypes(ir_type):
"""Convert LLVM IR type to ctypes type."""
if isinstance(ir_type, ir.IntType):
width = ir_type.width
type_map = {
8: ctypes.c_uint8,
16: ctypes.c_uint16,
32: ctypes.c_uint32,
64: ctypes.c_uint64,
}
if width not in type_map:
raise ValueError(f"Unsupported integer width: {width}")
return type_map[width]
elif isinstance(ir_type, ir.ArrayType):
count = ir_type.count
element_type_ir = ir_type.element
if isinstance(element_type_ir, ir.IntType) and element_type_ir.width == 8:
# Use c_char for string fields (will have .decode())
return ctypes.c_char * count
else:
element_type = ir_type_to_ctypes(element_type_ir)
return element_type * count
elif isinstance(ir_type, ir.PointerType):
return ctypes.c_void_p
else:
raise TypeError(f"Unsupported IR type: {ir_type}")
def _make_repr(struct_name: str, fields: list):
"""Create a __repr__ function for a struct"""
def __repr__(self):
field_strs = []
for field_name, _ in fields:
value = getattr(self, field_name)
field_strs.append(f"{field_name}={value}")
return f"<{struct_name} {' '.join(field_strs)}>"
return __repr__
def convert_structs_to_ctypes(structs_sym_tab) -> dict[str, type[ctypes.Structure]]:
"""Convert PythonBPF's structs_sym_tab to ctypes.Structure classes."""
if not structs_sym_tab:
return {}
ctypes_structs = {}
for struct_name, struct_type_obj in structs_sym_tab.items():
try:
fields = []
for field_name, field_ir_type in struct_type_obj.fields.items():
field_ctypes = ir_type_to_ctypes(field_ir_type)
fields.append((field_name, field_ctypes))
repr_func = _make_repr(struct_name, fields)
struct_class = type(
struct_name,
(ctypes.Structure,),
{
"_fields_": fields,
"__module__": "pylibbpf.ir_to_ctypes",
"__doc__": f"Auto-generated ctypes structure for {struct_name}",
"__repr__": repr_func,
},
)
ctypes_structs[struct_name] = struct_class
# Pretty print field info
field_info = ", ".join(f"{name}: {typ.__name__}" for name, typ in fields)
logger.debug(f" {struct_name}({field_info})")
except Exception as e:
logger.error(f"Failed to convert struct '{struct_name}': {e}")
raise
logger.info(f"Converted struct '{struct_name}' to ctypes")
return ctypes_structs
def is_pythonbpf_structs(structs) -> bool:
"""Check if structs dict is from PythonBPF."""
if not isinstance(structs, dict) or not structs:
return False
first_value = next(iter(structs.values()))
return (
hasattr(first_value, "ir_type")
and hasattr(first_value, "fields")
and hasattr(first_value, "size")
)
__all__ = ["convert_structs_to_ctypes", "is_pythonbpf_structs"]

View File

View File

@ -1,77 +0,0 @@
from collections.abc import Callable
class PerfEventArrayHelper:
"""Fluent wrapper for PERF_EVENT_ARRAY maps."""
def __init__(self, bpf_map):
self._map = bpf_map
self._perf_buffer = None
def open_perf_buffer(
self,
callback: Callable,
struct_name: str = "",
page_cnt: int = 8,
lost_callback: Callable | None = None,
):
"""Open perf buffer with auto-deserialization."""
from .pylibbpf import PerfEventArray
if struct_name:
self._perf_buffer = PerfEventArray(
self._map,
page_cnt,
callback,
struct_name,
lost_callback or (lambda cpu, cnt: None),
)
else:
self._perf_buffer = PerfEventArray(
self._map, page_cnt, callback, lost_callback or (lambda cpu, cnt: None)
)
return self
def poll(self, timeout_ms: int = -1) -> int:
if not self._perf_buffer:
raise RuntimeError("Call open_perf_buffer() first")
return self._perf_buffer.poll(timeout_ms)
def consume(self) -> int:
if not self._perf_buffer:
raise RuntimeError("Call open_perf_buffer() first")
return self._perf_buffer.consume()
def __getattr__(self, name):
return getattr(self._map, name)
class BpfObjectWrapper:
"""Smart wrapper that returns map-specific helpers."""
BPF_MAP_TYPE_PERF_EVENT_ARRAY = 4
BPF_MAP_TYPE_RINGBUF = 27
def __init__(self, bpf_object):
self._obj = bpf_object
self._map_helpers = {}
def __getitem__(self, name: str):
"""Return appropriate helper based on map type."""
if name in self._map_helpers:
return self._map_helpers[name]
map_obj = self._obj[name]
map_type = map_obj.get_type()
if map_type == self.BPF_MAP_TYPE_PERF_EVENT_ARRAY:
helper = PerfEventArrayHelper(map_obj)
else:
helper = map_obj
self._map_helpers[name] = helper
return helper
def __getattr__(self, name):
return getattr(self._obj, name)

View File

@ -1,24 +1,23 @@
[build-system] [build-system]
requires = [ requires = [
"setuptools>=77", "setuptools>=42",
"wheel", "wheel",
"ninja", "ninja",
"cmake>=4.0", "cmake>=4.0",
"pybind11>=2.10",
] ]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "pylibbpf" name = "pylibbpf"
version = "0.0.7" version = "0.0.5"
description = "Python Bindings for Libbpf" description = "Python Bindings for Libbpf"
authors = [ authors = [
{ name = "r41k0u", email = "pragyanshchaturvedi18@gmail.com" }, { name = "r41k0u", email = "pragyanshchaturvedi18@gmail.com" },
{ name = "varun-r-mallya", email = "varunrmallya@gmail.com" } { name = "varun-r-mallya", email = "varunrmallya@gmail.com" }
] ]
readme = "README.md" readme = "README.md"
license = "Apache-2.0" license = { file = "LICENSE" }
requires-python = ">=3.12" requires-python = ">=3.8"
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@ -29,26 +28,22 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: C++", "Programming Language :: C++",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Operating System Kernels :: Linux", "Topic :: System :: Operating System Kernels :: Linux",
] ]
dependencies = [
"llvmlite>=0.40.0",
]
[project.optional-dependencies] [project.optional-dependencies]
test = ["pytest>=6.0"] test = ["pytest>=6.0"]
[project.urls] [project.urls]
Homepage = "https://github.com/pythonbpf/pylibbpf" Homepage = "https://github.com/varun-r-mallya/pylibbpf"
Repository = "https://github.com/pythonbpf/pylibbpf" Repository = "https://github.com/varun-r-mallya/pylibbpf"
Issues = "https://github.com/pythonbpf/pylibbpf/issues" Issues = "https://github.com/varun-r-mallya/pylibbpf/issues"
[tool.mypy] [tool.mypy]
files = "setup.py" files = "setup.py"
python_version = "3.12" python_version = "3.8"
strict = true strict = true
show_error_codes = true show_error_codes = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
@ -69,7 +64,7 @@ filterwarnings = [
testpaths = ["tests"] testpaths = ["tests"]
[tool.ruff] [tool.ruff]
target-version = "py312" target-version = "py38"
line-length = 88 line-length = 88
[tool.ruff.lint] [tool.ruff.lint]
@ -80,7 +75,3 @@ extend-select = [
"RUF", # Ruff-specific "RUF", # Ruff-specific
"UP", # pyupgrade "UP", # pyupgrade
] ]
[tool.setuptools]
packages = ["pylibbpf"]
package-data = {"pylibbpf" = ["*.py", "*.so", "*.pyd", "py.typed"]}

View File

@ -6,6 +6,14 @@ from pathlib import Path
from setuptools import Extension, setup from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
# Convert distutils Windows platform specifiers to CMake -A arguments
PLAT_TO_CMAKE = {
"win32": "Win32",
"win-amd64": "x64",
"win-arm32": "ARM",
"win-arm64": "ARM64",
}
# A CMakeExtension needs a sourcedir instead of a file list. # A CMakeExtension needs a sourcedir instead of a file list.
# The name must be the _single_ output extension from the CMake build. # The name must be the _single_ output extension from the CMake build.
@ -37,8 +45,8 @@ class CMakeBuild(build_ext):
# from Python. # from Python.
cmake_args = [ cmake_args = [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}",
f"-DPYTHON_EXECUTABLE={sys.executable}",
f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm
f"-DPython_EXECUTABLE={sys.executable}",
] ]
build_args = [] build_args = []
@ -114,6 +122,31 @@ if readme_path.exists():
long_description = readme_path.read_text(encoding="utf-8") long_description = readme_path.read_text(encoding="utf-8")
setup( setup(
ext_modules=[CMakeExtension("pylibbpf.pylibbpf")], name="pylibbpf",
version="0.0.1",
author="varun-r-mallya, r41k0u",
author_email="varunrmallyagmail.com",
description="Python Bindings for Libbpf",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/varun-r-mallya/pylibbpf",
ext_modules=[CMakeExtension("pylibbpf")],
cmdclass={"build_ext": CMakeBuild}, cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: C++",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Operating System Kernels :: Linux",
],
extras_require={"test": ["pytest>=6.0"]},
python_requires=">=3.8",
) )

View File

@ -2,12 +2,13 @@
#define STRINGIFY(x) #x #define STRINGIFY(x) #x
#define MACRO_STRINGIFY(x) STRINGIFY(x) #define MACRO_STRINGIFY(x) STRINGIFY(x)
extern "C" {
#include <libbpf.h>
}
#include "core/bpf_program.h"
#include "core/bpf_exception.h" #include "core/bpf_exception.h"
#include "core/bpf_map.h" #include "core/bpf_map.h"
#include "core/bpf_object.h"
#include "core/bpf_program.h"
#include "maps/perf_event_array.h"
#include "utils/struct_parser.h"
namespace py = pybind11; namespace py = pybind11;
@ -29,70 +30,34 @@ PYBIND11_MODULE(pylibbpf, m) {
// Register the custom exception // Register the custom exception
py::register_exception<BpfException>(m, "BpfException"); py::register_exception<BpfException>(m, "BpfException");
// BpfObject py::class_<BpfProgram>(m, "BpfProgram")
py::class_<BpfObject, std::shared_ptr<BpfObject>>(m, "BpfObject") .def(py::init<const std::string &>())
.def(py::init<std::string, py::dict>(), py::arg("object_path"), .def(py::init<const std::string &, const std::string &>())
py::arg("structs") = py::dict()) .def("load", &BpfProgram::load)
.def("load", &BpfObject::load)
.def("is_loaded", &BpfObject::is_loaded)
.def("get_program_names", &BpfObject::get_program_names)
.def("get_program", &BpfObject::get_program, py::arg("name"))
.def("attach_all", &BpfObject::attach_all)
.def("get_map_names", &BpfObject::get_map_names)
.def("get_map", &BpfObject::get_map, py::arg("name"))
.def("get_struct_defs", &BpfObject::get_struct_defs)
.def("__getitem__", &BpfObject::get_map, py::arg("name"));
// BpfProgram
py::class_<BpfProgram, std::shared_ptr<BpfProgram>>(m, "BpfProgram")
.def("attach", &BpfProgram::attach) .def("attach", &BpfProgram::attach)
.def("detach", &BpfProgram::detach) .def("destroy", &BpfProgram::destroy)
.def("is_attached", &BpfProgram::is_attached) .def("load_and_attach", &BpfProgram::load_and_attach)
.def("get_name", &BpfProgram::get_name); .def("is_loaded", &BpfProgram::is_loaded)
.def("is_attached", &BpfProgram::is_attached);
// BpfMap py::class_<BpfMap>(m, "BpfMap")
py::class_<BpfMap, std::shared_ptr<BpfMap>>(m, "BpfMap") .def(py::init<BpfProgram *, py::object &>())
.def("lookup", &BpfMap::lookup, py::arg("key")) .def("lookup", &BpfMap::lookup)
.def("update", &BpfMap::update, py::arg("key"), py::arg("value")) .def("update", &BpfMap::update)
.def("delete_elem", &BpfMap::delete_elem, py::arg("key")) .def("delete", &BpfMap::delete_elem)
.def("get_next_key", &BpfMap::get_next_key, py::arg("key") = py::none()) .def("get_next_key", &BpfMap::get_next_key, py::arg("key") = py::none())
.def("items", &BpfMap::items) .def("items", &BpfMap::items)
.def("keys", &BpfMap::keys) .def("keys", &BpfMap::keys)
.def("values", &BpfMap::values) .def("values", &BpfMap::values)
.def("get_name", &BpfMap::get_name) .def("get_name", &BpfMap::get_name)
.def("get_fd", &BpfMap::get_fd)
.def("get_type", &BpfMap::get_type) .def("get_type", &BpfMap::get_type)
.def("get_key_size", &BpfMap::get_key_size) .def("get_key_size", &BpfMap::get_key_size)
.def("get_value_size", &BpfMap::get_value_size) .def("get_value_size", &BpfMap::get_value_size)
.def("get_max_entries", &BpfMap::get_max_entries) .def("get_max_entries", &BpfMap::get_max_entries)
.def("set_value_struct", &BpfMap::set_value_struct, .def("__getitem__", &BpfMap::lookup)
py::arg("struct_name")) .def("__setitem__", &BpfMap::update)
.def("get_value_struct_name", &BpfMap::get_value_struct_name) .def("__delitem__", &BpfMap::delete_elem);
.def("has_struct_value", &BpfMap::has_struct_value)
.def("__getitem__", &BpfMap::lookup, py::arg("key"))
.def("__setitem__", &BpfMap::update, py::arg("key"), py::arg("value"))
.def("__delitem__", &BpfMap::delete_elem, py::arg("key"));
// StructParser
py::class_<StructParser>(m, "StructParser")
.def(py::init<py::dict>(), py::arg("structs"))
.def("parse", &StructParser::parse, py::arg("struct_name"),
py::arg("data"))
.def("has_struct", &StructParser::has_struct, py::arg("struct_name"));
// PerfEventArray
py::class_<PerfEventArray, std::shared_ptr<PerfEventArray>>(m,
"PerfEventArray")
.def(py::init<std::shared_ptr<BpfMap>, int, py::function, py::object>(),
py::arg("map"), py::arg("page_cnt"), py::arg("callback"),
py::arg("lost_callback") = py::none())
.def(py::init<std::shared_ptr<BpfMap>, int, py::function, std::string,
py::object>(),
py::arg("map"), py::arg("page_cnt"), py::arg("callback"),
py::arg("struct_name"), py::arg("lost_callback") = py::none())
.def("poll", &PerfEventArray::poll, py::arg("timeout_ms"))
.def("consume", &PerfEventArray::consume)
.def("get_map", &PerfEventArray::get_map);
#ifdef VERSION_INFO #ifdef VERSION_INFO
m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO);

View File

@ -7,9 +7,12 @@
class BpfException final : public std::runtime_error { class BpfException final : public std::runtime_error {
public: public:
explicit BpfException(const std::string &message) explicit BpfException(const std::string &message)
: std::runtime_error(message) {} : std::runtime_error(message) {
}
explicit BpfException(const char *message) : std::runtime_error(message) {} explicit BpfException(const char *message)
: std::runtime_error(message) {
}
}; };
#endif // PYLIBBPF_BPF_EXCEPTION_H #endif // PYLIBBPF_BPF_EXCEPTION_H

View File

@ -1,246 +1,43 @@
#include "core/bpf_map.h" #include "bpf_map.h"
#include "core/bpf_exception.h"
#include "core/bpf_object.h"
#include "utils/struct_parser.h"
#include <algorithm>
#include <cerrno>
#include <cstring>
BpfMap::BpfMap(std::shared_ptr<BpfObject> parent, struct bpf_map *raw_map, #include "bpf_exception.h"
const std::string &map_name)
: parent_obj_(parent), map_(raw_map), map_fd_(-1), map_name_(map_name),
key_size_(0), value_size_(0) {
if (!parent)
throw BpfException("Parent BpfObject is null");
if (!(parent->is_loaded()))
throw BpfException("Parent BpfObject is not loaded");
if (!raw_map)
throw BpfException("bpf_map pointer is null");
map_fd_ = bpf_map__fd(map_); BpfMap::BpfMap(BpfProgram *program_, const py::object &map_from_python) {
if (map_fd_ < 0) if (py::isinstance<py::function>(map_from_python)) {
throw BpfException("Failed to get file descriptor for map '" + map_name_ + const auto name = map_from_python.attr("__name__").cast<std::string>();
"'"); bpf_program = program_;
map_ = bpf_object__find_map_by_name(bpf_program->get_obj(), name.c_str());
key_size_ = bpf_map__key_size(map_); if (!map_) {
value_size_ = bpf_map__value_size(map_); throw BpfException("Failed to find map by name");
} }
map_fd = bpf_map__fd(map_);
void BpfMap::set_value_struct(const std::string &struct_name) { if (map_fd == -1) {
auto parent = parent_obj_.lock(); throw BpfException("Failed to open map File Descriptor");
if (!parent) {
throw BpfException("Parent BpfObject no longer exists");
} }
struct_parser_ = parent->get_struct_parser();
if (!struct_parser_) {
throw BpfException("No struct definitions available in BpfObject");
}
if (!struct_parser_->has_struct(struct_name)) {
throw BpfException("Unknown struct: " + struct_name);
}
value_struct_name_ = struct_name;
}
py::object BpfMap::lookup(const py::object &key) const {
if (map_fd_ < 0)
throw BpfException("Map '" + map_name_ + "' is not initialized properly");
BufferManager<> key_buf, value_buf;
auto key_span = key_buf.get_span(key_size_);
auto value_span = value_buf.get_span(value_size_);
// Convert Python → bytes
python_to_bytes_inplace(key, key_span);
// The flags field here matters only when spin locks are used.
// Skipping it for now.
const int ret = bpf_map__lookup_elem(map_, key_span.data(), key_size_,
value_span.data(), value_size_, BPF_ANY);
if (ret < 0) {
if (ret == -ENOENT)
return py::none();
throw BpfException("Failed to lookup key in map '" + map_name_ +
"': " + std::strerror(-ret));
}
return bytes_to_python(value_span);
}
void BpfMap::update(const py::object &key, const py::object &value) const {
if (map_fd_ < 0)
throw BpfException("Map '" + map_name_ + "' is not initialized properly");
BufferManager<> key_buf, value_buf;
auto key_span = key_buf.get_span(key_size_);
auto value_span = value_buf.get_span(value_size_);
python_to_bytes_inplace(key, key_span);
python_to_bytes_inplace(value, value_span);
const int ret = bpf_map__update_elem(map_, key_span.data(), key_size_,
value_span.data(), value_size_, BPF_ANY);
if (ret < 0) {
throw BpfException("Failed to update key in map '" + map_name_ +
"': " + std::strerror(-ret));
}
}
void BpfMap::delete_elem(const py::object &key) const {
if (map_fd_ < 0)
throw BpfException("Map '" + map_name_ + "' is not initialized properly");
BufferManager<> key_buf;
auto key_span = key_buf.get_span(key_size_);
// Convert Python → bytes
python_to_bytes_inplace(key, key_span);
const int ret =
bpf_map__delete_elem(map_, key_span.data(), key_size_, BPF_ANY);
if (ret != 0) {
if (ret == -ENOENT)
throw py::key_error("Key not found in map '" + map_name_ + "'");
throw BpfException("Failed to delete key from map '" + map_name_ +
"': " + std::strerror(-ret));
}
}
py::object BpfMap::get_next_key(const py::object &key) const {
BufferManager<> next_key_buf;
auto next_key = next_key_buf.get_span(key_size_);
int ret;
if (key.is_none()) {
ret = bpf_map__get_next_key(map_, nullptr, next_key.data(), key_size_);
} else { } else {
BufferManager<> key_buf; throw BpfException("Invalid map object passed to function.");
auto key_bytes = key_buf.get_span(key_size_);
python_to_bytes_inplace(key, key_bytes);
ret = bpf_map__get_next_key(map_, key_bytes.data(), next_key.data(),
key_size_);
}
if (ret < 0) {
if (ret == -ENOENT) {
// No more keys
return py::none();
}
throw BpfException("Failed to get next key in map '" + map_name_ +
"': " + std::strerror(-ret));
}
return bytes_to_python(next_key);
}
py::dict BpfMap::items() const {
py::dict result;
py::object current_key = get_next_key(py::none());
if (current_key.is_none()) {
return result;
}
while (!current_key.is_none()) {
try {
py::object value = lookup(current_key);
result[current_key] = value;
current_key = get_next_key(current_key);
} catch (const py::key_error &) {
break;
} }
} }
return result; std::vector<uint8_t> BpfMap::python_to_bytes(const py::object &obj, size_t size) {
} std::vector<uint8_t> result(size, 0);
py::list BpfMap::keys() const {
py::list result;
py::object current_key = get_next_key(py::none());
if (current_key.is_none()) {
return result;
}
while (!current_key.is_none()) {
result.append(current_key);
current_key = get_next_key(current_key);
}
return result;
}
py::list BpfMap::values() const {
py::list result;
py::object current_key = get_next_key(py::none());
if (current_key.is_none()) {
return result;
}
while (!current_key.is_none()) {
try {
py::object value = lookup(current_key);
result.append(value);
current_key = get_next_key(current_key);
} catch (const py::key_error &) {
break;
}
}
return result;
}
int BpfMap::get_type() const { return bpf_map__type(map_); }
int BpfMap::get_max_entries() const { return bpf_map__max_entries(map_); }
// Helper functions
void BpfMap::python_to_bytes_inplace(const py::object &obj,
std::span<uint8_t> buffer) {
std::fill(buffer.begin(), buffer.end(), 0);
if (py::isinstance<py::int_>(obj)) { if (py::isinstance<py::int_>(obj)) {
if (buffer.size() <= sizeof(uint64_t)) { const auto value = obj.cast<uint64_t>();
uint64_t value = obj.cast<uint64_t>(); std::memcpy(result.data(), &value, std::min(size, sizeof(uint64_t)));
std::memcpy(buffer.data(), &value, buffer.size());
} else {
throw BpfException("Integer key/value size exceeds maximum (8 bytes)");
}
} else if (py::isinstance<py::bytes>(obj)) { } else if (py::isinstance<py::bytes>(obj)) {
std::string bytes_str = obj.cast<std::string>(); const auto bytes_str = obj.cast<std::string>();
std::memcpy(result.data(), bytes_str.data(), std::min(size, bytes_str.size()));
if (bytes_str.size() > buffer.size()) {
throw BpfException("Bytes size " + std::to_string(bytes_str.size()) +
" exceeds expected size " +
std::to_string(buffer.size()));
}
std::memcpy(buffer.data(), bytes_str.data(), bytes_str.size());
} else if (py::isinstance<py::str>(obj)) { } else if (py::isinstance<py::str>(obj)) {
std::string str_val = obj.cast<std::string>(); const auto str_val = obj.cast<std::string>();
std::memcpy(result.data(), str_val.data(), std::min(size, str_val.size()));
if (str_val.size() >= buffer.size()) {
throw BpfException("String size exceeds expected size");
} }
std::memcpy(buffer.data(), str_val.data(), str_val.size()); return result;
buffer[str_val.size()] = '\0';
} else {
throw BpfException("Unsupported type for BPF map key/value");
}
}
py::object BpfMap::bytes_to_python(std::span<const uint8_t> data) const {
// NOTE: Struct parsing for value type
if (struct_parser_ && !value_struct_name_.empty()) {
py::bytes py_data(reinterpret_cast<const char *>(data.data()), data.size());
return struct_parser_->parse(value_struct_name_, py_data);
} }
py::object BpfMap::bytes_to_python(const std::vector<uint8_t> &data) {
// Try to interpret as integer if it's a common integer size
if (data.size() == 4) { if (data.size() == 4) {
uint32_t value; uint32_t value;
std::memcpy(&value, data.data(), 4); std::memcpy(&value, data.data(), 4);
@ -250,6 +47,165 @@ py::object BpfMap::bytes_to_python(std::span<const uint8_t> data) const {
std::memcpy(&value, data.data(), 8); std::memcpy(&value, data.data(), 8);
return py::cast(value); return py::cast(value);
} else { } else {
// Return as bytes
return py::bytes(reinterpret_cast<const char *>(data.data()), data.size()); return py::bytes(reinterpret_cast<const char *>(data.data()), data.size());
} }
} }
void BpfMap::update(const py::object &key, const py::object &value) const {
const size_t key_size = bpf_map__key_size(map_);
const size_t value_size = bpf_map__value_size(map_);
const auto key_bytes = python_to_bytes(key, key_size);
const auto value_bytes = python_to_bytes(value, value_size);
const int ret = bpf_map__update_elem(
map_,
key_bytes.data(),
key_size,
value_bytes.data(),
value_size,
BPF_ANY);
if (ret != 0) {
throw BpfException("Failed to update map element");
}
}
void BpfMap::delete_elem(const py::object &key) const {
const size_t key_size = bpf_map__key_size(map_);
std::vector<uint8_t> key_bytes;
key_bytes = python_to_bytes(key, key_size);
if (const int ret = bpf_map__delete_elem(map_, key_bytes.data(), key_size, BPF_ANY); ret != 0) {
throw BpfException("Failed to delete map element");
}
}
py::list BpfMap::get_next_key(const py::object &key) const {
const size_t key_size = bpf_map__key_size(map_);
std::vector<uint8_t> next_key(key_size);
int ret;
if (key.is_none()) {
ret = bpf_map__get_next_key(map_, nullptr, next_key.data(), key_size);
} else {
const auto key_bytes = python_to_bytes(key, key_size);
ret = bpf_map__get_next_key(map_, key_bytes.data(), next_key.data(), key_size);
}
py::list result;
if (ret == 0) {
result.append(bytes_to_python(next_key));
}
return result;
}
py::list BpfMap::keys() const {
py::list result;
const size_t key_size = bpf_map__key_size(map_);
std::vector<uint8_t> key(key_size);
std::vector<uint8_t> next_key(key_size);
int ret = bpf_map__get_next_key(map_, nullptr, key.data(), key_size);
while (ret == 0) {
result.append(bytes_to_python(key));
ret = bpf_map__get_next_key(map_, key.data(), next_key.data(), key_size);
key = next_key;
}
return result;
}
py::list BpfMap::values() const {
py::list result;
const size_t key_size = bpf_map__key_size(map_);
const size_t value_size = bpf_map__value_size(map_);
std::vector<uint8_t> key(key_size);
std::vector<uint8_t> next_key(key_size);
std::vector<uint8_t> value(value_size);
int ret = bpf_map__get_next_key(map_, nullptr, key.data(), key_size);
while (ret == 0) {
if (bpf_map__lookup_elem(map_, key.data(), key_size, value.data(), value_size, BPF_ANY) == 0) {
result.append(bytes_to_python(value));
}
ret = bpf_map__get_next_key(map_, key.data(), next_key.data(), key_size);
key = next_key;
}
return result;
}
std::string BpfMap::get_name() const {
const char *name = bpf_map__name(map_);
return name ? std::string(name) : "";
}
int BpfMap::get_type() const {
return bpf_map__type(map_);
}
int BpfMap::get_key_size() const {
return bpf_map__key_size(map_);
}
int BpfMap::get_value_size() const {
return bpf_map__value_size(map_);
}
int BpfMap::get_max_entries() const {
return bpf_map__max_entries(map_);
}
py::dict BpfMap::items() const {
py::dict result;
const size_t key_size = bpf_map__key_size(map_);
const size_t value_size = bpf_map__value_size(map_);
std::vector<uint8_t> key(key_size);
std::vector<uint8_t> next_key(key_size);
std::vector<uint8_t> value(value_size);
// Get first key
int ret = bpf_map__get_next_key(map_, nullptr, key.data(), key_size);
while (ret == 0) {
// Lookup value for current key
if (bpf_map__lookup_elem(map_, key.data(), key_size, value.data(), value_size, BPF_ANY) == 0) {
result[bytes_to_python(key)] = bytes_to_python(value);
}
// Get next key
ret = bpf_map__get_next_key(map_, key.data(), next_key.data(), key_size);
key = next_key;
}
return result;
}
py::object BpfMap::lookup(const py::object &key) const {
const __u32 key_size = bpf_map__key_size(map_);
const __u32 value_size = bpf_map__value_size(map_);
const auto key_bytes = python_to_bytes(key, key_size);
std::vector<uint8_t> value_bytes(value_size);
// The flags field here matters only when spin locks are used which is close to fucking never, so fuck no,
// im not adding it
const int ret = bpf_map__lookup_elem(
map_,
key_bytes.data(),
key_size,
value_bytes.data(),
value_size,
BPF_ANY);
if (ret != 0) {
return py::none();
}
return bytes_to_python(value_bytes);
}

View File

@ -1,85 +1,55 @@
#ifndef PYLIBBPF_BPF_MAP_H #ifndef PYLIBBPF_MAPS_H
#define PYLIBBPF_BPF_MAP_H #define PYLIBBPF_MAPS_H
#include <array>
#include <libbpf.h> #include <libbpf.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <span>
#include <string>
#include <vector> #include <vector>
#include <string>
class BpfObject; #include "bpf_program.h"
class StructParser;
namespace py = pybind11; namespace py = pybind11;
class BpfMap : public std::enable_shared_from_this<BpfMap> { class BpfMap {
private: private:
std::weak_ptr<BpfObject> parent_obj_;
struct bpf_map *map_; struct bpf_map *map_;
int map_fd_; int map_fd = -1;
std::string map_name_; //TODO: turn below into a shared pointer and ref count it so that there is no resource leakage
__u32 key_size_, value_size_; BpfProgram *bpf_program;
// TODO: For now, we'll only support struct parsing for value types
// later we can extend this to keys
std::shared_ptr<StructParser> struct_parser_;
std::string value_struct_name_;
template <size_t StackSize = 64> struct BufferManager {
std::array<uint8_t, StackSize> stack_buf;
std::vector<uint8_t> heap_buf;
std::span<uint8_t> get_span(size_t size) {
if (size <= StackSize) {
return std::span<uint8_t>(stack_buf.data(), size);
} else {
heap_buf.resize(size);
return std::span<uint8_t>(heap_buf);
}
}
};
public: public:
BpfMap(std::shared_ptr<BpfObject> parent, struct bpf_map *raw_map, BpfMap(BpfProgram *program_, const py::object &map_from_python);
const std::string &map_name);
~BpfMap() = default; ~BpfMap() = default;
BpfMap(const BpfMap &) = delete;
BpfMap &operator=(const BpfMap &) = delete;
BpfMap(BpfMap &&) noexcept = default;
BpfMap &operator=(BpfMap &&) noexcept = default;
[[nodiscard]] py::object lookup(const py::object &key) const; [[nodiscard]] py::object lookup(const py::object &key) const;
void update(const py::object &key, const py::object &value) const;
void delete_elem(const py::object &key) const;
py::object get_next_key(const py::object &key = py::none()) const;
py::dict items() const;
py::list keys() const;
py::list values() const;
void set_value_struct(const std::string &struct_name);
[[nodiscard]] std::string get_name() const { return map_name_; } void update(const py::object &key, const py::object &value) const;
[[nodiscard]] int get_fd() const { return map_fd_; }
[[nodiscard]] int get_type() const; void delete_elem(const py::object &key) const;
[[nodiscard]] int get_key_size() const { return key_size_; };
[[nodiscard]] int get_value_size() const { return value_size_; }; py::list get_next_key(const py::object &key = py::none()) const;
[[nodiscard]] int get_max_entries() const;
[[nodiscard]] std::shared_ptr<BpfObject> get_parent() const { py::dict items() const;
return parent_obj_.lock();
} py::list keys() const;
[[nodiscard]] std::string get_value_struct_name() const {
return value_struct_name_; py::list values() const;
}
[[nodiscard]] bool has_struct_value() const { [[nodiscard]] std::string get_name() const;
return !value_struct_name_.empty();
} int get_type() const;
int get_key_size() const;
int get_value_size() const;
int get_max_entries() const;
private: private:
static void python_to_bytes_inplace(const py::object &obj, static std::vector<uint8_t> python_to_bytes(const py::object &obj, size_t size);
std::span<uint8_t> buffer);
py::object bytes_to_python(std::span<const uint8_t> data) const; static py::object bytes_to_python(const std::vector<uint8_t> &data);
}; };
#endif // PYLIBBPF_BPF_MAP_H #endif //PYLIBBPF_MAPS_H

View File

@ -1,272 +0,0 @@
#include "core/bpf_object.h"
#include "core/bpf_exception.h"
#include "core/bpf_map.h"
#include "core/bpf_program.h"
#include "utils/struct_parser.h"
#include <cerrno>
#include <cstring>
#include <utility>
BpfObject::BpfObject(std::string object_path, py::dict structs)
: obj_(nullptr), object_path_(std::move(object_path)), loaded_(false),
struct_defs_(structs), struct_parser_(nullptr) {}
BpfObject::~BpfObject() {
// Clear caches first (order matters!)
prog_cache_.clear(); // Detaches programs
maps_cache_.clear(); // Closes maps
// Then close object
if (obj_) {
bpf_object__close(obj_);
obj_ = nullptr;
}
}
BpfObject::BpfObject(BpfObject &&other) noexcept
: obj_(std::exchange(other.obj_, nullptr)),
object_path_(std::move(other.object_path_)),
loaded_(std::exchange(other.loaded_, false)),
maps_cache_(std::move(other.maps_cache_)),
prog_cache_(std::move(other.prog_cache_)),
struct_defs_(std::move(other.struct_defs_)),
struct_parser_(std::move(other.struct_parser_)) {
other.obj_ = nullptr;
other.loaded_ = false;
}
BpfObject &BpfObject::operator=(BpfObject &&other) noexcept {
if (this != &other) {
prog_cache_.clear();
maps_cache_.clear();
if (obj_) {
bpf_object__close(obj_);
}
obj_ = std::exchange(other.obj_, nullptr);
object_path_ = std::move(other.object_path_);
loaded_ = std::exchange(other.loaded_, false);
maps_cache_ = std::move(other.maps_cache_);
prog_cache_ = std::move(other.prog_cache_);
struct_defs_ = std::move(other.struct_defs_);
struct_parser_ = std::move(other.struct_parser_);
}
return *this;
}
void BpfObject::load() {
if (loaded_) {
throw BpfException("BPF object already loaded");
}
std::string error_msg = "Failed to open BPF object";
obj_ = bpf_object__open_file(object_path_.c_str(), nullptr);
if (!obj_) {
error_msg += " file '" + object_path_ + "': " + std::strerror(errno);
throw BpfException(error_msg);
}
if (bpf_object__load(obj_)) {
error_msg +=
" object from file '" + object_path_ + "': " + std::strerror(errno);
bpf_object__close(obj_);
obj_ = nullptr;
throw BpfException(error_msg);
}
loaded_ = true;
}
// ==================== Program Methods ====================
py::list BpfObject::get_program_names() {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
py::list names;
struct bpf_program *prog = nullptr;
bpf_object__for_each_program(prog, obj_) {
_get_or_create_program(prog); // Ensure cached
names.append(bpf_program__name(prog));
}
return names;
}
std::shared_ptr<BpfProgram>
BpfObject::_get_or_create_program(struct bpf_program *prog) {
if (!prog) {
throw BpfException("bpf_program pointer is null");
}
const char *name = bpf_program__name(prog);
std::string prog_name(name ? name : "");
// Check cache
auto it = prog_cache_.find(prog_name);
if (it != prog_cache_.end()) {
return it->second;
}
// Create and cache
auto bpf_prog =
std::make_shared<BpfProgram>(shared_from_this(), prog, prog_name);
prog_cache_[prog_name] = bpf_prog;
return bpf_prog;
}
std::shared_ptr<BpfProgram> BpfObject::get_program(const std::string &name) {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
// Check cache
auto it = prog_cache_.find(name);
if (it != prog_cache_.end()) {
return it->second;
}
// Create and cache
struct bpf_program *raw_prog = find_program_by_name(name);
auto prog = std::make_shared<BpfProgram>(shared_from_this(), raw_prog, name);
prog_cache_[name] = prog;
return prog;
}
struct bpf_program *
BpfObject::find_program_by_name(const std::string &name) const {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
struct bpf_program *prog =
bpf_object__find_program_by_name(obj_, name.c_str());
if (!prog) {
throw BpfException("Program '" + name + "' not found");
}
return prog;
}
py::dict BpfObject::get_cached_programs() const {
py::dict programs;
for (const auto &entry : prog_cache_) {
programs[entry.first.c_str()] = entry.second;
}
return programs;
}
py::dict BpfObject::attach_all() {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
py::dict attached_programs;
struct bpf_program *prog = nullptr;
bpf_object__for_each_program(prog, obj_) {
auto bpf_prog = _get_or_create_program(prog);
if (!bpf_prog->is_attached()) {
bpf_prog->attach();
}
const char *name = bpf_program__name(prog);
attached_programs[name] = bpf_prog;
}
return attached_programs;
}
// ==================== Map Methods ====================
py::list BpfObject::get_map_names() {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
py::list names;
struct bpf_map *map = nullptr;
bpf_object__for_each_map(map, obj_) {
_get_or_create_map(map); // Ensure cached
names.append(bpf_map__name(map));
}
return names;
}
std::shared_ptr<BpfMap> BpfObject::get_map(const std::string &name) {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
// Check cache
auto it = maps_cache_.find(name);
if (it != maps_cache_.end()) {
return it->second;
}
// Create and cache
struct bpf_map *raw_map = find_map_by_name(name);
auto map = std::make_shared<BpfMap>(shared_from_this(), raw_map, name);
maps_cache_[name] = map;
return map;
}
std::shared_ptr<BpfMap> BpfObject::_get_or_create_map(struct bpf_map *map) {
if (!map) {
throw BpfException("bpf_map pointer is null");
}
const char *name = bpf_map__name(map);
std::string map_name(name ? name : "");
// Check cache
auto it = maps_cache_.find(map_name);
if (it != maps_cache_.end()) {
return it->second;
}
// Create and cache
auto bpf_map = std::make_shared<BpfMap>(shared_from_this(), map, map_name);
maps_cache_[map_name] = bpf_map;
return bpf_map;
}
struct bpf_map *BpfObject::find_map_by_name(const std::string &name) const {
if (!loaded_) {
throw BpfException("BPF object not loaded");
}
struct bpf_map *map = bpf_object__find_map_by_name(obj_, name.c_str());
if (!map) {
throw BpfException("Map '" + name + "' not found");
}
return map;
}
py::dict BpfObject::get_cached_maps() const {
py::dict maps;
for (const auto &entry : maps_cache_) {
maps[entry.first.c_str()] = entry.second;
}
return maps;
}
std::shared_ptr<StructParser> BpfObject::get_struct_parser() const {
if (!struct_parser_ && !struct_defs_.empty()) {
// Create parser on first access
struct_parser_ = std::make_shared<StructParser>(struct_defs_);
}
return struct_parser_;
}

View File

@ -1,88 +0,0 @@
#ifndef PYLIBBPF_BPF_OBJECT_H
#define PYLIBBPF_BPF_OBJECT_H
#include <libbpf.h>
#include <memory>
#include <pybind11/pybind11.h>
#include <string>
#include <unordered_map>
namespace py = pybind11;
class BpfProgram;
class BpfMap;
class StructParser;
/**
* BpfObject - Represents a loaded BPF object file.
*
* This is the main entry point for loading BPF programs.
* Owns the bpf_object* and manages all programs and maps within it.
*/
class BpfObject : public std::enable_shared_from_this<BpfObject> {
private:
struct bpf_object *obj_;
std::string object_path_;
bool loaded_;
mutable std::unordered_map<std::string, std::shared_ptr<BpfMap>> maps_cache_;
mutable std::unordered_map<std::string, std::shared_ptr<BpfProgram>>
prog_cache_;
py::dict struct_defs_;
mutable std::shared_ptr<StructParser> struct_parser_;
std::shared_ptr<BpfProgram> _get_or_create_program(struct bpf_program *prog);
std::shared_ptr<BpfMap> _get_or_create_map(struct bpf_map *map);
public:
explicit BpfObject(std::string object_path, py::dict structs = py::dict());
~BpfObject();
// Disable copy, allow move
BpfObject(const BpfObject &) = delete;
BpfObject &operator=(const BpfObject &) = delete;
BpfObject(BpfObject &&) noexcept;
BpfObject &operator=(BpfObject &&) noexcept;
/**
* Load the BPF object into the kernel.
* Must be called before accessing programs or maps.
*/
void load();
/**
* Check if object is loaded.
*/
[[nodiscard]] bool is_loaded() const { return loaded_; }
/**
* Get the underlying bpf_object pointer.
* Only for internal use by BpfProgram and BpfMap.
*/
[[nodiscard]] struct bpf_object *get_obj() const { return obj_; }
/**
* Attach all programs in the object.
*/
py::dict attach_all();
// Program access
[[nodiscard]] py::list get_program_names();
[[nodiscard]] std::shared_ptr<BpfProgram>
get_program(const std::string &name);
[[nodiscard]] struct bpf_program *
find_program_by_name(const std::string &name) const;
[[nodiscard]] py::dict get_cached_programs() const;
// Map access
[[nodiscard]] py::list get_map_names();
[[nodiscard]] std::shared_ptr<BpfMap> get_map(const std::string &name);
[[nodiscard]] struct bpf_map *find_map_by_name(const std::string &name) const;
[[nodiscard]] py::dict get_cached_maps() const;
// Struct parsing
[[nodiscard]] py::dict get_struct_defs() const { return struct_defs_; }
[[nodiscard]] std::shared_ptr<StructParser> get_struct_parser() const;
};
#endif // PYLIBBPF_BPF_OBJECT_H

View File

@ -1,74 +1,84 @@
#include "core/bpf_program.h" #include "bpf_program.h"
#include "core/bpf_exception.h" #include "bpf_exception.h"
#include "core/bpf_object.h" #include <filesystem>
#include <cerrno>
#include <cstring>
#include <utility> #include <utility>
BpfProgram::BpfProgram(std::shared_ptr<BpfObject> parent, BpfProgram::BpfProgram(std::string object_path, std::string program_name)
struct bpf_program *raw_prog, : obj_(nullptr), prog_(nullptr), link_(nullptr),
const std::string &program_name) object_path_(std::move(object_path)), program_name_(std::move(program_name)) {
: parent_obj_(parent), prog_(raw_prog), link_(nullptr),
program_name_(program_name) {
if (!parent)
throw BpfException("Parent BpfObject is null");
if (!(parent->is_loaded()))
throw BpfException("Parent BpfObject is not loaded");
if (!raw_prog)
throw BpfException("bpf_program pointer is null");
} }
BpfProgram::~BpfProgram() { detach(); } BpfProgram::~BpfProgram() {
destroy();
BpfProgram::BpfProgram(BpfProgram &&other) noexcept if (obj_) {
: parent_obj_(std::move(other.parent_obj_)), prog_(other.prog_), bpf_object__close(obj_);
link_(other.link_), program_name_(std::move(other.program_name_)) { }
other.prog_ = nullptr;
other.link_ = nullptr;
} }
BpfProgram &BpfProgram::operator=(BpfProgram &&other) noexcept { struct bpf_object * BpfProgram::get_obj() const {
if (this != &other) { return obj_;
detach();
parent_obj_ = std::move(other.parent_obj_);
prog_ = other.prog_;
link_ = other.link_;
program_name_ = std::move(other.program_name_);
other.prog_ = nullptr;
other.link_ = nullptr;
}
return *this;
} }
void BpfProgram::attach() { bool BpfProgram::load() {
// Check if parent is still alive // Open the eBPF object file
auto parent = parent_obj_.lock(); obj_ = bpf_object__open_file(object_path_.c_str(), nullptr);
if (!parent) { if (libbpf_get_error(obj_)) {
throw BpfException("Parent BpfObject has been destroyed"); throw BpfException("Failed to open BPF object file: " + object_path_);
}
if (link_) {
throw BpfException("Program '" + program_name_ + "' already attached");
} }
// Find the program by name (if specified)
if (!program_name_.empty()) {
prog_ = bpf_object__find_program_by_name(obj_, program_name_.c_str());
if (!prog_) { if (!prog_) {
throw BpfException("Program '" + program_name_ + "' not initialized"); throw BpfException("Program '" + program_name_ + "' not found in object");
}
} else {
while ((prog_ = bpf_object__next_program(obj_, prog_)) != nullptr) {
programs.emplace_back(prog_, nullptr);
} }
link_ = bpf_program__attach(prog_); // throw if no programs found
if (!link_) { if (programs.empty()) {
std::string err_msg = "bpf_program__attach failed for program '" + throw BpfException("No programs found in object file");
program_name_ + "': " + std::strerror(errno);
throw BpfException(err_msg);
} }
} }
void BpfProgram::detach() { // Load the eBPF object into the kernel
if (link_) { if (bpf_object__load(obj_)) {
bpf_link__destroy(link_); throw BpfException("Failed to load BPF object into kernel");
link_ = nullptr; }
return true;
}
bool BpfProgram::attach() {
for (auto [prog, link]: programs) {
if (!prog) {
throw BpfException("Program not loaded");
}
link = bpf_program__attach(prog);
if (libbpf_get_error(link)) {
link = nullptr;
throw BpfException("Failed to attach BPF program");
} }
} }
return true;
}
bool BpfProgram::destroy() {
bool success = true;
for (auto [prog, link]: programs) {
if (!prog) {
throw BpfException("Program not loaded");
}
success = success & bpf_link__destroy(link);
}
return success;
}
void BpfProgram::load_and_attach() {
load();
attach();
}

View File

@ -2,35 +2,37 @@
#define PYLIBBPF_BPF_PROGRAM_H #define PYLIBBPF_BPF_PROGRAM_H
#include <libbpf.h> #include <libbpf.h>
#include <memory> #include <pybind11/stl.h>
#include <string> #include <string>
class BpfObject; namespace py = pybind11;
class BpfProgram { class BpfProgram {
private: private:
std::weak_ptr<BpfObject> parent_obj_; struct bpf_object *obj_;
struct bpf_program *prog_; struct bpf_program *prog_;
struct bpf_link *link_; struct bpf_link *link_;
std::string object_path_;
std::string program_name_; std::string program_name_;
std::vector<std::pair<bpf_program *, bpf_link *> > programs;
public: public:
explicit BpfProgram(std::shared_ptr<BpfObject> parent, explicit BpfProgram(std::string object_path, std::string program_name = "");
struct bpf_program *raw_prog,
const std::string &program_name);
~BpfProgram(); ~BpfProgram();
BpfProgram(const BpfProgram &) = delete; struct bpf_object *get_obj() const;
BpfProgram &operator=(const BpfProgram &) = delete;
BpfProgram(BpfProgram &&) noexcept;
BpfProgram &operator=(BpfProgram &&) noexcept;
void attach(); bool load();
void detach();
bool attach();
bool destroy();
void load_and_attach();
[[nodiscard]] bool is_loaded() const { return obj_ != nullptr; }
[[nodiscard]] bool is_attached() const { return link_ != nullptr; } [[nodiscard]] bool is_attached() const { return link_ != nullptr; }
[[nodiscard]] std::string get_name() const { return program_name_; }
}; };
#endif //PYLIBBPF_BPF_PROGRAM_H #endif //PYLIBBPF_BPF_PROGRAM_H

View File

@ -1,117 +0,0 @@
#include "maps/perf_event_array.h"
#include "core/bpf_exception.h"
#include "core/bpf_map.h"
#include "core/bpf_object.h"
#include "utils/struct_parser.h"
#include <cerrno>
#include <cstring>
PerfEventArray::PerfEventArray(std::shared_ptr<BpfMap> map, int page_cnt,
py::function callback, py::object lost_callback)
: map_(map), pb_(nullptr), callback_(std::move(callback)),
lost_callback_(std::move(lost_callback)) {
if (map->get_type() != BPF_MAP_TYPE_PERF_EVENT_ARRAY) {
throw BpfException("Map '" + map->get_name() +
"' is not a PERF_EVENT_ARRAY");
}
if (page_cnt <= 0 || (page_cnt & (page_cnt - 1)) != 0) {
throw BpfException("page_cnt must be a positive power of 2");
}
struct perf_buffer_opts pb_opts = {};
pb_opts.sz = sizeof(pb_opts); // Required for forward compatibility
pb_ = perf_buffer__new(
map->get_fd(), page_cnt,
sample_callback_wrapper, // sample_cb
lost_callback.is_none() ? nullptr : lost_callback_wrapper, // lost_cb
this, // ctx
&pb_opts // opts
);
if (!pb_) {
throw BpfException("Failed to create perf buffer: " +
std::string(std::strerror(errno)));
}
}
PerfEventArray::PerfEventArray(std::shared_ptr<BpfMap> map, int page_cnt,
py::function callback,
const std::string &struct_name,
py::object lost_callback)
: PerfEventArray(map, page_cnt, callback, lost_callback) {
auto parent = map->get_parent();
if (!parent) {
throw BpfException("Parent BpfObject has been destroyed");
}
parser_ = parent->get_struct_parser();
struct_name_ = struct_name;
if (!parser_) {
throw BpfException("No struct definitions available");
}
}
PerfEventArray::~PerfEventArray() {
if (pb_) {
perf_buffer__free(pb_);
}
}
void PerfEventArray::sample_callback_wrapper(void *ctx, int cpu, void *data,
unsigned int size) {
auto *self = static_cast<PerfEventArray *>(ctx);
// Acquire GIL for Python calls
py::gil_scoped_acquire acquire;
try {
// Convert data to Python bytes
py::bytes py_data(static_cast<const char *>(data), size);
if (self->parser_ && !self->struct_name_.empty()) {
py::object event = self->parser_->parse(self->struct_name_, py_data);
self->callback_(cpu, event);
} else {
self->callback_(cpu, py_data);
}
} catch (const py::error_already_set &e) {
PyErr_Print();
} catch (const std::exception &e) {
py::print("C++ error in perf callback:", e.what());
}
}
void PerfEventArray::lost_callback_wrapper(void *ctx, int cpu,
unsigned long long cnt) {
auto *self = static_cast<PerfEventArray *>(ctx);
py::gil_scoped_acquire acquire;
try {
if (!self->lost_callback_.is_none()) {
py::function lost_fn = py::cast<py::function>(self->lost_callback_);
lost_fn(cpu, cnt);
} else {
py::print("Lost", cnt, "events on CPU", cpu);
}
} catch (const py::error_already_set &e) {
PyErr_Print();
}
}
int PerfEventArray::poll(int timeout_ms) {
// Release GIL during blocking poll
py::gil_scoped_release release;
return perf_buffer__poll(pb_, timeout_ms);
}
int PerfEventArray::consume() {
py::gil_scoped_release release;
return perf_buffer__consume(pb_);
}

View File

@ -1,46 +0,0 @@
#ifndef PYLIBBPF_PERF_EVENT_ARRAY_H
#define PYLIBBPF_PERF_EVENT_ARRAY_H
#include <libbpf.h>
#include <memory>
#include <pybind11/pybind11.h>
#include <string>
class StructParser;
class BpfMap;
namespace py = pybind11;
class PerfEventArray {
private:
std::shared_ptr<BpfMap> map_;
struct perf_buffer *pb_;
py::function callback_;
py::object lost_callback_;
std::shared_ptr<StructParser> parser_;
std::string struct_name_;
// Static callback wrappers for C API
static void sample_callback_wrapper(void *ctx, int cpu, void *data,
unsigned int size);
static void lost_callback_wrapper(void *ctx, int cpu, unsigned long long cnt);
public:
PerfEventArray(std::shared_ptr<BpfMap> map, int page_cnt,
py::function callback, py::object lost_callback = py::none());
PerfEventArray(std::shared_ptr<BpfMap> map, int page_cnt,
py::function callback, const std::string &struct_name,
py::object lost_callback = py::none());
~PerfEventArray();
PerfEventArray(const PerfEventArray &) = delete;
PerfEventArray &operator=(const PerfEventArray &) = delete;
int poll(int timeout_ms);
int consume();
[[nodiscard]] std::shared_ptr<BpfMap> get_map() const { return map_; }
};
#endif // PYLIBBPF_PERF_EVENT_ARRAY_H

View File

@ -1,25 +0,0 @@
#include "struct_parser.h"
#include "core/bpf_exception.h"
StructParser::StructParser(py::dict structs) {
for (auto item : structs) {
std::string name = py::str(item.first);
struct_types_[name] = py::reinterpret_borrow<py::object>(item.second);
}
}
py::object StructParser::parse(const std::string &struct_name, py::bytes data) {
auto it = struct_types_.find(struct_name);
if (it == struct_types_.end()) {
throw BpfException("Unknown struct: " + struct_name);
}
py::object struct_type = it->second;
// Use ctypes.from_buffer_copy() to create struct from bytes
return struct_type.attr("from_buffer_copy")(data);
}
bool StructParser::has_struct(const std::string &struct_name) const {
return struct_types_.find(struct_name) != struct_types_.end();
}

View File

@ -1,20 +0,0 @@
#ifndef PYLIBBPF_STRUCT_PARSER_H
#define PYLIBBPF_STRUCT_PARSER_H
#include <pybind11/pybind11.h>
#include <string>
#include <unordered_map>
namespace py = pybind11;
class StructParser {
private:
std::unordered_map<std::string, py::object> struct_types_;
public:
explicit StructParser(py::dict structs);
py::object parse(const std::string &struct_name, py::bytes data);
bool has_struct(const std::string &struct_name) const;
};
#endif

View File

@ -2,6 +2,6 @@ import pylibbpf as m
def test_main(): def test_main():
assert m.__version__ == "0.0.7" assert m.__version__ == "0.0.5"
prog = m.BpfObject("tests/execve2.o", structs={}) prog = m.BpfProgram("tests/execve2.o")
print(prog) print(prog)

1
xdp-tools Submodule

Submodule xdp-tools added at fa3bcd03cc