|
| 1 | +# Nanobind Migration Plan |
| 2 | + |
| 3 | +## Overview |
| 4 | +This document outlines the plan for migrating the python-samplerate-ledfx bindings from pybind11 to nanobind 2.9.2. The migration will create a new nanobind implementation that can be imported as `samplerate-nb` while maintaining the existing pybind11 bindings for regression testing. |
| 5 | + |
| 6 | +## Goals |
| 7 | +1. Create a drop-in replacement for the existing pybind11 bindings |
| 8 | +2. Maintain API compatibility (seamless consumer experience) |
| 9 | +3. Leverage nanobind's performance improvements (smaller binaries, faster compilation) |
| 10 | +4. Enable comprehensive regression testing against pybind11 baseline |
| 11 | +5. Development name: `samplerate-nb`, internal module: `samplerate` for easy migration |
| 12 | + |
| 13 | +## Key Differences: pybind11 vs nanobind 2.9.2 |
| 14 | + |
| 15 | +### Philosophy |
| 16 | +- **pybind11**: Broad feature coverage, aims to bind all of C++ |
| 17 | +- **nanobind**: Focused on common use cases, optimized for efficiency and simplicity |
| 18 | + |
| 19 | +### Performance Benefits |
| 20 | +- **Binary size**: ~5x smaller |
| 21 | +- **Compile time**: ~4x faster |
| 22 | +- **Runtime overhead**: ~10x reduction |
| 23 | +- **Memory footprint**: ~2.3x reduction per wrapped object |
| 24 | + |
| 25 | +### Technical Requirements |
| 26 | +- **Minimum C++**: C++17 (vs C++14 for pybind11) |
| 27 | +- **Minimum Python**: 3.8+ |
| 28 | +- **CMake**: 3.15+ |
| 29 | + |
| 30 | +### API Changes |
| 31 | +- Very similar syntax to pybind11 for most use cases |
| 32 | +- Some fringe features removed/changed |
| 33 | +- Better stub generation with NDArray types |
| 34 | +- Improved multi-threading support with localized locking |
| 35 | + |
| 36 | +## Migration Phases |
| 37 | + |
| 38 | +### Phase 1: Infrastructure Setup ✅ |
| 39 | +**Status**: Ready to begin |
| 40 | + |
| 41 | +**Tasks**: |
| 42 | +1. ✅ Explore repository structure |
| 43 | +2. ✅ Understand existing pybind11 bindings |
| 44 | +3. ✅ Build and test baseline (87 tests passing) |
| 45 | +4. ✅ Research nanobind 2.9.2 features |
| 46 | +5. Create NANOBIND_PLAN.md document |
| 47 | +6. Update `.gitignore` for compiled extensions |
| 48 | + |
| 49 | +**Deliverables**: |
| 50 | +- Working baseline with all tests passing |
| 51 | +- Comprehensive understanding of codebase |
| 52 | +- Migration plan document |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +### Phase 2: Build System Configuration |
| 57 | +**Status**: Not started |
| 58 | + |
| 59 | +**Tasks**: |
| 60 | +1. Update `external/CMakeLists.txt` to fetch nanobind |
| 61 | +2. Create new `CMakeLists_nb.txt` for nanobind module |
| 62 | +3. Update `setup.py` to support dual builds (both pybind11 and nanobind) |
| 63 | +4. Configure nanobind module to output as `samplerate` (internal) but be importable as `samplerate-nb` |
| 64 | +5. Test basic build infrastructure |
| 65 | + |
| 66 | +**Deliverables**: |
| 67 | +- CMake configuration for nanobind |
| 68 | +- Build system supporting both binding libraries |
| 69 | +- Empty nanobind module that compiles successfully |
| 70 | + |
| 71 | +**Technical Notes**: |
| 72 | +- Use FetchContent to get nanobind (similar to pybind11) |
| 73 | +- nanobind_add_module() replaces pybind11_add_module() |
| 74 | +- Separate build targets: `python-samplerate` (pybind11) and `python-samplerate-nb` (nanobind) |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +### Phase 3: Core Bindings Implementation |
| 79 | +**Status**: Not started |
| 80 | + |
| 81 | +**Tasks**: |
| 82 | +1. Create `src/samplerate_nb.cpp` (new file, do not modify existing) |
| 83 | +2. Port header includes from pybind11 to nanobind |
| 84 | +3. Implement basic module structure |
| 85 | +4. Port `ConverterType` enum |
| 86 | +5. Port `ResamplingException` custom exception |
| 87 | +6. Implement `get_converter_type()` helper function |
| 88 | +7. Implement `error_handler()` function |
| 89 | + |
| 90 | +**Deliverables**: |
| 91 | +- Working module skeleton with basic types |
| 92 | +- Exception handling matching pybind11 behavior |
| 93 | +- Helper functions operational |
| 94 | + |
| 95 | +**API Mapping**: |
| 96 | +```cpp |
| 97 | +pybind11 → nanobind |
| 98 | +#include <pybind11/pybind11.h> → #include <nanobind/nanobind.h> |
| 99 | +#include <pybind11/numpy.h> → #include <nanobind/ndarray.h> |
| 100 | +#include <pybind11/stl.h> → #include <nanobind/stl/string.h> |
| 101 | +#include <pybind11/functional.h> → #include <nanobind/stl/function.h> |
| 102 | + |
| 103 | +namespace py = pybind11; → namespace nb = nanobind; |
| 104 | +PYBIND11_MODULE(...) → NB_MODULE(...) |
| 105 | +py::array_t<float> → nb::ndarray<nb::numpy, float> |
| 106 | +py::gil_scoped_release → nb::gil_scoped_release |
| 107 | +py::gil_scoped_acquire → nb::gil_scoped_acquire |
| 108 | +``` |
| 109 | +
|
| 110 | +--- |
| 111 | +
|
| 112 | +### Phase 4: Simple API Implementation |
| 113 | +**Status**: Not started |
| 114 | +
|
| 115 | +**Tasks**: |
| 116 | +1. Port `resample()` function |
| 117 | +2. Adapt NumPy array handling for nanobind |
| 118 | +3. Handle GIL release/acquire |
| 119 | +4. Implement verbose parameter |
| 120 | +5. Write test comparing against pybind11 `resample()` |
| 121 | +
|
| 122 | +**Deliverables**: |
| 123 | +- Working `resample()` function |
| 124 | +- Tests passing for Simple API |
| 125 | +- Performance comparison data |
| 126 | +
|
| 127 | +**Testing Strategy**: |
| 128 | +```python |
| 129 | +import samplerate # pybind11 version |
| 130 | +import samplerate_nb # nanobind version |
| 131 | +
|
| 132 | +# Regression test |
| 133 | +output_pb = samplerate.resample(input_data, ratio, converter) |
| 134 | +output_nb = samplerate_nb.resample(input_data, ratio, converter) |
| 135 | +assert np.allclose(output_pb, output_nb) |
| 136 | +``` |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +### Phase 5: Full API Implementation |
| 141 | +**Status**: Not started |
| 142 | + |
| 143 | +**Tasks**: |
| 144 | +1. Port `Resampler` class |
| 145 | +2. Implement constructor, copy constructor, move constructor |
| 146 | +3. Port `process()` method with NumPy array handling |
| 147 | +4. Implement `set_ratio()`, `reset()`, `clone()` methods |
| 148 | +5. Expose readonly attributes |
| 149 | +6. Write tests for all Resampler functionality |
| 150 | + |
| 151 | +**Deliverables**: |
| 152 | +- Fully functional `Resampler` class |
| 153 | +- All methods tested against pybind11 baseline |
| 154 | +- Clone/copy semantics verified |
| 155 | + |
| 156 | +**Key Considerations**: |
| 157 | +- Ensure state management matches pybind11 behavior |
| 158 | +- Verify destructor behavior (src_delete handles nullptr) |
| 159 | +- Test multi-channel support (1D vs 2D arrays) |
| 160 | + |
| 161 | +--- |
| 162 | + |
| 163 | +### Phase 6: Callback API Implementation |
| 164 | +**Status**: Not started |
| 165 | + |
| 166 | +**Tasks**: |
| 167 | +1. Port `CallbackResampler` class |
| 168 | +2. Implement callback function handling |
| 169 | +3. Port context manager support (`__enter__`, `__exit__`) |
| 170 | +4. Implement `read()` method |
| 171 | +5. Handle callback error propagation |
| 172 | +6. Port callback wrapper function (`the_callback_func`) |
| 173 | +7. Write comprehensive callback tests |
| 174 | + |
| 175 | +**Deliverables**: |
| 176 | +- Fully functional `CallbackResampler` class |
| 177 | +- Context manager support working |
| 178 | +- Callback error handling tested |
| 179 | +- Multi-channel callback support verified |
| 180 | + |
| 181 | +**Key Considerations**: |
| 182 | +- Callback GIL management is critical (acquire when calling Python) |
| 183 | +- Error propagation from C callback to Python needs careful handling |
| 184 | +- Context manager should properly destroy state |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +### Phase 7: Module Organization |
| 189 | +**Status**: Not started |
| 190 | + |
| 191 | +**Tasks**: |
| 192 | +1. Port submodule structure (`exceptions`, `converters`, `_internals`) |
| 193 | +2. Implement convenience imports |
| 194 | +3. Add version attributes (`__version__`, `__libsamplerate_version__`) |
| 195 | +4. Verify all imports work as expected |
| 196 | +5. Test import patterns used in tests |
| 197 | + |
| 198 | +**Deliverables**: |
| 199 | +- Module organization matching pybind11 |
| 200 | +- All convenience imports working |
| 201 | +- Version information accessible |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +### Phase 8: Comprehensive Testing |
| 206 | +**Status**: Not started |
| 207 | + |
| 208 | +**Tasks**: |
| 209 | +1. Run all existing tests against nanobind implementation |
| 210 | +2. Create regression test suite comparing pybind11 vs nanobind |
| 211 | +3. Test all converter types (0-4, strings, enum) |
| 212 | +4. Test 1D and 2D array inputs |
| 213 | +5. Test edge cases (empty arrays, large ratios, etc.) |
| 214 | +6. Verify exception handling |
| 215 | +7. Test clone operations |
| 216 | +8. Performance benchmarking |
| 217 | + |
| 218 | +**Deliverables**: |
| 219 | +- All 87+ tests passing for nanobind |
| 220 | +- Regression test suite showing identical behavior |
| 221 | +- Performance comparison report |
| 222 | +- Documentation of any behavioral differences |
| 223 | + |
| 224 | +**Test Categories**: |
| 225 | +- Simple API: `test_simple()`, `test_match()` |
| 226 | +- Full API: `test_process()`, `test_Resampler_clone()` |
| 227 | +- Callback API: `test_callback()`, `test_callback_with()`, `test_CallbackResampler_clone()` |
| 228 | +- Type handling: `test_converter_type()` |
| 229 | +- Exceptions: tests in `test_exception.py` |
| 230 | +- Threading: `test_threading_performance.py` |
| 231 | +- Async: `test_asyncio_performance.py` |
| 232 | + |
| 233 | +--- |
| 234 | + |
| 235 | +### Phase 9: Performance Analysis |
| 236 | +**Status**: Not started |
| 237 | + |
| 238 | +**Tasks**: |
| 239 | +1. Compare compile times (pybind11 vs nanobind) |
| 240 | +2. Compare binary sizes |
| 241 | +3. Benchmark runtime performance |
| 242 | +4. Measure memory usage |
| 243 | +5. Test multi-threading performance |
| 244 | +6. Document findings |
| 245 | + |
| 246 | +**Deliverables**: |
| 247 | +- Performance comparison report |
| 248 | +- Binary size comparison |
| 249 | +- Runtime benchmark results |
| 250 | +- Threading scalability data |
| 251 | + |
| 252 | +**Metrics to Track**: |
| 253 | +- Compilation time (cold and warm builds) |
| 254 | +- Binary size (`.so` file) |
| 255 | +- Function call overhead |
| 256 | +- Memory per wrapped object |
| 257 | +- Multi-threaded scaling |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +### Phase 10: Documentation & Integration |
| 262 | +**Status**: Not started |
| 263 | + |
| 264 | +**Tasks**: |
| 265 | +1. Document build process for nanobind variant |
| 266 | +2. Update README with nanobind information |
| 267 | +3. Document performance improvements |
| 268 | +4. Create migration guide for consumers |
| 269 | +5. Document import changes (samplerate-nb) |
| 270 | +6. Add CI/CD configuration for dual builds (if needed) |
| 271 | + |
| 272 | +**Deliverables**: |
| 273 | +- Updated documentation |
| 274 | +- Migration guide for end users |
| 275 | +- CI/CD configuration |
| 276 | +- Final validation |
| 277 | + |
| 278 | +--- |
| 279 | + |
| 280 | +## Technical Implementation Notes |
| 281 | + |
| 282 | +### NumPy Array Handling |
| 283 | + |
| 284 | +**pybind11**: |
| 285 | +```cpp |
| 286 | +py::array_t<float, py::array::c_style | py::array::forcecast> input |
| 287 | +py::buffer_info inbuf = input.request(); |
| 288 | +``` |
| 289 | + |
| 290 | +**nanobind**: |
| 291 | +```cpp |
| 292 | +nb::ndarray<nb::numpy, float, nb::c_contig> input |
| 293 | +// Direct shape/data access without buffer_info |
| 294 | +size_t rows = input.shape(0); |
| 295 | +float* data = input.data(); |
| 296 | +``` |
| 297 | + |
| 298 | +### GIL Management |
| 299 | +Both libraries support similar GIL scoped release/acquire: |
| 300 | +```cpp |
| 301 | +// Release GIL for C++ operations |
| 302 | +{ |
| 303 | + nb::gil_scoped_release release; |
| 304 | + // ... call libsamplerate ... |
| 305 | +} |
| 306 | + |
| 307 | +// Acquire GIL for Python calls |
| 308 | +{ |
| 309 | + nb::gil_scoped_acquire acquire; |
| 310 | + // ... call Python callback ... |
| 311 | +} |
| 312 | +``` |
| 313 | + |
| 314 | +### Exception Handling |
| 315 | +nanobind uses same approach but with `nb::` namespace: |
| 316 | +```cpp |
| 317 | +nb::register_exception<ResamplingException>(m_exceptions, "ResamplingError", PyExc_RuntimeError); |
| 318 | +``` |
| 319 | + |
| 320 | +### Build Configuration |
| 321 | +```cmake |
| 322 | +# Fetch nanobind |
| 323 | +FetchContent_Declare( |
| 324 | + nanobind |
| 325 | + GIT_REPOSITORY https://github.com/wjakob/nanobind |
| 326 | + GIT_TAG v2.9.2 |
| 327 | +) |
| 328 | +FetchContent_MakeAvailable(nanobind) |
| 329 | +
|
| 330 | +# Create module |
| 331 | +nanobind_add_module(python-samplerate-nb src/samplerate_nb.cpp) |
| 332 | +``` |
| 333 | + |
| 334 | +## Success Criteria |
| 335 | + |
| 336 | +1. ✅ **Functional Parity**: All 87+ tests pass with nanobind implementation |
| 337 | +2. ✅ **API Compatibility**: Drop-in replacement (importable as samplerate-nb) |
| 338 | +3. ✅ **Regression Testing**: Identical behavior to pybind11 version |
| 339 | +4. ✅ **Performance**: Binary size reduction, faster compile times |
| 340 | +5. ✅ **Documentation**: Clear migration path for consumers |
| 341 | + |
| 342 | +## Risk Mitigation |
| 343 | + |
| 344 | +1. **Preserve pybind11 bindings**: Keep original for regression testing |
| 345 | +2. **Incremental implementation**: Test each component before moving on |
| 346 | +3. **Comprehensive testing**: Use existing test suite as baseline |
| 347 | +4. **Performance validation**: Benchmark at each phase |
| 348 | +5. **Documentation**: Record all differences and workarounds |
| 349 | + |
| 350 | +## Timeline Estimate |
| 351 | + |
| 352 | +- **Phase 1**: Infrastructure Setup - ✅ Complete |
| 353 | +- **Phase 2**: Build System - 1 hour |
| 354 | +- **Phase 3**: Core Bindings - 2 hours |
| 355 | +- **Phase 4**: Simple API - 1 hour |
| 356 | +- **Phase 5**: Full API - 2 hours |
| 357 | +- **Phase 6**: Callback API - 3 hours |
| 358 | +- **Phase 7**: Module Organization - 1 hour |
| 359 | +- **Phase 8**: Testing - 2 hours |
| 360 | +- **Phase 9**: Performance Analysis - 1 hour |
| 361 | +- **Phase 10**: Documentation - 1 hour |
| 362 | + |
| 363 | +**Total Estimated Time**: ~14 hours |
| 364 | + |
| 365 | +## Current Status |
| 366 | + |
| 367 | +**Phase 1 Complete**: Infrastructure setup done, baseline established with 87 tests passing. |
| 368 | + |
| 369 | +**Next Steps**: Begin Phase 2 - Build System Configuration |
0 commit comments