Skip to content

feat(build): support nanobind 3, widening the bound to <4 - #87

Merged
Ravenwater merged 1 commit into
mainfrom
fix/nanobind-3-port
Aug 27, 2026
Merged

Ravenwater merged 1 commit into
mainfrom
fix/nanobind-3-port

Conversation

@Ravenwater

Copy link
Copy Markdown
Contributor

Closes #85.

The <3 cap added in #84 — when nanobind 3.0.0 released and broke every wheel build — was a hold, not a fix. This is the port, and it keeps both majors working rather than trading one for the other.

nanobind result
2.15.0 1374 passed, 3 skipped
3.0.0 1374 passed, 3 skipped

Both are clean configure → full build → link → full suite. The link matters: when #85 was filed, the build had never got past the failing object, so link and runtime compatibility were explicitly unverified. They are now.

The change

nanobind 3 removed the free nb::detail::keep_alive(nurse, patient) and routes the operation through a backend slot that must be handed the extension's context pointer. That pointer is available to extension code — NB_CTX is only unavailable inside nanobind's own build, under NB_BUILD — so this is a respelling, not a redesign.

mtl5_ndarray.cpp now spells it once, in keep_view_alive(), selecting on NB_VERSION_MAJOR. That's why the bound moves to <4 instead of jumping to >=3.

The obvious simplification is wrong

Worth stating plainly, because the compiler suggests it and a reviewer will think it:

replace the three manual calls with the declarative nb::keep_alive<0, 1> annotation

That compiles, and it passes the suite as it existed. It is still wrong. The annotation applies to every return of a .def, and all three sites return a view on one path and a copy — or, for a fully-integer index, a scalar — on another:

method aliasing path other path
__getitem__ slice → view all-int index → scalar
reshape C-contiguous → view otherwise → copy
ravel C-contiguous → view otherwise → copy

Pinning a large source array to an independent copy of itself is a memory-retention bug. The pin has to stay per-path, so the manual call stays.

Two lifetime-test gaps, closed

Both would have survived a broken keep-alive, which is why the port needed them:

test_a_view_of_an_OWNING_array_keeps_it_alive — the existing lifetime test derives its views from asarray(), where the view also carries the NumPy owner object. The buffer survives on that reference alone, so the keep-alive is never load-bearing there. An array from copy() owns its memory with no second owner — that is the configuration where dropping the parent is a genuine use-after-free. Covers all three manual sites.

test_a_copy_does_not_pin_its_source — asserts the aliasing/copying split directly, via refcount:

view path:  getrefcount(src) 2 -> 3     (pinned)
copy path:  getrefcount(nc)  2 -> 2     (not pinned)

This is the test that fails if someone later folds the manual calls into the annotation.

Version alignment

python/CMakeLists.txt's FetchContent fallback moves v2.15.0 → v3.0.0 to stay in step with what the bound resolves. #84's lesson was that letting those two drift is exactly how a source change passes locally and fails every CI job.

The upper bound stays. nanobind 3.0.0 broke every build the day it released precisely because there wasn't one.

🤖 Generated with Claude Code

Closes #85. The <3 cap added in #84 when nanobind 3.0.0 broke every wheel
build was a hold, not a fix. This is the port, and it keeps BOTH majors
working rather than trading one for the other: 1374 passed, 3 skipped
under each of nanobind 2.15.0 and 3.0.0.

nanobind 3 removed the free nb::detail::keep_alive(nurse, patient) and
routes the operation through a backend slot needing the extension's
context pointer. That pointer IS available to extension code -- NB_CTX is
only unavailable inside nanobind's own build, under NB_BUILD -- so the
port is a respelling, not a redesign. mtl5_ndarray.cpp spells it once in
keep_view_alive(), selecting on NB_VERSION_MAJOR.

The obvious simplification is wrong, and now has a test saying so.
Replacing these calls with the declarative nb::keep_alive<0, 1>
annotation compiles and passes the old suite, but the annotation applies
to EVERY return of a .def, while __getitem__, reshape and ravel each
return a view on one path and a copy (or a scalar) on another. Pinning a
large source array to an independent copy of itself is a
memory-retention bug, so the pin stays per-path.

Two lifetime-test gaps closed, both of which a broken keep-alive would
have survived:

  - test_a_view_of_an_OWNING_array_keeps_it_alive. The existing lifetime
    test derives its views from asarray(), where the view also carries
    the NumPy owner object -- so the buffer survives on that reference
    alone and the keep-alive is never actually load-bearing. An array
    from copy() owns its memory with no second owner, which is where
    dropping the parent is a real use-after-free. Covers all three
    manual sites.
  - test_a_copy_does_not_pin_its_source. Asserts the aliasing/copying
    split directly via refcount: a view increfs its parent (2 -> 3), a
    copy does not (2 -> 2). This is the test that fails if someone
    folds the manual calls into the annotation.

python/CMakeLists.txt's FetchContent fallback moves v2.15.0 -> v3.0.0 to
stay in step with what the bound resolves; #84's lesson was that letting
those two drift is how a source change passes locally and fails every CI
job. The upper bound stays: nanobind 3.0.0 broke every build the day it
released precisely because there was none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 24 days. After that, they cost $0.25 per reviewed file.

Or wait 23 minutes for your next included review.

View limit details

Limit details: You’ve used the included review currently available. Your 74 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12767848-7d99-4b7e-b589-f5179db182b8

📥 Commits

Reviewing files that changed from the base of the PR and between 28f809c and 3e62bac.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • pyproject.toml
  • python/CMakeLists.txt
  • python/src/mtl5_ndarray.cpp
  • tests/test_ndarray.py

Comment @coderabbitai help to get the list of available commands.

@Ravenwater Ravenwater self-assigned this Aug 27, 2026
@Ravenwater
Ravenwater merged commit 92da1c8 into main Aug 27, 2026
14 checks passed
@Ravenwater
Ravenwater deleted the fix/nanobind-3-port branch August 27, 2026 23:44
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.

Port to nanobind 3.x (currently capped at <3)

1 participant