Conversation
Decompose PYBIND11_NOINLINE into the attribute part plus inline, and add PYBIND11_INLINE, which becomes empty under PYBIND11_PRECOMPILED. Groundwork for optional pre-compilation; all current expansions are unchanged and PYBIND11_INLINE is not used yet. Assisted-by: ClaudeCode:claude-fable-5
…h pytypes.h Split the out-of-line pytypes.h definitions into pytypes-inl.h (fmtlib/ CLI11 style): inline by default, compiled once into a per-project static library when PYBIND11_PRECOMPILED is defined. Infrastructure: - pybind11_precompile() creates the lazy pybind11::precompiled STATIC library from the installed or in-tree src/ sources; PRECOMPILE / NO_PRECOMPILE keywords on pybind11_add_module and a global PYBIND11_PRECOMPILE switch select it per target. - A link-time guard symbol encodes PYBIND11_INTERNALS_VERSION, Py_GIL_DISABLED, PYBIND11_SIMPLE_GIL_MANAGEMENT, and PYBIND11_DETAILED_ERROR_MESSAGES, so a configuration mismatch is one readable undefined symbol. - src/ is installed to share/pybind11/src (wheel stays pure); src/pybind11_combined.cpp is a single-TU build for non-CMake use. - Tests: PYBIND11_TEST_PRECOMPILE builds the whole suite against the library, two new test_cmake_build cases, packaging file lists, tidy preset, and a 3-platform CI job. Assisted-by: ClaudeCode:claude-fable-5
In precompiled mode the -inl.h definitions are intentionally non-inline, so misc-definitions-in-headers fires on every one. The header-only tidy build already analyzes all -inl.h bodies via the bottom-of-header includes. Assisted-by: ClaudeCode:claude-fable-5
…macros pybind11::pybind11 only carries headers; Py_GIL_DISABLED lives on Python::Module via pybind11::module. Without it the library is ABI-mismatched on free-threaded builds, and on Windows the pyconfig.h autolink pragma in its objects requests pythonXY.lib instead of pythonXYt.lib. Assisted-by: ClaudeCode:claude-fable-5
All 32 functions in detail/class.h are non-template plumbing (type/slot machinery); move them out of line for the precompiled mode. Forward declarations of these functions in other headers lose their inline keyword to stay ODR-consistent in both modes. Assisted-by: ClaudeCode:claude-fable-5
Several functions are forward-declared in headers that cannot include class.h; GCC's -Wredundant-decls (used in CI cxx_flags) flags the second declaration. Assisted-by: ClaudeCode:claude-fable-5
…_base-inl.h Moves the free functions (type-info lookup and registration, instance layout, isinstance_generic, cpp_conduit_method, type_info_description), the loader_life_support members (the function-local thread_local stack stays per-module in both modes), and the two heavy type_caster_generic members (the type_info constructor and the main cast overload). Templates, including load_impl<>, stay in the header. Assisted-by: ClaudeCode:claude-fable-5
…nt-decls gil.h evaluated PYBIND11_SIMPLE_GIL_MANAGEMENT before including common.h, which defines it on PyPy/GraalPy. Every existing TU included common.h first through pybind11.h, so this only surfaced when src/type_caster_base.cpp reached gil.h directly. Also suppress GCC -Wredundant-decls for the isinstance_generic declaration duplicated in pytypes.h. Assisted-by: ClaudeCode:claude-fable-5
…t of line The internals accessor family (get_internals, ensure_internals, the local-internals key and capsules, exception translators) moves into internals-inl.h; per-module identity is unchanged because the function-local statics move with their functions into whatever binary each module links. Also adds common-inl.h (pybind11_fail) and exception_translation-inl.h. Tiny hot accessors and all templates stay in the headers. Assisted-by: ClaudeCode:claude-fable-5
Assisted-by: ClaudeCode:claude-fable-5
The headline of the split: cpp_function's make_function_record, initialize_generic, destruct, and the 440-line dispatcher move out of line, along with generic_type::initialize, enum_base, the function signature generators, module cache helpers, keep_alive_impl, get_type_override, error_already_set::what, and detail::print. The templated initialize(), descr.h machinery, and the module/class_ API stay in the header. Assisted-by: ClaudeCode:claude-fable-5
These functions are already declared in function_record_pyobject.h, attr.h, and type_caster_base.h; GCC 13 -Wredundant-decls rejects the repeats. Assisted-by: ClaudeCode:claude-fable-5
Adds pybind11.get_source_dir() / python -m pybind11 --srcdir and a srcdir variable in pybind11.pc, so build systems such as Meson can compile src/pybind11_combined.cpp with PYBIND11_PRECOMPILED defined. Assisted-by: ClaudeCode:claude-fable-5
Assisted-by: ClaudeCode:claude-fable-5
|
Below is a complete transcript of a short codex gpt-5.6-sol medium session. @henryiii codex has five ideas for things to look into. After those are reviewed/resolved, merging the stack would seem fine to me. Codex transcript
User — 2026-09-13T14:23:37-07:00 (PDT)Codex — 2026-09-13T14:23:43-07:00 (PDT) · +5s after UserI’ll trace the stack from the local refs, map what moved into each Codex — 2026-09-13T14:23:58-07:00 (PDT) · +14s after Codex · +20s since UserThe branch is a 14-commit stack, not one monolithic patch. The central trick is already clear: header-only mode still includes every Codex — 2026-09-13T14:26:48-07:00 (PDT) · +2m 50s after Codex · +3m 10s since UserYour understanding is essentially right, with one important qualification: this creates declaration/definition pairs, not two independently maintained implementations. What the stack doesIn normal mode, nothing changes for consumers: With
Thus:
The merge stack is #6132–#6138: infrastructure and What actually movedThe split is selective:
Templates and deliberately hot functions remain in the original headers. So this is not an attempt to turn pybind11 into a conventional compiled library; it extracts the substantial non-template subset. Are the benefits real?Yes, though binary size is not an unconditional win. The stack reports:
The Boost.Histogram experiment is especially informative: precompilation removes a roughly fixed cost from each TU, but template-heavy application code can dominate enough that the one-time library compilation cancels the clean-build CPU saving. Incremental rebuilds still benefit. Boost.Histogram measurements, PyTorch measurements. These are promising author-run measurements, not yet a broad independent benchmark corpus. My view on the human-time tradeoffI would now favor the architecture, but not primarily because LLMs can edit both files. The strongest argument is aggregate human time: a modest maintainer burden can save repeated compiler time for thousands of downstream developers and CI jobs. The split is also ordinary C++ structure, and the canonical implementation still exists in exactly one place. LLMs substantially reduce:
They do not substantially reduce—and may increase—the expensive parts:
So I would reframe the cost as:
That cost is material, but the measurements are large enough to justify it—especially because the feature is opt-in. What I would want resolved before mergingA few things stand out on the current head:
My bottom line: conditional yes. LLMs move your old cost calculation in favor of accepting the source split, but the decisive point is that this concentrates implementation rather than duplicating it and produces credible downstream savings. I would approve the direction while treating configuration correctness and dual-mode CI—not the seven extra filenames—as the main review burden. User — 2026-09-13T14:43:42-07:00 (PDT) · +16m 53s after Codex · prior Codex turn: 3m 10sCodex — 2026-09-13T14:44:11-07:00 (PDT) · +29s after UserYes—this is a more substantial concern than the routine cost of maintaining two files. LLMs reduce it, but do not eliminate it. The effect depends strongly on the kind of refactoring. Local implementation changes are nearly unaffected. Renaming variables, restructuring a function body, extracting an internal helper, or changing an algorithm happens mostly inside the Interface-level refactoring becomes moderately harder:
Dependency-level refactoring is the serious case. The split introduces constraints that do not exist in a purely header-only implementation:
The stack itself demonstrates this. Splitting the code exposed a non-self-contained There is therefore an interesting counterpoint: the split can make some structural refactoring easier by imposing a clearer interface/implementation boundary. A 2,600-line What LLMs changeLLMs are very good at the graph-rewriting part:
But they do not remove the state-space expansion. After the split, every structural change still has to be correct under: An LLM can generate the patch rapidly; it cannot infer that all combinations are safe without suitable tests. In fact, cheap generation can make this worse psychologically: a large structural rewrite looks effortless even though its verification cost has not fallen proportionally. My updated assessment would be:
I would not reject the stack for this reason, provided the project keeps a simple, stable partitioning rule:
The last point matters. Seven fairly coarse implementation files are manageable. If this evolves into dozens of small So my concern level is moderate but not blocking. I would count it as a real continuing cost of the proposal—not merely an obsolete pre-LLM concern—but the current coarse split looks below the threshold where it should outweigh the measured benefits. |
This is the full stack, to show all changes at once. GH stacked PRs don't seem to have that option.
We'll actually merge #6138 as a stack (#6139 is the stack, but doesn't link).
📚 Documentation preview 📚: https://pybind11--6146.org.readthedocs.build/