Files
uLib/docs/python/developer_guide.md
2026-03-06 10:45:33 +00:00

5.5 KiB
Raw Blame History

Developer Guide Python Bindings

This guide is aimed at contributors who want to extend or modify the Python bindings for uLib.


Repository Layout

ulib/
├── src/
│   └── Python/
│       ├── module.cpp               # pybind11 module entry point
│       ├── core_bindings.cpp        # uLib::Core bindings
│       ├── math_bindings.cpp        # uLib::Math bindings
│       ├── math_filters_bindings.cpp# VoxImageFilter bindings
│       ├── CMakeLists.txt           # builds uLib_python shared lib
│       ├── testing/                 # Python unit tests
│       │   ├── pybind_test.py
│       │   ├── core_pybind_test.py
│       │   ├── math_pybind_test.py
│       │   └── math_filters_test.py
│       └── uLib/                    # Python package (uLib_python.so lands here)
│           └── __init__.py
├── build_python.py                  # poetry build hook (calls CMake)
├── pyproject.toml                   # poetry metadata
└── condaenv.yml                     # conda/micromamba environment

Adding a New Binding

All bindings live in the four source files listed above. The module entry point module.cpp calls init_core(), init_math(), and init_math_filters() in order.

1. Pick (or create) the right binding file

C++ header location Binding file
src/Core/ core_bindings.cpp
src/Math/ (geometry, grids, VoxImage) math_bindings.cpp
src/Math/VoxImageFilter*.hpp math_filters_bindings.cpp

2. Add the #include directive

// math_bindings.cpp
#include "Math/MyNewClass.h"

3. Write the pybind11 binding inside the appropriate init_* function

void init_math(py::module_ &m) {
    // ... existing bindings ...

    py::class_<MyNewClass>(m, "MyNewClass")
        .def(py::init<>())
        .def("MyMethod",  &MyNewClass::MyMethod)
        .def("AnotherMethod", &MyNewClass::AnotherMethod,
             py::arg("x"), py::arg("y") = 0.0f);
}

4. Rebuild only the Python target

cmake --build build --target uLib_python -j$(nproc)

5. Write a Python test

Add a new test class to the relevant test file (or create a new one under src/Python/testing/):

# src/Python/testing/math_pybind_test.py
class TestMyNewClass(unittest.TestCase):
    def test_basic(self):
        obj = uLib.Math.MyNewClass()
        result = obj.MyMethod()
        self.assertAlmostEqual(result, expected_value)

Register the test in src/Python/CMakeLists.txt if you add a new file:

add_test(NAME pybind_my_new
         COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/my_new_test.py)
set_tests_properties(pybind_my_new PROPERTIES
    ENVIRONMENT "PYTHONPATH=$<TARGET_FILE_DIR:uLib_python>:${PROJECT_SOURCE_DIR}/src/Python")

Build System Details

CMakeLists.txt (src/Python/)

pybind11_add_module compiles the shared library uLib_python and links it against the C++ static/shared libraries uLibCore and uLibMath. The install target copies the .so into the standard library directory.

pybind11_add_module(uLib_python
    module.cpp core_bindings.cpp math_bindings.cpp math_filters_bindings.cpp)

target_link_libraries(uLib_python PRIVATE uLibCore uLibMath)

poetry / build_python.py

pyproject.toml declares build_python.py as the custom build hook. When poetry install or poetry build is invoked it:

  1. Calls cmake <root> -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=<pkg_dir> ... in build_python/.
  2. Builds only the uLib_python target.
  3. The resulting .so is placed inside src/Python/uLib/ so it is picked up by Poetry as a package data file.

The USE_CUDA environment variable gates CUDA support at build time:

USE_CUDA=ON poetry install   # with CUDA
USE_CUDA=OFF poetry install  # CPU only (default)

Running All Tests

# From the repository root, with PYTHONPATH set:
export PYTHONPATH="$(pwd)/build/src/Python:$(pwd)/src/Python"

python -m pytest src/Python/testing/ -v

Or through CMake's test runner (after building the full project):

cd build
ctest --output-on-failure -R pybind

Expected output (all passing):

    Start 1: pybind_general
1/4 Test #1: pybind_general .............   Passed
    Start 2: pybind_core
2/4 Test #2: pybind_core ................   Passed
    Start 3: pybind_math
3/4 Test #3: pybind_math ................   Passed
    Start 4: pybind_math_filters
4/4 Test #4: pybind_math_filters ........   Passed

Memory Management Notes

uLib::Vector<T> has explicit GPU memory management. When wrapping methods that return references to internal data, use py::return_value_policy::reference_internal to avoid dangling references:

.def("Data", &VoxImage<Voxel>::Data,
     py::return_value_policy::reference_internal)

For objects held by std::unique_ptr without Python-side deletion, use py::nodelete:

py::class_<Abstract::VoxImageFilter,
           std::unique_ptr<Abstract::VoxImageFilter, py::nodelete>>(m, "AbstractVoxImageFilter")

Useful References