Skip to content

Port the extension from pybind11 to nanobind - #40

Merged
hmaarrfk merged 2 commits into
ramonaoptics:mainfrom
hmaarrfk:nanobind
Aug 9, 2026
Merged

hmaarrfk merged 2 commits into
ramonaoptics:mainfrom
hmaarrfk:nanobind

Conversation

@hmaarrfk

@hmaarrfk hmaarrfk commented Aug 9, 2026

Copy link
Copy Markdown
Member

Prepared by Mark Harfouche's Claude bot; Mark Harfouche (@hmaarrfk) reviews via the GitHub interface.

Ports ojph_bindings.cpp from pybind11 to nanobind, anticipating a 0.10.1 release. The full binding surface and semantics are preserved.

Measured effect: binding-layer call overhead drops ~2.5× (encode-style call 238 → 97 ns, decode-style 254 → 107 ns). End-to-end encode/decode is unchanged — the codec dominates by 3–4 orders of magnitude (rev12 4096²: 5.1 ms encode / 4.3 ms decode, level-2 reads 0.50 ms, all within noise of the pybind11 build). The practical win is staying on the actively developed binding framework; any future fine-grained (per-tile/per-block) APIs would benefit directly.

Key mapping decisions:

  • nb::ndarray replaces py::array; DLPack strides are in elements, so helpers convert to byte strides once at the boundary, and dtype codes replace buffer-format chars
  • Codestream.pull/exchange use rv_policy::reference, taking over the job of the py::nodelete holder (the codestream owns its line_bufs)
  • errors inside gil_scoped_release regions are carried out and raised after the GIL is reacquired
  • MemInfile.open keeps the backing array alive via keep_alive; MemOutfile.get_data remains a zero-copy memoryview
  • free-threading: nanobind declares Py_MOD_GIL_NOT_USED when compiled with NB_FREE_THREADED, which setup.py defines when building for a free-threaded interpreter (replaces py::mod_gil_not_used); the dedicated free-threading tests cover this in CI
  • nanobind's support library is compiled into the extension via a small shim (ojph/nanobind_support.cpp) since there is no setuptools integration; build requirements swap pybind11 for nanobind >= 2.2

Verified locally: 186 tests pass (3 skipped) on Linux/py3.12 against the statically linked fork.

https://claude.ai/code/session_015rdawfurPCmefzjoeFfuC4

<details><summary>Claude's draft</summary>

Rewrite ojph/ojph_bindings.cpp against nanobind, preserving the whole
binding surface and semantics. Notable mapping decisions:

- nb::ndarray replaces py::array/py::array_t; dlpack strides are in
  elements, so helpers convert to byte strides once at the boundary,
  and dtype codes replace buffer-format characters for the
  signed/unsigned dispatch
- Codestream.pull/exchange return nb::rv_policy::reference, which
  takes over the job of the py::nodelete holder on LineBuf (the
  codestream owns those buffers)
- errors inside nb::gil_scoped_release regions are carried out via an
  err pointer and raised after the GIL is reacquired, instead of
  throwing while unlocked
- MemInfile.open keeps the backing array alive via nb::keep_alive;
  MemOutfile.get_data still returns a zero-copy memoryview
- the base-class read/write bindings now take a 1-D uint8 array /
  bytes respectively (the old void* signatures were not callable from
  Python in any useful way)
- free-threaded CPython: nanobind declares Py_MOD_GIL_NOT_USED when
  compiled with NB_FREE_THREADED, which setup.py defines when
  sysconfig reports Py_GIL_DISABLED (replaces py::mod_gil_not_used)

Build changes: nanobind's support library is compiled into the
extension via ojph/nanobind_support.cpp (setuptools has no nanobind
CMake integration to lean on); include dirs gain nanobind's include/,
its vendored robin_map, and src/ for the amalgamated nb_combined.cpp;
NB_COMPACT_ASSERTIONS keeps the cheap internal checks; on Windows
NOMINMAX is required because the amalgamated build includes
<windows.h> (free-threading support in nb_internals.cpp) ahead of
translation units that use std::max. pyproject/CI build requirements
swap pybind11 for nanobind >= 2.2.

Measured: binding call overhead 238 -> 97 ns (encode-style call),
254 -> 107 ns (decode-style); end-to-end encode/decode unchanged
(codec-dominated). 186 tests pass, 3 skipped.

Resume this Claude session:
```
cd /home/mark/git/jp2k
claude --resume 487c1e20-36f3-416f-8be1-437c25ed67fe
```
</details>
<details><summary>Claude's draft</summary>

Stamp the nanobind port section as 0.10.1 (2026-08-08). After this PR
merges, tagging 0.10.1 on the merge commit publishes the release.

Resume this Claude session:
```
cd /home/mark/git/jp2k
claude --resume 487c1e20-36f3-416f-8be1-437c25ed67fe
```
</details>
@hmaarrfk
hmaarrfk merged commit c61e0db into ramonaoptics:main Aug 9, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant