180 lines
5.5 KiB
Markdown
180 lines
5.5 KiB
Markdown
# 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
|
||
|
||
```cpp
|
||
// math_bindings.cpp
|
||
#include "Math/MyNewClass.h"
|
||
```
|
||
|
||
### 3. Write the pybind11 binding inside the appropriate `init_*` function
|
||
|
||
```cpp
|
||
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
|
||
|
||
```bash
|
||
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/`):
|
||
|
||
```python
|
||
# 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:
|
||
|
||
```cmake
|
||
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.
|
||
|
||
```cmake
|
||
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:
|
||
|
||
```bash
|
||
USE_CUDA=ON poetry install # with CUDA
|
||
USE_CUDA=OFF poetry install # CPU only (default)
|
||
```
|
||
|
||
---
|
||
|
||
## Running All Tests
|
||
|
||
```bash
|
||
# 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):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```cpp
|
||
.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`:
|
||
|
||
```cpp
|
||
py::class_<Abstract::VoxImageFilter,
|
||
std::unique_ptr<Abstract::VoxImageFilter, py::nodelete>>(m, "AbstractVoxImageFilter")
|
||
```
|
||
|
||
---
|
||
|
||
## Useful References
|
||
|
||
- [pybind11 documentation](https://pybind11.readthedocs.io)
|
||
- [pybind11 – STL containers](https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html)
|
||
- [pybind11 – Eigen integration](https://pybind11.readthedocs.io/en/stable/advanced/cast/eigen.html)
|
||
- [CMake – pybind11 integration](https://pybind11.readthedocs.io/en/stable/compiling.html)
|