Repository navigation
Conversation
|
If this is better in smaller parts, there are some good candidates for that, including two bug fixes and a nice performance speedup: 🤖 AI text below 🤖 Yes. Beyond the method-descriptor patch, there are two real bug fixes and one large feature that do not depend on Bug fixes (strongest candidates)
Feature, independent of the stable ABI
Method descriptor / instancemethod (the one you named)
Prep refactors, low value alone but they shrink the main diff
Not separable |
Py_LIMITED_API is not supported yet; a build with it defined failed with pages of unrelated errors. Add a single #error in common.h, and reserve a bit for Py_LIMITED_API in the precompiled configuration check so later stable-ABI work can switch it on. See pybind#6104 (B1). Assisted-by: ClaudeCode:claude-fable-5-1
…ULE_LOCAL_ID Modules built with Py_LIMITED_API will use a different internals layout, so they must not share internals with regular modules in one process. The tag is empty today and becomes "_stable" under Py_LIMITED_API. It is not part of PYBIND11_PLATFORM_ABI_ID, so cpp_conduit keeps bridging both worlds. See pybind#6104 (B2). Assisted-by: ClaudeCode:claude-fable-5-1
…p_bases/tp_mro Direct reads of tp_bases/tp_mro are not possible under the Python stable ABI. Route all reads through two small helpers so the limited-API branch can be added in one place later. See pybind#6104 (B3). Assisted-by: ClaudeCode:claude-fable-5-1
…tead of raw tp_name reads Direct reads of tp_name are not possible under the Python stable ABI. obj_class_name() now returns std::string. See pybind#6104 (B4). Assisted-by: ClaudeCode:claude-fable-5-1
Comparing tp_new against this module's pybind11_object_new fails for types whose pybind11_object base was created by another module (the inherited pointer belongs to that module's copy). The registry lookup, which was the PyPy path, is correct across modules and does not read type-object fields. See pybind#6104 (B5). Assisted-by: ClaudeCode:claude-fable-5-1
Read tb_next, tb_frame, f_back, co_filename and co_name through attribute access and use the public PyFrame_GetCode/PyFrame_GetLineNumber functions, instead of poking traceback, frame and code object struct fields, which are not part of the stable ABI. See pybind#6104 (B6). Assisted-by: ClaudeCode:claude-fable-5-1
_PyType_Lookup is a private CPython function and is not available under the stable ABI. Route the three users (metaclass getattro/setattro and the cpp_conduit instance-method check) through one helper. See pybind#6104 (B7). Assisted-by: ClaudeCode:claude-fable-5-1
PyThreadState is opaque under the stable ABI; the accessor function is public since Python 3.9. See pybind#6104 (B8). Assisted-by: ClaudeCode:claude-fable-5-1
… fallbacks Gate the PyPy fallbacks that exist because cpyext does not expose CPython struct layouts (static-property __set__, bool conversion, tuple/list fast iterators) on one semantic macro. The stable ABI needs the same fallbacks. See pybind#6104 (B9). Assisted-by: ClaudeCode:claude-fable-5-1
Use the function forms (available under the stable ABI) at the non-hot sites, and route the dispatcher and tuple/list caster sites through small detail:: wrappers that keep the macro forms when struct access is available. See pybind#6104 (B10). Assisted-by: ClaudeCode:claude-fable-5-1
The macro will select PyType_FromMetaclass-based type creation on CPython 3.12+. It is a no-op for now; this adds the switch, its precompiled config-check bit, a CMake option, and a CI matrix that builds with it on. See pybind#6104 (C1). Assisted-by: ClaudeCode:claude-fable-5-1
The metaclass and static-property implementations called the tp_* fields of PyType_Type and PyProperty_Type directly. Look the slots up once per module with PyType_GetSlot(), which works on static types since 3.10 and is part of the stable ABI; older versions and PyPy keep the direct reads. See pybind#6104 (C2). Assisted-by: ClaudeCode:claude-fable-5-1
…ON_VIA_SPEC Create the static-property type with PyType_FromMetaclass() when the opt-in macro is defined. The __dict__ getset is shared with the legacy path through dynamic_attr_getset(). See pybind#6104 (C3). Assisted-by: ClaudeCode:claude-fable-5-1
…N_VIA_SPEC See pybind#6104 (C4). Assisted-by: ClaudeCode:claude-fable-5-1
…IA_SPEC Weak-reference support moves from tp_weaklistoffset to a __weaklistoffset__ member, which is how PyType_Spec expresses it. See pybind#6104 (C5). Assisted-by: ClaudeCode:claude-fable-5-1
…_VIA_SPEC Bound classes are created with PyType_FromMetaclass(): bases are passed as a tuple, the docstring through Py_tp_doc (no manual tp_doc allocation), dynamic attributes through Py_TPFLAGS_MANAGED_DICT plus traverse/clear slots, the buffer protocol through Py_bf_* slots, and py::is_final by omitting Py_TPFLAGS_BASETYPE. __qualname__ is set after creation. The hand-filled path stays as make_new_python_type_legacy() and is still used for py::custom_type_setup, metaclasses with a custom tp_new, and a py::metaclass less derived than pybind11_type, which PyType_FromMetaclass refuses. See pybind#6104 (C6). Assisted-by: ClaudeCode:claude-fable-5-1
type_alloc()/type_free() use PyType_GetSlot() when direct slot access is unavailable; instance_dict_ptr() is the single place that reads the instance dictionary pointer. See pybind#6104 (C7). Assisted-by: ClaudeCode:claude-fable-5-1
Replace the Py_LIMITED_API #error with real gates and the missing pieces: - Require CPython 3.12+, reject PyPy/GraalPy and free-threaded builds, force PYBIND11_SIMPLE_GIL_MANAGEMENT and PYBIND11_TYPE_CREATION_VIA_SPEC, disable subinterpreter support; embed.h and chrono.h #error, and py::custom_type_setup is unavailable. - Heap-allocated Py_tss_t keys (the struct is opaque). - dynamic_attr types and pybind11_static_property append the __dict__ slot to the base layout (no Py_TPFLAGS_MANAGED_DICT); the offset is cached in type_info because _PyObject_GetDictPtr is unavailable. - A pybind11-provided instancemethod type (shared through the internals) replaces PyInstanceMethod_Type; bound methods go through types.MethodType. - Attribute-based fallbacks for tp_name, __bases__/__mro__, type lookup, frame inspection, docstrings on PyCFunctions, dict.setdefault, length hints, generators, complex numbers, and eval/exec (Py_CompileString). - The import-time version check accepts any interpreter from the Py_LIMITED_API version on. Modules built this way run unchanged on every CPython from the targeted version on. See docs/advanced/stable_abi.rst (next commits) for the feature matrix. See pybind#6104 (D1). Assisted-by: ClaudeCode:claude-fable-5-1
pybind11_add_module(... STABLE_ABI) builds the module against the Python stable ABI through python_add_library(USE_SABI) (CMake 3.26+, FindPython mode) and names it *.abi3.so / *.pyd. PYBIND11_STABLE_ABI makes it the default for a build tree and NO_STABLE_ABI opts a target out; PYBIND11_STABLE_ABI_VERSION (default 3.12) selects the targeted version. SHARED helper libraries get the same Py_LIMITED_API definition. The classic (FindPythonLibs) mode reports a clear error. pybind11_precompile(STABLE_ABI) compiles the precompiled library against the limited API; one build tree cannot mix stable-ABI and regular precompiled modules. The test suite gains PYBIND11_TEST_STABLE_ABI, which builds every test module as abi3 and skips the features that the stable ABI cannot offer. See pybind#6104 (D2). Assisted-by: ClaudeCode:claude-fable-5-1
py_limited_api=True (or a version such as "3.13") defines Py_LIMITED_API for the targeted version in addition to setuptools' abi3 file naming, so one wheel serves every CPython from that version on. See pybind#6104 (D3). Assisted-by: ClaudeCode:claude-fable-5-1
Build every test module as abi3 on Linux, macOS, and Windows (one job also with the precompiled library), and run the modules built against the 3.12 headers unchanged on 3.13 and 3.14. See pybind#6104 (D4). Assisted-by: ClaudeCode:claude-fable-5-1
How to enable it with CMake, setuptools, or by hand; the feature matrix; ABI isolation between stable-ABI and regular modules; and the implementation differences that affect performance. See pybind#6104 (D5). Assisted-by: ClaudeCode:claude-fable-5-1
When the consumer sets Python_ARTIFACTS_INTERACTIVE before add_subdirectory(pybind11), FindPython runs in pybind11's directory scope. Python::SABIModule was not promoted to global and Python_SOSABI was not cached, so STABLE_ABI modules either failed to configure or lost the .abi3 suffix. Add a test_cmake_build case that covers this. Assisted-by: ClaudeCode:claude-fable-5-1
Accept Py_LIMITED_API >= 0x030F0000 together with Py_GIL_DISABLED (PEP 803). PyObject and PyModuleDef are then incomplete types, so pybind11: - keeps `instance` and its other heap-type data as PEP 697 type data and reaches it through get_instance()/instance_object() instead of casts; - exports modules through the PEP 793 PyModExport_<name> hook; the PYBIND11_MODULE macro is unchanged, create_extension_module() is gone; - locks the internals with critical sections on a private object (PyMutex is not in the stable ABI) and keeps one weak reference per registered instance in place of PyUnstable_TryIncRef(); - tags the internals "_stable_ft", since abi3t modules also load on GIL-enabled interpreters. numpy.h is not available under abi3t: it mirrors NumPy's object layouts. CMake raises the stable ABI version to 3.15 on free-threaded Python and Pybind11Extension(py_limited_api=True) does the same. The test suite is built as .abi3t modules in CI for 3.15t and also run on GIL-enabled 3.15. Assisted-by: ClaudeCode:claude-fable-5-1
Under abi3t the size of PyObject_HEAD is not known at compile time, so the proxy structs drop it and the accessors go through the field getters NumPy 2.5 added for the same purpose (_PyArray_GET_ITEM_DATA and _PyDataType_GET_ITEM_DATA); void scalars are read through the buffer protocol. Older NumPy versions are rejected at run time. The numpy and Eigen tests run again under abi3t. Assisted-by: ClaudeCode:claude-fable-5-1
The datetime C API is not part of the limited API. Under Py_LIMITED_API, read the fields as attributes and build objects by calling the datetime types; regular builds keep the C macros. The chrono test runs under the stable ABI again. Assisted-by: ClaudeCode:claude-fable-5-1
Attribute reads on bound classes (and Python subclasses of them) went through pybind11_meta_getattro, which walks the MRO with attribute calls and raised KeyError for every miss: ~20x slower than `type`. pybind11's own instancemethod type already returns itself from `__get__` when accessed on a class, so the metaclass does not need to intercept reads. Drop the slot under Py_LIMITED_API and speed up the remaining type_lookup() users (setattro, conduit) with interned names and PyMapping_GetOptionalItem. Assisted-by: ClaudeCode:claude-fable-5-1
Avoids the varargs collection in PyObject_CallFunctionObjArgs on every `instance.method` access under the limited API (36 -> 32 ns here; the regular build's PyMethod_New path is 22 ns). Assisted-by: ClaudeCode:claude-fable-5-1
Collapse the ad hoc stable-ABI code paths into shared helpers: - type_dictoffset/type_basicsize with cached interned names replace seven __dictoffset__/__basicsize__ reads; attr.h uses the cached type_info::dictoffset - type_from_spec replaces five "PyType_FromMetaclass or fail" copies - type_data<T> replaces the two PyObject_GetTypeData wrappers - value_and_holder::type_is_exact replaces five alias checks - tp_name_is_one_of fetches the type name once per candidate list - PYBIND11_BASE_TYPE_SLOT defines whole functions (readable layout) - chrono.h: one accessor macro per branch, interned field names - getattr(obj, name, handle()) replaces hand-rolled lookups Cheaper paths: type_lookup returns a borrowed handle with _PyType_Lookup, the bool caster reads nb_bool via PyType_GetSlot under the stable ABI, and PYBIND11_CHECK_PYTHON_VERSION compares Py_Version instead of parsing Py_GetVersion(). Remove dead code: unused list_get_item, the unreachable try_incref stub under PYBIND11_OPAQUE_PYOBJECT, the inert force-define of PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET, a redundant PyGen_Check branch and a dead preprocessor condition. Assisted-by: ClaudeCode:claude-fable-5-1 ClaudeCode:claude-fable-5-1
A stable-ABI build targeting 3.12 with 3.13+ headers selected PyDict_GetItemStringRef, PyDict_SetDefaultRef, PyEval_GetFrameBuiltins and PyEval_GetFrameGlobals, which the headers hide below their limited API version. Add PYBIND11_API_VERSION_HEX (Py_LIMITED_API when defined, else PY_VERSION_HEX) and use it in those guards. annotations() always takes the 3.14 path under the stable ABI, as the module can run there. Assisted-by: ClaudeCode:claude-fable-5-1
With Py_TPFLAGS_METHOD_DESCRIPTOR and Py_TPFLAGS_HAVE_VECTORCALL (both in the 3.12 stable ABI), `obj.method(...)` calls the instancemethod with `obj` prepended instead of creating a types.MethodType first. `obj.method` without a call is unchanged. Release, CPython 3.12, `d.name()`: 77 ns regular, 45 ns abi3 (was 63). Assisted-by: ClaudeCode:claude-fable-5-1
`PyDateTimeAPI` is a static per translation unit. The non-limited helpers had external linkage, so the linker could pair one unit's `datetime_import()` with another unit's `is_datetime()`, which then read a null pointer. Give the helpers internal linkage and add a second chrono translation unit to the test module. Assisted-by: ClaudeCode:claude-fable-5-1
…spension Regular free-threaded builds lock the instance-map shards with PyMutex again, which is never suspended. Under abi3t only critical sections are available; `find_registered_python_instance()` now takes strong references under the shard lock and checks the types after releasing it, since `all_type_info()` can suspend the section while waiting for the internals lock. Assisted-by: ClaudeCode:claude-fable-5-1
…ited API CPython's `PyInstanceMethod_Type`, used in regular builds, does not set `Py_TPFLAGS_METHOD_DESCRIPTOR`. Assisted-by: ClaudeCode:claude-fable-5-1
`python_add_library(SHARED)` always links `Python::Python`. Create the target with `add_library()` instead so the helper only depends on the stable ABI. Assisted-by: ClaudeCode:claude-fable-5-1
Assisted-by: ClaudeCode:claude-fable-5-1
PyPy has `PyCFunction_GET_SELF` only as a macro and has no `PyThreadState_GetInterpreter()`. Assisted-by: ClaudeCode:claude-fable-5-1
Assisted-by: ClaudeCode:claude-fable-5-1
Add PYBIND11_ABI3T (CMake) and py_limited_api="3.15t" (setuptools) to target abi3t with Py_TARGET_ABI3T from GIL-enabled CPython 3.15+, with the .abi3t suffix and python3t.lib on Windows. Document Py_TARGET_ABI3T. Use `auto it` in the tuple/dict iterator tests under Py_LIMITED_API (as on PyPy), and build the cross-module RTTI bindings without the stable ABI so they share internals with the embedding executable. Assisted-by: ClaudeCode:claude-opus-5-5
…heck Assisted-by: ClaudeCode:claude-opus-5-5
…thon 3.12+ PYBIND11_TYPE_CREATION_VIA_SPEC is now a 0/1 value that defaults to 1 on CPython 3.12+ (and is forced on under Py_LIMITED_API). Define it as 0, or configure with -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF, to keep the legacy PyHeapTypeObject path. The CI job that used to opt in now opts out. The spec path now honors PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET (appending a `__dictoffset__` member instead of Py_TPFLAGS_MANAGED_DICT), and pybind11_traverse/pybind11_clear no longer call the managed-dict functions on 3.13+ when that macro is defined. Assisted-by: ClaudeCode:claude-fable-5-1 ClaudeCode:claude-fable-5-1
wheel.py-api only selects the wheel tag; the CMake side must enable STABLE_ABI from SKBUILD_SABI_COMPONENT and SKBUILD_SABI_VERSION. Assisted-by: ClaudeCode:claude-fable-5-1 ClaudeCode:claude-fable-5-1
PyDict_SetDefaultRef is only in the limited API from 3.15, so a Py_LIMITED_API=0x030E0000 build failed to compile. Alpha-suffixed gates also never matched a limited API target, so 3.13 targets did not use PyDict_GetItemStringRef. Add a 3.14-target CI build. Assisted-by: ClaudeCode:claude-fable-5-1 ClaudeCode:claude-fable-5-1
|
@henryiii this is the first pass reviewing with codex GPT-6.1-Sol ultra: Rechecked against 75be228. All six findings remain after the rebase:
Finding 2 was reproduced through compilation at this revision. The others are based on code inspection; no test suites were run. |
- Pin registry candidates before values_and_holders() takes the internals lock in get_object_handle(); it now returns an owning object. - Provide type_dictoffset()/type_basicsize() for non-limited builds so PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET compiles on 3.12+. - Fall back to the legacy type-creation path when a base's metaclass is not compatible with the requested one. - Detect Windows free-threaded SOABI (cp315t-win_amd64). - Read the python3t.lib directory from the Python::SABIModule target. - Include test_chrono_second_tu when PYBIND11_TEST_OVERRIDE selects test_chrono. Assisted-by: ClaudeCode:claude-fable-5-1 ClaudeCode:claude-fable-5-1
Only build one wheel for 3.12+ and 3.15t+ per platform and support all future versions of Python.
Implemented with Fable, added abi3t, got numpy working, got chrono working, iterated with linked Claude sessions on several downstream projects (finding both fixes and performance optimizations to keep this from being to costly), simplified with Fable, and reviewed with Astra, which found five issues. This was originally following #6104, with a commit for each "PR" in that roadmap, but there was quite a bit of iteration, so I opened this as one PR. I'm thinking it might be preferred to review this way these days anyway.
This supports abi3t on 3.15+ and abi3 on 3.12+. Original performance was pretty bad, but optimized a bit and some parts are faster than the unstable ABI (though those optimizations can of course be applied there too later). It depends very much on what you are doing (some things are slower with stable ABI, like attribute access with no call), but for most projects, this should be performant enough. awkward showed a 1-3% drop for one microbenchmark, otherwise unmeasurable. After fixes, iminuit shows no measurable slowdown (one microbenchmark is 17% faster due to an optimization to method-descriptor calls we could also apply to non-stable ABI builds).
Questions:
🤖 AI text below 🤖
Description
Adds support for the CPython stable ABI (
Py_LIMITED_API >= 0x030C0000, abi3) and the free-threaded stable ABI (abi3t, CPython 3.15+, PEP 803). One*.abi3.soper platform then runs on every CPython from the targeted version on.Draft: opened so that others can build their projects against it. Tried so far on boost-histogram, iminuit, and awkward. PyTorch can't use it, due to custom code calling C-API functions, but checked to make sure it still works with the mode turned off.
How to try it:
pybind11_add_module(example STABLE_ABI example.cpp)or-DPYBIND11_STABLE_ABI=ON(CMake 3.26+, FindPython mode).PYBIND11_STABLE_ABI_VERSIONselects the targeted version (default 3.12; 3.15 on free-threaded Python).Pybind11Extension(..., py_limited_api=True).Py_LIMITED_API=0x030C0000and name the module*.abi3.so.What changes:
PyType_SpecwithPyType_FromMetaclass(). This path is also available to regular builds on 3.12+ as thePYBIND11_TYPE_CREATION_VIA_SPECopt-in. (Edit: now opt-out)PyType_GetSlot(),PyObject_GetTypeData(), checked tuple/list accessors,__mro__/__dict__lookups).instancemethodtype, which is a vectorcall method descriptor, soobj.method(...)does not create a bound method.chrono.huses thedatetimetypes by attribute;numpy.hunder abi3t needs NumPy 2.5+.embed.h,subinterpreter.h,py::custom_type_setup, GIL-held assertions;py::metaclass()is restricted. Seedocs/advanced/stable_abi.rstfor the full table._stable/_stable_fttag);pybind11_conduit_v1bridges them.CI builds and runs the test suite for abi3 on 3.12 to 3.14 and abi3t on 3.15t, and runs the 3.12 abi3 binaries on 3.13 and 3.14 and the 3.15t abi3t binaries on GIL-enabled 3.15.
Suggested changelog entry:
Py_LIMITED_API >= 0x030C0000) and the free-threaded stable ABI (abi3t, CPython 3.15+). Enable withSTABLE_ABIinpybind11_add_module(),py_limited_api=TrueinPybind11Extension, or by definingPy_LIMITED_API. Type creation fromPyType_Specis also available to regular builds as thePYBIND11_TYPE_CREATION_VIA_SPECopt-in.PyType_FromMetaclass()by default on CPython 3.12+. DefinePYBIND11_TYPE_CREATION_VIA_SPEC=0(CMake-DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF) to restore the previous path.📚 Documentation preview 📚: https://pybind11--6198.org.readthedocs.build/