diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 467cc4c..f4f5ada 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -45,7 +45,7 @@ jobs: run: pip install --verbose .[test] - name: Test import - run: python -c "import pylibbpf; print('Import successful')" + run: python -I -c "import pylibbpf; print('Import successful')" - name: Test - run: python -m pytest -v + run: python -I -m pytest -v diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bc7371..c8b690d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,31 @@ cmake_minimum_required(VERSION 4.0) project(pylibbpf) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + # pybind11 include_directories(${CMAKE_SOURCE_DIR}/src) add_subdirectory(pybind11) pybind11_add_module( pylibbpf + # Core src/core/bpf_program.h src/core/bpf_exception.h src/core/bpf_map.h - src/bindings/main.cpp + src/core/bpf_object.h 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 --- set(LIBBPF_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libbpf/src) diff --git a/examples/execve.py b/examples/execve.py index c6a3f9f..d47f327 100644 --- a/examples/execve.py +++ b/examples/execve.py @@ -1,10 +1,11 @@ import time 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.maps import HashMap +from pylibbpf import BpfMap + @bpf @map diff --git a/pylibbpf/__init__.py b/pylibbpf/__init__.py new file mode 100644 index 0000000..e746b99 --- /dev/null +++ b/pylibbpf/__init__.py @@ -0,0 +1,46 @@ +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.6" diff --git a/pylibbpf/ir_to_ctypes.py b/pylibbpf/ir_to_ctypes.py new file mode 100644 index 0000000..059e664 --- /dev/null +++ b/pylibbpf/ir_to_ctypes.py @@ -0,0 +1,105 @@ +import ctypes +import logging +from typing import Dict, Type + +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"] diff --git a/pylibbpf/py.typed b/pylibbpf/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pylibbpf/wrappers.py b/pylibbpf/wrappers.py new file mode 100644 index 0000000..aa7679c --- /dev/null +++ b/pylibbpf/wrappers.py @@ -0,0 +1,77 @@ +from typing import Callable, Optional + + +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: Optional[Callable] = 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) diff --git a/pyproject.toml b/pyproject.toml index 45b0c4a..08c7b8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,13 @@ requires = [ "wheel", "ninja", "cmake>=4.0", + "pybind11>=2.10", ] build-backend = "setuptools.build_meta" [project] name = "pylibbpf" -version = "0.0.5" +version = "0.0.6" description = "Python Bindings for Libbpf" authors = [ { name = "r41k0u", email = "pragyanshchaturvedi18@gmail.com" }, @@ -32,14 +33,17 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Operating System Kernels :: Linux", ] +dependencies = [ + "llvmlite>=0.40.0", +] [project.optional-dependencies] test = ["pytest>=6.0"] [project.urls] -Homepage = "https://github.com/varun-r-mallya/pylibbpf" -Repository = "https://github.com/varun-r-mallya/pylibbpf" -Issues = "https://github.com/varun-r-mallya/pylibbpf/issues" +Homepage = "https://github.com/pythonbpf/pylibbpf" +Repository = "https://github.com/pythonbpf/pylibbpf" +Issues = "https://github.com/pythonbpf/pylibbpf/issues" [tool.mypy] files = "setup.py" diff --git a/setup.py b/setup.py index 37e5302..41abc05 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import sys from pathlib import Path -from setuptools import Extension, setup +from setuptools import Extension, find_packages, setup from setuptools.command.build_ext import build_ext # Convert distutils Windows platform specifiers to CMake -A arguments @@ -129,8 +129,11 @@ def build_extension(self, ext: CMakeExtension) -> None: 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")], + url="https://github.com/pythonbpf/pylibbpf", + packages=find_packages(where="."), + package_dir={"": "."}, + py_modules=[], # Empty since we use packages + ext_modules=[CMakeExtension("pylibbpf.pylibbpf")], cmdclass={"build_ext": CMakeBuild}, zip_safe=False, classifiers=[ @@ -147,6 +150,16 @@ def build_extension(self, ext: CMakeExtension) -> None: "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Operating System Kernels :: Linux", ], + install_requires=[ + "llvmlite>=0.40.0", # Required for struct conversion + ], extras_require={"test": ["pytest>=6.0"]}, python_requires=">=3.8", + package_data={ + "pylibbpf": [ + "*.py", + "py.typed", # For type hints + ], + }, + include_package_data=True, ) diff --git a/src/bindings/main.cpp b/src/bindings/main.cpp index ea3a6e3..023215c 100644 --- a/src/bindings/main.cpp +++ b/src/bindings/main.cpp @@ -2,18 +2,17 @@ #define STRINGIFY(x) #x #define MACRO_STRINGIFY(x) STRINGIFY(x) -extern "C" { -#include -} - -#include "core/bpf_program.h" #include "core/bpf_exception.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; PYBIND11_MODULE(pylibbpf, m) { - m.doc() = R"pbdoc( + m.doc() = R"pbdoc( Pylibbpf - libbpf bindings for Python ----------------------- @@ -27,41 +26,73 @@ PYBIND11_MODULE(pylibbpf, m) { BpfException )pbdoc"; - // Register the custom exception - py::register_exception(m, "BpfException"); + // Register the custom exception + py::register_exception(m, "BpfException"); + + // BpfObject + py::class_>(m, "BpfObject") + .def(py::init(), py::arg("object_path"), + py::arg("structs") = py::dict()) + .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_>(m, "BpfProgram") + .def("attach", &BpfProgram::attach) + .def("detach", &BpfProgram::detach) + .def("is_attached", &BpfProgram::is_attached) + .def("get_name", &BpfProgram::get_name); - py::class_(m, "BpfProgram") - .def(py::init()) - .def(py::init()) - .def("load", &BpfProgram::load) - .def("attach", &BpfProgram::attach) - .def("destroy", &BpfProgram::destroy) - .def("load_and_attach", &BpfProgram::load_and_attach) - .def("is_loaded", &BpfProgram::is_loaded) - .def("is_attached", &BpfProgram::is_attached); + // BpfMap + py::class_>(m, "BpfMap") + .def("lookup", &BpfMap::lookup, py::arg("key")) + .def("update", &BpfMap::update, py::arg("key"), py::arg("value")) + .def("delete_elem", &BpfMap::delete_elem, py::arg("key")) + .def("get_next_key", &BpfMap::get_next_key, py::arg("key") = py::none()) + .def("items", &BpfMap::items) + .def("keys", &BpfMap::keys) + .def("values", &BpfMap::values) + .def("get_name", &BpfMap::get_name) + .def("get_fd", &BpfMap::get_fd) + .def("get_type", &BpfMap::get_type) + .def("get_key_size", &BpfMap::get_key_size) + .def("get_value_size", &BpfMap::get_value_size) + .def("get_max_entries", &BpfMap::get_max_entries) + .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")); - py::class_(m, "BpfMap") - .def(py::init()) - .def("lookup", &BpfMap::lookup) - .def("update", &BpfMap::update) - .def("delete", &BpfMap::delete_elem) - .def("get_next_key", &BpfMap::get_next_key, py::arg("key") = py::none()) - .def("items", &BpfMap::items) - .def("keys", &BpfMap::keys) - .def("values", &BpfMap::values) - .def("get_name", &BpfMap::get_name) - .def("get_type", &BpfMap::get_type) - .def("get_key_size", &BpfMap::get_key_size) - .def("get_value_size", &BpfMap::get_value_size) - .def("get_max_entries", &BpfMap::get_max_entries) - .def("__getitem__", &BpfMap::lookup) - .def("__setitem__", &BpfMap::update) - .def("__delitem__", &BpfMap::delete_elem); + // StructParser + py::class_(m, "StructParser") + .def(py::init(), 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_>(m, + "PerfEventArray") + .def(py::init, int, py::function, py::object>(), + py::arg("map"), py::arg("page_cnt"), py::arg("callback"), + py::arg("lost_callback") = py::none()) + .def(py::init, 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 - m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); #else - m.attr("__version__") = "dev"; + m.attr("__version__") = "dev"; #endif } diff --git a/src/core/bpf_exception.h b/src/core/bpf_exception.h index 4616c19..7266ce9 100644 --- a/src/core/bpf_exception.h +++ b/src/core/bpf_exception.h @@ -6,13 +6,10 @@ class BpfException final : public std::runtime_error { public: - explicit BpfException(const std::string &message) - : std::runtime_error(message) { - } + explicit BpfException(const std::string &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 diff --git a/src/core/bpf_map.cpp b/src/core/bpf_map.cpp index 3e19aa0..213233f 100644 --- a/src/core/bpf_map.cpp +++ b/src/core/bpf_map.cpp @@ -1,211 +1,230 @@ -#include "bpf_map.h" - -#include "bpf_exception.h" - -BpfMap::BpfMap(BpfProgram *program_, const py::object &map_from_python) { - if (py::isinstance(map_from_python)) { - const auto name = map_from_python.attr("__name__").cast(); - bpf_program = program_; - map_ = bpf_object__find_map_by_name(bpf_program->get_obj(), name.c_str()); - if (!map_) { - throw BpfException("Failed to find map by name"); - } - map_fd = bpf_map__fd(map_); - if (map_fd == -1) { - throw BpfException("Failed to open map File Descriptor"); - } - } else { - throw BpfException("Invalid map object passed to function."); - } +#include "core/bpf_map.h" +#include "core/bpf_exception.h" +#include "core/bpf_object.h" +#include +#include +#include + +BpfMap::BpfMap(std::shared_ptr parent, struct bpf_map *raw_map, + 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_); + if (map_fd_ < 0) + throw BpfException("Failed to get file descriptor for map '" + map_name_ + + "'"); + + key_size_ = bpf_map__key_size(map_); + value_size_ = bpf_map__value_size(map_); } -std::vector BpfMap::python_to_bytes(const py::object &obj, size_t size) { - std::vector result(size, 0); - - if (py::isinstance(obj)) { - const auto value = obj.cast(); - std::memcpy(result.data(), &value, std::min(size, sizeof(uint64_t))); - } else if (py::isinstance(obj)) { - const auto bytes_str = obj.cast(); - std::memcpy(result.data(), bytes_str.data(), std::min(size, bytes_str.size())); - } else if (py::isinstance(obj)) { - const auto str_val = obj.cast(); - std::memcpy(result.data(), str_val.data(), std::min(size, str_val.size())); - } - - return result; -} - -py::object BpfMap::bytes_to_python(const std::vector &data) { - // Try to interpret as integer if it's a common integer size - if (data.size() == 4) { - uint32_t value; - std::memcpy(&value, data.data(), 4); - return py::cast(value); - } else if (data.size() == 8) { - uint64_t value; - std::memcpy(&value, data.data(), 8); - return py::cast(value); - } else { - // Return as bytes - return py::bytes(reinterpret_cast(data.data()), data.size()); - } +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) + throw py::key_error("Key not found in map '" + map_name_ + "'"); + 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 { - 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"); - } + 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 { - const size_t key_size = bpf_map__key_size(map_); - std::vector key_bytes; - key_bytes = python_to_bytes(key, key_size); + if (map_fd_ < 0) + throw BpfException("Map '" + map_name_ + "' is not initialized properly"); - 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"); + 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 { + BufferManager<> key_buf; + 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::list BpfMap::get_next_key(const py::object &key) const { - const size_t key_size = bpf_map__key_size(map_); - std::vector next_key(key_size); +py::dict BpfMap::items() const { + py::dict result; - 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::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; } + } - py::list result; - if (ret == 0) { - result.append(bytes_to_python(next_key)); - } - return result; + return result; } py::list BpfMap::keys() const { - py::list result; - const size_t key_size = bpf_map__key_size(map_); + py::list result; - std::vector key(key_size); - std::vector next_key(key_size); + py::object current_key = get_next_key(py::none()); + if (current_key.is_none()) { + return result; + } - int ret = bpf_map__get_next_key(map_, nullptr, key.data(), key_size); + while (!current_key.is_none()) { + result.append(current_key); + current_key = get_next_key(current_key); + } - 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; + 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 key(key_size); - std::vector next_key(key_size); - std::vector 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; - } + 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; + } + } -std::string BpfMap::get_name() const { - const char *name = bpf_map__name(map_); - return name ? std::string(name) : ""; + return result; } -int BpfMap::get_type() const { - return bpf_map__type(map_); -} +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_max_entries() const { return bpf_map__max_entries(map_); } -int BpfMap::get_value_size() const { - return bpf_map__value_size(map_); -} +// Helper functions +void BpfMap::python_to_bytes_inplace(const py::object &obj, + std::span buffer) { + std::fill(buffer.begin(), buffer.end(), 0); -int BpfMap::get_max_entries() const { - return bpf_map__max_entries(map_); -} + if (py::isinstance(obj)) { + if (buffer.size() <= sizeof(uint64_t)) { + uint64_t value = obj.cast(); + std::memcpy(buffer.data(), &value, buffer.size()); + } else { + throw BpfException("Integer key/value size exceeds maximum (8 bytes)"); + } + } else if (py::isinstance(obj)) { + std::string bytes_str = obj.cast(); -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 key(key_size); - std::vector next_key(key_size); - std::vector 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; + if (bytes_str.size() > buffer.size()) { + throw BpfException("Bytes size " + std::to_string(bytes_str.size()) + + " exceeds expected size " + + std::to_string(buffer.size())); } - return result; -} + std::memcpy(buffer.data(), bytes_str.data(), bytes_str.size()); + } else if (py::isinstance(obj)) { + std::string str_val = obj.cast(); -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 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(); + if (str_val.size() >= buffer.size()) { + throw BpfException("String size exceeds expected size"); } - return bytes_to_python(value_bytes); + std::memcpy(buffer.data(), str_val.data(), str_val.size()); + buffer[str_val.size()] = '\0'; + } else { + throw BpfException("Unsupported type for BPF map key/value"); + } +} + +py::object BpfMap::bytes_to_python(std::span data) { + if (data.size() == 4) { + uint32_t value; + std::memcpy(&value, data.data(), 4); + return py::cast(value); + } else if (data.size() == 8) { + uint64_t value; + std::memcpy(&value, data.data(), 8); + return py::cast(value); + } else { + return py::bytes(reinterpret_cast(data.data()), data.size()); + } } diff --git a/src/core/bpf_map.h b/src/core/bpf_map.h index e6ad13a..e3d7af3 100644 --- a/src/core/bpf_map.h +++ b/src/core/bpf_map.h @@ -1,55 +1,72 @@ -#ifndef PYLIBBPF_MAPS_H -#define PYLIBBPF_MAPS_H +#ifndef PYLIBBPF_BPF_MAP_H +#define PYLIBBPF_BPF_MAP_H +#include #include #include -#include +#include #include +#include -#include "bpf_program.h" +class BpfObject; namespace py = pybind11; -class BpfMap { +class BpfMap : public std::enable_shared_from_this { private: - struct bpf_map *map_; - int map_fd = -1; - //TODO: turn below into a shared pointer and ref count it so that there is no resource leakage - BpfProgram *bpf_program; + std::weak_ptr parent_obj_; + struct bpf_map *map_; + int map_fd_; + std::string map_name_; + __u32 key_size_, value_size_; + + template struct BufferManager { + std::array stack_buf; + std::vector heap_buf; + + std::span get_span(size_t size) { + if (size <= StackSize) { + return std::span(stack_buf.data(), size); + } else { + heap_buf.resize(size); + return std::span(heap_buf); + } + } + }; public: - BpfMap(BpfProgram *program_, const py::object &map_from_python); - - ~BpfMap() = default; - - [[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::list get_next_key(const py::object &key = py::none()) const; - - py::dict items() const; - - py::list keys() const; - - py::list values() const; - - [[nodiscard]] std::string get_name() const; - - int get_type() const; - - int get_key_size() const; - - int get_value_size() const; - - int get_max_entries() const; + BpfMap(std::shared_ptr parent, struct bpf_map *raw_map, + const std::string &map_name); + + ~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; + 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; + + [[nodiscard]] std::string get_name() const { return map_name_; } + [[nodiscard]] int get_fd() const { return map_fd_; } + [[nodiscard]] int get_type() const; + [[nodiscard]] int get_key_size() const { return key_size_; }; + [[nodiscard]] int get_value_size() const { return value_size_; }; + [[nodiscard]] int get_max_entries() const; + [[nodiscard]] std::shared_ptr get_parent() const { + return parent_obj_.lock(); + } private: - static std::vector python_to_bytes(const py::object &obj, size_t size); - - static py::object bytes_to_python(const std::vector &data); + static void python_to_bytes_inplace(const py::object &obj, + std::span buffer); + static py::object bytes_to_python(std::span data); }; -#endif //PYLIBBPF_MAPS_H +#endif // PYLIBBPF_BPF_MAP_H diff --git a/src/core/bpf_object.cpp b/src/core/bpf_object.cpp new file mode 100644 index 0000000..fd80c8f --- /dev/null +++ b/src/core/bpf_object.cpp @@ -0,0 +1,272 @@ +#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 +#include +#include + +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 +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(shared_from_this(), prog, prog_name); + prog_cache_[prog_name] = bpf_prog; + + return bpf_prog; +} + +std::shared_ptr 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(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 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(shared_from_this(), raw_map, name); + maps_cache_[name] = map; + + return map; +} + +std::shared_ptr 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(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 BpfObject::get_struct_parser() const { + if (!struct_parser_ && !struct_defs_.empty()) { + // Create parser on first access + struct_parser_ = std::make_shared(struct_defs_); + } + return struct_parser_; +} diff --git a/src/core/bpf_object.h b/src/core/bpf_object.h new file mode 100644 index 0000000..bbc0576 --- /dev/null +++ b/src/core/bpf_object.h @@ -0,0 +1,88 @@ +#ifndef PYLIBBPF_BPF_OBJECT_H +#define PYLIBBPF_BPF_OBJECT_H + +#include +#include +#include +#include +#include + +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 { +private: + struct bpf_object *obj_; + std::string object_path_; + bool loaded_; + + mutable std::unordered_map> maps_cache_; + mutable std::unordered_map> + prog_cache_; + py::dict struct_defs_; + mutable std::shared_ptr struct_parser_; + + std::shared_ptr _get_or_create_program(struct bpf_program *prog); + std::shared_ptr _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 + 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 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 get_struct_parser() const; +}; + +#endif // PYLIBBPF_BPF_OBJECT_H diff --git a/src/core/bpf_program.cpp b/src/core/bpf_program.cpp index 5b9fbb2..cf2a603 100644 --- a/src/core/bpf_program.cpp +++ b/src/core/bpf_program.cpp @@ -1,84 +1,74 @@ -#include "bpf_program.h" -#include "bpf_exception.h" -#include +#include "core/bpf_program.h" +#include "core/bpf_exception.h" +#include "core/bpf_object.h" +#include +#include #include -BpfProgram::BpfProgram(std::string object_path, std::string program_name) - : obj_(nullptr), prog_(nullptr), link_(nullptr), - object_path_(std::move(object_path)), program_name_(std::move(program_name)) { +BpfProgram::BpfProgram(std::shared_ptr parent, + struct bpf_program *raw_prog, + const std::string &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() { - destroy(); - if (obj_) { - bpf_object__close(obj_); - } -} - -struct bpf_object * BpfProgram::get_obj() const { - return obj_; -} +BpfProgram::~BpfProgram() { detach(); } -bool BpfProgram::load() { - // Open the eBPF object file - obj_ = bpf_object__open_file(object_path_.c_str(), nullptr); - if (libbpf_get_error(obj_)) { - throw BpfException("Failed to open BPF object file: " + object_path_); - } +BpfProgram::BpfProgram(BpfProgram &&other) noexcept + : parent_obj_(std::move(other.parent_obj_)), prog_(other.prog_), + link_(other.link_), program_name_(std::move(other.program_name_)) { - // 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_) { - throw BpfException("Program '" + program_name_ + "' not found in object"); - } - } else { - while ((prog_ = bpf_object__next_program(obj_, prog_)) != nullptr) { - programs.emplace_back(prog_, nullptr); - } + other.prog_ = nullptr; + other.link_ = nullptr; +} - // throw if no programs found - if (programs.empty()) { - throw BpfException("No programs found in object file"); - } - } +BpfProgram &BpfProgram::operator=(BpfProgram &&other) noexcept { + if (this != &other) { + detach(); - // Load the eBPF object into the kernel - if (bpf_object__load(obj_)) { - throw BpfException("Failed to load BPF object into kernel"); - } + parent_obj_ = std::move(other.parent_obj_); + prog_ = other.prog_; + link_ = other.link_; + program_name_ = std::move(other.program_name_); - return true; + other.prog_ = nullptr; + other.link_ = nullptr; + } + return *this; } -bool BpfProgram::attach() { - for (auto [prog, link]: programs) { - if (!prog) { - throw BpfException("Program not loaded"); - } +void BpfProgram::attach() { + // Check if parent is still alive + auto parent = parent_obj_.lock(); + if (!parent) { + throw BpfException("Parent BpfObject has been destroyed"); + } - link = bpf_program__attach(prog); - if (libbpf_get_error(link)) { - link = nullptr; - throw BpfException("Failed to attach BPF program"); - } - } + if (link_) { + throw BpfException("Program '" + program_name_ + "' already attached"); + } - return true; -} + if (!prog_) { + throw BpfException("Program '" + program_name_ + "' not initialized"); + } -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; + link_ = bpf_program__attach(prog_); + if (!link_) { + std::string err_msg = "bpf_program__attach failed for program '" + + program_name_ + "': " + std::strerror(errno); + throw BpfException(err_msg); + } } -void BpfProgram::load_and_attach() { - load(); - attach(); +void BpfProgram::detach() { + if (link_) { + bpf_link__destroy(link_); + link_ = nullptr; + } } diff --git a/src/core/bpf_program.h b/src/core/bpf_program.h index 93532da..2cc16a9 100644 --- a/src/core/bpf_program.h +++ b/src/core/bpf_program.h @@ -2,37 +2,35 @@ #define PYLIBBPF_BPF_PROGRAM_H #include -#include +#include #include -namespace py = pybind11; +class BpfObject; class BpfProgram { private: - struct bpf_object *obj_; - struct bpf_program *prog_; - struct bpf_link *link_; - std::string object_path_; - std::string program_name_; - std::vector > programs; + std::weak_ptr parent_obj_; + struct bpf_program *prog_; + struct bpf_link *link_; + std::string program_name_; public: - explicit BpfProgram(std::string object_path, std::string program_name = ""); + explicit BpfProgram(std::shared_ptr parent, + struct bpf_program *raw_prog, + const std::string &program_name); - ~BpfProgram(); + ~BpfProgram(); - struct bpf_object *get_obj() const; + BpfProgram(const BpfProgram &) = delete; + BpfProgram &operator=(const BpfProgram &) = delete; + BpfProgram(BpfProgram &&) noexcept; + BpfProgram &operator=(BpfProgram &&) noexcept; - bool load(); + void attach(); + 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 diff --git a/src/maps/perf_event_array.cpp b/src/maps/perf_event_array.cpp new file mode 100644 index 0000000..8ffc20d --- /dev/null +++ b/src/maps/perf_event_array.cpp @@ -0,0 +1,117 @@ +#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 +#include + +PerfEventArray::PerfEventArray(std::shared_ptr 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 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(ctx); + + // Acquire GIL for Python calls + py::gil_scoped_acquire acquire; + + try { + // Convert data to Python bytes + py::bytes py_data(static_cast(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(ctx); + + py::gil_scoped_acquire acquire; + + try { + if (!self->lost_callback_.is_none()) { + py::function lost_fn = py::cast(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_); +} diff --git a/src/maps/perf_event_array.h b/src/maps/perf_event_array.h new file mode 100644 index 0000000..1a1cba9 --- /dev/null +++ b/src/maps/perf_event_array.h @@ -0,0 +1,46 @@ +#ifndef PYLIBBPF_PERF_EVENT_ARRAY_H +#define PYLIBBPF_PERF_EVENT_ARRAY_H + +#include +#include +#include +#include + +class StructParser; +class BpfMap; + +namespace py = pybind11; + +class PerfEventArray { +private: + std::shared_ptr map_; + struct perf_buffer *pb_; + py::function callback_; + py::object lost_callback_; + + std::shared_ptr 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 map, int page_cnt, + py::function callback, py::object lost_callback = py::none()); + PerfEventArray(std::shared_ptr 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 get_map() const { return map_; } +}; + +#endif // PYLIBBPF_PERF_EVENT_ARRAY_H diff --git a/src/utils/struct_parser.cpp b/src/utils/struct_parser.cpp new file mode 100644 index 0000000..e1d1700 --- /dev/null +++ b/src/utils/struct_parser.cpp @@ -0,0 +1,25 @@ +#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(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(); +} diff --git a/src/utils/struct_parser.h b/src/utils/struct_parser.h new file mode 100644 index 0000000..aa9ed64 --- /dev/null +++ b/src/utils/struct_parser.h @@ -0,0 +1,20 @@ +#ifndef PYLIBBPF_STRUCT_PARSER_H +#define PYLIBBPF_STRUCT_PARSER_H + +#include +#include +#include + +namespace py = pybind11; + +class StructParser { +private: + std::unordered_map 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 diff --git a/tests/test_basic.py b/tests/test_basic.py index 98ac1cf..31ac054 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,6 +2,6 @@ def test_main(): - assert m.__version__ == "0.0.5" - prog = m.BpfProgram("tests/execve2.o") + assert m.__version__ == "0.0.6" + prog = m.BpfObject("tests/execve2.o", structs={}) print(prog)