diff --git a/pybind11/.clang-tidy b/pybind11/.clang-tidy index 3a1995c..a375f76 100644 --- a/pybind11/.clang-tidy +++ b/pybind11/.clang-tidy @@ -48,7 +48,10 @@ Checks: | readability-misplaced-array-index, readability-non-const-parameter, readability-qualified-auto, + readability-redundant-casting, readability-redundant-function-ptr-dereference, + readability-redundant-inline-specifier, + readability-redundant-member-init, readability-redundant-smartptr-get, readability-redundant-string-cstr, readability-simplify-subscript-expr, diff --git a/pybind11/.codespell-ignore-lines b/pybind11/.codespell-ignore-lines index e8cbf31..c03cd2f 100644 --- a/pybind11/.codespell-ignore-lines +++ b/pybind11/.codespell-ignore-lines @@ -2,6 +2,7 @@ template auto &this_ = static_cast(*this); if (load_impl(temp, false)) { + return load_impl(src, false); ssize_t nd = 0; auto trivial = broadcast(buffers, nd, shape); auto ndim = (size_t) nd; diff --git a/pybind11/.github/CONTRIBUTING.md b/pybind11/.github/CONTRIBUTING.md index fbdf075..a09b630 100644 --- a/pybind11/.github/CONTRIBUTING.md +++ b/pybind11/.github/CONTRIBUTING.md @@ -165,7 +165,7 @@ The valid options are: * Use `cmake --build build -j12` to build with 12 cores (for example). * Use `-G` and the name of a generator to use something different. `cmake --help` lists the generators available. - - On Unix, setting `CMAKE_GENERATER=Ninja` in your environment will give + - On Unix, setting `CMAKE_GENERATOR=Ninja` in your environment will give you automatic multithreading on all your CMake projects! * Open the `CMakeLists.txt` with QtCreator to generate for that IDE. * You can use `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to generate the `.json` file diff --git a/pybind11/.github/dependabot.yml b/pybind11/.github/dependabot.yml index 22c34bd..8b13673 100644 --- a/pybind11/.github/dependabot.yml +++ b/pybind11/.github/dependabot.yml @@ -4,12 +4,8 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "monthly" groups: actions: patterns: - "*" - ignore: - - dependency-name: actions/checkout - versions: - - "<5" diff --git a/pybind11/.github/workflows/ci.yml b/pybind11/.github/workflows/ci.yml index 0985b5c..2149685 100644 --- a/pybind11/.github/workflows/ci.yml +++ b/pybind11/.github/workflows/ci.yml @@ -84,11 +84,11 @@ jobs: python-version: '3.12' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON - runs-on: ubuntu-latest - python-version: '3.13t' + python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=20 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON - runs-on: ubuntu-latest python-version: '3.14' - cmake-args: -DCMAKE_CXX_STANDARD=14 -DCMAKE_CXX_FLAGS="-DPYBIND11_HAS_SUBINTERPRETER_SUPPORT=0" + cmake-args: -DCMAKE_CXX_STANDARD=14 - runs-on: ubuntu-latest python-version: 'pypy-3.10' cmake-args: -DCMAKE_CXX_STANDARD=14 @@ -96,22 +96,22 @@ jobs: python-version: 'graalpy-24.1' # No SciPy for macOS ARM - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: '3.8' cmake-args: -DCMAKE_CXX_STANDARD=14 - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: '3.11' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON + - runs-on: macos-15-intel + python-version: '3.13' + cmake-args: -DCMAKE_CXX_STANDARD=11 - runs-on: macos-latest python-version: '3.12' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON - - runs-on: macos-13 - python-version: '3.13t' - cmake-args: -DCMAKE_CXX_STANDARD=11 - runs-on: macos-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: macos-13 + - runs-on: macos-15-intel python-version: 'pypy-3.10' cmake-args: -DCMAKE_CXX_STANDARD=17 - runs-on: macos-latest @@ -138,9 +138,6 @@ jobs: - runs-on: windows-2022 python-version: '3.13' cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL - - runs-on: windows-latest - python-version: '3.13t' - cmake-args: -DCMAKE_CXX_STANDARD=17 - runs-on: windows-latest python-version: '3.14' cmake-args: -DCMAKE_CXX_STANDARD=20 @@ -179,18 +176,19 @@ jobs: name: "๐Ÿ ${{ matrix.python-version }} โ€ข ${{ matrix.runs-on }} โ€ข x64 inplace C++14" runs-on: ${{ matrix.runs-on }} + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: true @@ -202,7 +200,8 @@ jobs: if: runner.os != 'Windows' run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" - # More-or-less randomly adding a few extra flags here + # More-or-less randomly adding a few extra flags here. + # In particular, use this one to test the next ABI bump (internals version). - name: Configure run: > cmake -S. -B. @@ -212,6 +211,7 @@ jobs: -DDOWNLOAD_CATCH=ON -DDOWNLOAD_EIGEN=ON -DCMAKE_CXX_STANDARD=14 + -DPYBIND11_INTERNALS_VERSION=10000000 # Checks to makes sure defining `_` is allowed # Triggers EHsc missing error on Windows @@ -226,6 +226,7 @@ jobs: run: cmake --build . --target pytest - name: Compiled tests + timeout-minutes: 3 run: cmake --build . --target cpptest - name: Interface test @@ -236,13 +237,25 @@ jobs: manylinux: - name: Manylinux on ๐Ÿ 3.13t โ€ข GIL + name: Manylinux on ๐Ÿ ${{ matrix.python-version }} (${{ matrix.container }}) if: github.event.pull_request.draft == false + strategy: + fail-fast: false + matrix: + include: + - container: quay.io/pypa/manylinux_2_28_x86_64:latest + python-version: '3.13t' + - container: quay.io/pypa/musllinux_1_2_x86_64:latest + python-version: '3.13t' + - container: quay.io/pypa/manylinux_2_28_x86_64:latest + python-version: '3.14t' + - container: quay.io/pypa/musllinux_1_2_x86_64:latest + python-version: '3.14t' runs-on: ubuntu-latest timeout-minutes: 40 - container: quay.io/pypa/musllinux_1_2_x86_64:latest + container: ${{ matrix.container }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -253,7 +266,7 @@ jobs: run: uv tool install ninja - name: Configure via preset - run: cmake --preset venv -DPYBIND11_CREATE_WITH_UV=python3.13t + run: cmake --preset venv -DPYBIND11_CREATE_WITH_UV="${{ matrix.python-version }}" - name: Build C++11 run: cmake --build --preset venv @@ -277,9 +290,10 @@ jobs: name: "๐Ÿ ${{ matrix.python-version }}${{ matrix.python-debug && '-dbg' || '' }} (deadsnakes)${{ matrix.valgrind && ' โ€ข Valgrind' || '' }} โ€ข x64" runs-on: ubuntu-latest + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python-version }} (deadsnakes) uses: deadsnakes/action@v3.2.0 @@ -288,11 +302,11 @@ jobs: debug: ${{ matrix.python-debug }} - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Valgrind cache if: matrix.valgrind - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-valgrind with: path: valgrind @@ -330,6 +344,7 @@ jobs: run: cmake --build --preset default --target pytest - name: C++ tests + timeout-minutes: 3 run: cmake --build --preset default --target cpptest - name: Visibility test @@ -364,9 +379,10 @@ jobs: name: "๐Ÿ 3 โ€ข Clang ${{ matrix.clang }} โ€ข C++${{ matrix.std }} โ€ข x64${{ matrix.cxx_flags && ' โ€ข cxx_flags' || '' }}" container: "silkeh/clang:${{ matrix.clang }}${{ matrix.container_suffix }}" + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Add wget and python3 run: apt-get update && apt-get install -y python3-dev python3-numpy python3-pytest libeigen3-dev @@ -388,6 +404,7 @@ jobs: run: cmake --build build --target pytest - name: C++ tests + timeout-minutes: 3 run: cmake --build build --target cpptest - name: Interface test @@ -401,9 +418,10 @@ jobs: runs-on: ubuntu-latest name: "๐Ÿ 3.10 โ€ข CUDA 12.2 โ€ข Ubuntu 22.04" container: nvidia/cuda:12.2.0-devel-ubuntu22.04 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND - name: Install ๐Ÿ 3 @@ -427,7 +445,7 @@ jobs: # container: centos:8 # # steps: -# - uses: actions/checkout@v4 +# - uses: actions/checkout@v6 # # - name: Add Python 3 and a few requirements # run: yum update -y && yum install -y git python3-devel python3-numpy python3-pytest make environment-modules @@ -464,16 +482,22 @@ jobs: # Testing on Ubuntu + NVHPC (previous PGI) compilers, which seems to require more workarounds - ubuntu-nvhpc7: + ubuntu-nvhpc: if: github.event.pull_request.draft == false - runs-on: ubuntu-22.04 - name: "๐Ÿ 3 โ€ข NVHPC 23.5 โ€ข C++17 โ€ข x64" + runs-on: ubuntu-24.04 + name: "๐Ÿ 3 โ€ข NVHPC 25.11 โ€ข C++17 โ€ข x64" + timeout-minutes: 90 env: # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND DEBIAN_FRONTEND: 'noninteractive' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - name: Clean out unused stuff to save space + run: | + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL + sudo apt-get clean - name: Add NVHPC Repo run: | @@ -482,10 +506,11 @@ jobs: - name: Install ๐Ÿ 3 & NVHPC run: | - sudo apt-get update -y && \ - sudo apt-get install -y cmake environment-modules git python3-dev python3-pip python3-numpy && \ - sudo apt-get install -y --no-install-recommends nvhpc-23-5 && \ + sudo apt-get update -y + sudo apt-get install -y cmake environment-modules git python3-dev python3-pip python3-numpy + sudo apt-get install -y --no-install-recommends nvhpc-25-11 sudo rm -rf /var/lib/apt/lists/* + apt-cache depends nvhpc-25-11 python3 -m pip install --upgrade pip python3 -m pip install --upgrade pytest @@ -495,7 +520,7 @@ jobs: shell: bash run: | source /etc/profile.d/modules.sh - module load /opt/nvidia/hpc_sdk/modulefiles/nvhpc/23.5 + module load /opt/nvidia/hpc_sdk/modulefiles/nvhpc/25.11 cmake -S . -B build -DDOWNLOAD_CATCH=ON \ -DCMAKE_CXX_STANDARD=17 \ -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \ @@ -503,12 +528,13 @@ jobs: -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp" - name: Build - run: cmake --build build -j 2 --verbose + run: cmake --build build -j $(nproc) --verbose - name: Python tests run: cmake --build build --target pytest - name: C++ tests + timeout-minutes: 3 run: cmake --build build --target cpptest - name: Interface test @@ -532,9 +558,10 @@ jobs: name: "๐Ÿ 3 โ€ข GCC ${{ matrix.gcc }} โ€ข C++${{ matrix.std }} โ€ข x64${{ matrix.cxx_flags && ' โ€ข cxx_flags' || '' }}" container: "gcc:${{ matrix.gcc }}" + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Add Python 3 run: apt-get update; apt-get install -y python3-dev python3-numpy python3-pytest python3-pip libeigen3-dev @@ -543,7 +570,7 @@ jobs: run: python3 -m pip install --upgrade pip - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Configure shell: bash @@ -562,6 +589,7 @@ jobs: run: cmake --build build --target pytest - name: C++ tests + timeout-minutes: 3 run: cmake --build build --target cpptest - name: Interface test @@ -593,11 +621,12 @@ jobs: icc: if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 + timeout-minutes: 90 name: "๐Ÿ 3 โ€ข ICC latest โ€ข x64" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Add apt repo run: | @@ -643,6 +672,7 @@ jobs: cmake --build build-11 --target check - name: C++ tests C++11 + timeout-minutes: 3 run: | set +e; source /opt/intel/oneapi/setvars.sh; set -e cmake --build build-11 --target cpptest @@ -680,6 +710,7 @@ jobs: cmake --build build-17 --target check - name: C++ tests C++17 + timeout-minutes: 3 run: | set +e; source /opt/intel/oneapi/setvars.sh; set -e cmake --build build-17 --target cpptest @@ -707,10 +738,11 @@ jobs: name: "๐Ÿ 3 โ€ข ${{ matrix.container }} โ€ข x64" container: "${{ matrix.container }}" + timeout-minutes: 90 steps: - name: Latest actions/checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Add Python 3.8 if: matrix.container == 'almalinux:8' @@ -750,6 +782,7 @@ jobs: run: cmake --build build --target pytest - name: C++ tests + timeout-minutes: 3 run: cmake --build build --target cpptest - name: Interface test @@ -765,9 +798,11 @@ jobs: name: "๐Ÿ 3.9 โ€ข Debian โ€ข x86 โ€ข Install" runs-on: ubuntu-latest container: i386/debian:bullseye + timeout-minutes: 90 steps: - - uses: actions/checkout@v1 # v1 is required to run inside docker + # v1 required for i386/debian container; pinned to SHA to prevent dependabot updates + - uses: actions/checkout@544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9 # v1 - name: Install requirements run: | @@ -809,11 +844,12 @@ jobs: if: github.event.pull_request.draft == false name: "Documentation build test" runs-on: ubuntu-latest + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -855,12 +891,13 @@ jobs: name: "๐Ÿ ${{ matrix.python }} โ€ข MSVC 2022 โ€ข x86 ${{ matrix.args }}" runs-on: windows-2022 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x86 @@ -869,7 +906,7 @@ jobs: ${{ matrix.python == '3.13' && runner.os == 'Windows' }} - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Prepare MSVC uses: ilammy/msvc-dev-cmd@v1.13.0 @@ -907,18 +944,19 @@ jobs: name: "๐Ÿ ${{ matrix.python }} โ€ข MSVC 2022 (Debug) โ€ข x86 ${{ matrix.args }}" runs-on: windows-2022 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: x86 - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Prepare MSVC uses: ilammy/msvc-dev-cmd@v1.13.0 @@ -955,12 +993,13 @@ jobs: name: "๐Ÿ ${{ matrix.python }} โ€ข MSVC 2022 C++20 โ€ข x64" runs-on: windows-2022 + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -968,7 +1007,7 @@ jobs: run: python3 -m pip install -r tests/requirements.txt - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Configure C++20 run: > @@ -985,6 +1024,7 @@ jobs: run: cmake --build build --target pytest - name: C++20 tests + timeout-minutes: 3 run: cmake --build build --target cpptest -j 2 - name: Interface test C++20 @@ -1012,6 +1052,7 @@ jobs: if: github.event.pull_request.draft == false name: "๐Ÿ 3 โ€ข windows-latest โ€ข ${{ matrix.sys }}" runs-on: windows-latest + timeout-minutes: 90 defaults: run: shell: msys2 {0} @@ -1019,8 +1060,15 @@ jobs: fail-fast: false matrix: include: - - { sys: mingw64, env: x86_64 } - - { sys: mingw32, env: i686 } + - sys: mingw32 + env: i686 + extra_install: "" + - sys: mingw64 + env: x86_64 + extra_install: | + mingw-w64-x86_64-python-numpy + mingw-w64-x86_64-python-scipy + mingw-w64-x86_64-eigen3 steps: - uses: msys2/setup-msys2@v2 with: @@ -1034,17 +1082,9 @@ jobs: mingw-w64-${{matrix.env}}-python-pytest mingw-w64-${{matrix.env}}-boost mingw-w64-${{matrix.env}}-catch + ${{ matrix.extra_install }} - - uses: msys2/setup-msys2@v2 - if: matrix.sys == 'mingw64' - with: - msystem: ${{matrix.sys}} - install: >- - mingw-w64-${{matrix.env}}-python-numpy - mingw-w64-${{matrix.env}}-python-scipy - mingw-w64-${{matrix.env}}-eigen3 - - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Configure C++11 # LTO leads to many undefined reference like @@ -1061,6 +1101,7 @@ jobs: run: cmake --build build --target pytest -j 2 - name: C++11 tests + timeout-minutes: 3 run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build --target cpptest -j 2 - name: Interface test C++11 @@ -1085,6 +1126,7 @@ jobs: run: cmake --build build2 --target pytest -j 2 - name: C++14 tests + timeout-minutes: 3 run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build2 --target cpptest -j 2 - name: Interface test C++14 @@ -1109,6 +1151,7 @@ jobs: run: cmake --build build3 --target pytest -j 2 - name: C++17 tests + timeout-minutes: 3 run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target cpptest -j 2 - name: Interface test C++17 @@ -1126,6 +1169,7 @@ jobs: python: ['3.10'] runs-on: "${{ matrix.os }}" + timeout-minutes: 90 name: "๐Ÿ ${{ matrix.python }} โ€ข ${{ matrix.os }} โ€ข clang-latest" @@ -1134,18 +1178,18 @@ jobs: run: env - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Clang - uses: egor-tensin/setup-clang@v1 + uses: egor-tensin/setup-clang@v2 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Install ninja-build tool uses: seanmiddleditch/gha-setup-ninja@v6 @@ -1179,6 +1223,7 @@ jobs: run: cmake --build . --target pytest -j 2 - name: C++ tests + timeout-minutes: 3 run: cmake --build . --target cpptest -j 2 - name: Interface test @@ -1190,57 +1235,49 @@ jobs: - name: Clean directory run: git clean -fdx - macos_brew_install_llvm: + # Clang with MSVC/Windows SDK toolchain + python.org CPython (Windows ARM) + windows_arm_clang_msvc: if: github.event.pull_request.draft == false - name: "macos-13 โ€ข brew install llvm" - runs-on: macos-13 - env: - # https://apple.stackexchange.com/questions/227026/how-to-install-recent-clang-with-homebrew - LDFLAGS: '-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib' + strategy: + fail-fast: false + matrix: + os: [windows-11-arm] + python: ['3.13'] - steps: - - name: Update PATH - run: echo "/usr/local/opt/llvm/bin" >> $GITHUB_PATH + runs-on: "${{ matrix.os }}" + timeout-minutes: 90 + + name: "๐Ÿ ${{ matrix.python }} โ€ข ${{ matrix.os }} โ€ข clang-msvc" + steps: - name: Show env run: env - name: Checkout - uses: actions/checkout@v4 - - - name: Show Clang++ version before brew install llvm - run: clang++ --version - - - name: brew install llvm - run: brew install llvm + uses: actions/checkout@v6 - - name: Show Clang++ version after brew install llvm - run: clang++ --version - - - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + - name: Setup Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + architecture: arm64 - name: Run pip installs run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r tests/requirements.txt - python3 -m pip install numpy - python3 -m pip install scipy - - - name: Show CMake version - run: cmake --version + python -m pip install --upgrade pip + python -m pip install -r tests/requirements.txt - - name: CMake Configure + - name: Configure CMake run: > - cmake -S . -B . - -DPYBIND11_WERROR=ON + cmake -G Ninja -S . -B . + -DPYBIND11_WERROR=OFF -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF -DDOWNLOAD_CATCH=ON -DDOWNLOAD_EIGEN=ON -DCMAKE_CXX_COMPILER=clang++ - -DCMAKE_CXX_STANDARD=17 - -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") + -DCMAKE_CXX_STANDARD=20 + -DPython_ROOT_DIR="$env:Python_ROOT_DIR" - name: Build run: cmake --build . -j 2 @@ -1249,6 +1286,7 @@ jobs: run: cmake --build . --target pytest -j 2 - name: C++ tests + timeout-minutes: 3 run: cmake --build . --target cpptest -j 2 - name: Interface test @@ -1257,23 +1295,75 @@ jobs: - name: Visibility test run: cmake --build . --target test_cross_module_rtti -j 2 - - name: CMake Configure - Exercise cmake -DPYBIND11_TEST_OVERRIDE - run: > - cmake -S . -B build_partial - -DPYBIND11_WERROR=ON - -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF - -DDOWNLOAD_CATCH=ON - -DDOWNLOAD_EIGEN=ON - -DCMAKE_CXX_COMPILER=clang++ - -DCMAKE_CXX_STANDARD=17 - -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") - "-DPYBIND11_TEST_OVERRIDE=test_call_policies.cpp;test_gil_scoped.cpp;test_thread.cpp" + # Clang in MSYS2/MinGW-w64 CLANGARM64 toolchain + MSYS2 Python (Windows ARM) + windows_arm_clang_msys2: + if: github.event.pull_request.draft == false + + strategy: + fail-fast: false + matrix: + os: [windows-11-arm] - - name: Build - Exercise cmake -DPYBIND11_TEST_OVERRIDE - run: cmake --build build_partial -j 2 + runs-on: "${{ matrix.os }}" + timeout-minutes: 90 - - name: Python tests - Exercise cmake -DPYBIND11_TEST_OVERRIDE - run: cmake --build build_partial --target pytest -j 2 + name: "${{ matrix.os }} โ€ข clang-msys2" - - name: Clean directory - run: git clean -fdx + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - uses: msys2/setup-msys2@v2 + with: + msystem: CLANGARM64 + update: true + install: | + mingw-w64-clang-aarch64-cmake + mingw-w64-clang-aarch64-clang + mingw-w64-clang-aarch64-ninja + mingw-w64-clang-aarch64-python-pip + mingw-w64-clang-aarch64-python-pytest + mingw-w64-clang-aarch64-python-numpy + + - name: Debug info + run: | + clang++ --version + cmake --version + ninja --version + python --version + + - name: Run pip installs + run: | + python -m pip install --upgrade pip + python -m pip install -r tests/requirements.txt + + - name: Configure CMake + run: >- + cmake -S . -B build + -DPYBIND11_WERROR=OFF + -DDOWNLOAD_CATCH=ON + -DDOWNLOAD_EIGEN=ON + -DCMAKE_CXX_COMPILER=clang++ + -DCMAKE_CXX_STANDARD=20 + -DPYTHON_EXECUTABLE=$(python -c "import sys; print(sys.executable)") + + - name: Build + run: cmake --build build -j 2 + + - name: Python tests + run: cmake --build build --target pytest -j 2 + + - name: C++ tests + timeout-minutes: 3 + run: PYTHONHOME=/clangarm64 PYTHONPATH=/clangarm64 cmake --build build --target cpptest -j 2 + + - name: Interface test + run: cmake --build build --target test_cmake_build -j 2 + + - name: Visibility test + run: cmake --build build --target test_cross_module_rtti -j 2 diff --git a/pybind11/.github/workflows/configure.yml b/pybind11/.github/workflows/configure.yml index 6a3b365..e849421 100644 --- a/pybind11/.github/workflows/configure.yml +++ b/pybind11/.github/workflows/configure.yml @@ -35,28 +35,28 @@ jobs: - runs-on: ubuntu-24.04 cmake: "3.29" - - runs-on: macos-13 + - runs-on: macos-15-intel cmake: "3.15" - runs-on: macos-14 - cmake: "4.0" + cmake: "4.2" - runs-on: windows-latest - cmake: "4.0" + cmake: "4.2" name: ๐Ÿ 3.11 โ€ข CMake ${{ matrix.cmake }} โ€ข ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.0.0 - name: Prepare env run: uv pip install --python=python --system -r tests/requirements.txt @@ -64,7 +64,7 @@ jobs: # An action for adding a specific version of CMake: # https://github.com/jwlawson/actions-setup-cmake - name: Setup CMake ${{ matrix.cmake }} - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 with: cmake-version: ${{ matrix.cmake }} diff --git a/pybind11/.github/workflows/docs-link.yml b/pybind11/.github/workflows/docs-link.yml index d1f1a17..ea25410 100644 --- a/pybind11/.github/workflows/docs-link.yml +++ b/pybind11/.github/workflows/docs-link.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest if: github.event.repository.fork == false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check for docs changes id: docs_changes diff --git a/pybind11/.github/workflows/format.yml b/pybind11/.github/workflows/format.yml index 9258e27..6bf7732 100644 --- a/pybind11/.github/workflows/format.yml +++ b/pybind11/.github/workflows/format.yml @@ -25,8 +25,8 @@ jobs: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.x" - name: Add matchers @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest container: silkeh/clang:20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install requirements run: apt-get update && apt-get install -y git python3-dev python3-pytest ninja-build diff --git a/pybind11/.github/workflows/labeler.yml b/pybind11/.github/workflows/labeler.yml index 2152abb..f5b618b 100644 --- a/pybind11/.github/workflows/labeler.yml +++ b/pybind11/.github/workflows/labeler.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 if: > github.event.pull_request.merged == true && !startsWith(github.event.pull_request.title, 'chore(deps):') && diff --git a/pybind11/.github/workflows/nightlies.yml b/pybind11/.github/workflows/nightlies.yml index 788f0cd..d7ab9b0 100644 --- a/pybind11/.github/workflows/nightlies.yml +++ b/pybind11/.github/workflows/nightlies.yml @@ -20,12 +20,12 @@ jobs: if: github.repository_owner == 'pybind' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.0.0 - name: Build SDist and wheels run: | @@ -33,7 +33,7 @@ jobs: nox -s build nox -s build_global - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: Packages path: dist/* @@ -44,7 +44,7 @@ jobs: needs: [build_wheel] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v8 with: name: Packages path: dist @@ -53,7 +53,7 @@ jobs: run: ls -lha dist/*.whl - name: Upload wheel to Anaconda Cloud as nightly - uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 + uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/pybind11/.github/workflows/pip.yml b/pybind11/.github/workflows/pip.yml index 9b6a9dc..c52857c 100644 --- a/pybind11/.github/workflows/pip.yml +++ b/pybind11/.github/workflows/pip.yml @@ -23,15 +23,15 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup ๐Ÿ 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.8 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.0.0 - name: Prepare env run: uv pip install --system -r tests/requirements.txt @@ -47,15 +47,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup ๐Ÿ 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.8 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.0.0 - name: Prepare env run: uv pip install --system -r tests/requirements.txt twine nox @@ -72,13 +72,13 @@ jobs: run: twine check dist/* - name: Save standard package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: standard path: dist/pybind11-* - name: Save global package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: global path: dist/*global-* @@ -100,10 +100,10 @@ jobs: steps: # Downloads all to directories matching the artifact names - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v8 - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v4 with: subject-path: "*/pybind11*" diff --git a/pybind11/.github/workflows/reusable-standard.yml b/pybind11/.github/workflows/reusable-standard.yml index 5e258e7..e53c275 100644 --- a/pybind11/.github/workflows/reusable-standard.yml +++ b/pybind11/.github/workflows/reusable-standard.yml @@ -28,12 +28,13 @@ jobs: standard: name: ๐Ÿงช runs-on: ${{ inputs.runs-on }} + timeout-minutes: 90 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ inputs.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ inputs.python-version }} allow-prereleases: true @@ -43,14 +44,14 @@ jobs: - name: Setup Boost (Linux) if: runner.os == 'Linux' - run: sudo apt-get install libboost-dev + run: sudo apt-get update && sudo apt-get install -y libboost-dev - name: Setup Boost (macOS) if: runner.os == 'macOS' run: brew install boost - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: true @@ -82,6 +83,7 @@ jobs: run: cmake --build build --target pytest - name: C++ tests + timeout-minutes: 3 run: cmake --build build --target cpptest - name: Interface test diff --git a/pybind11/.github/workflows/tests-cibw.yml b/pybind11/.github/workflows/tests-cibw.yml index 0abddd0..f7bca76 100644 --- a/pybind11/.github/workflows/tests-cibw.yml +++ b/pybind11/.github/workflows/tests-cibw.yml @@ -17,12 +17,12 @@ jobs: name: Pyodide wheel runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.4 env: PYODIDE_BUILD_EXPORTS: whole_archive with: @@ -35,16 +35,17 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [macos-14, macos-13] + runs-on: [macos-14, macos-15-intel] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 - - run: brew upgrade cmake + # We have to uninstall first because GH is now using a local tap to build cmake<4, iOS needs cmake>=4 + - run: brew uninstall cmake && brew install cmake - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.4 env: CIBW_PLATFORM: ios CIBW_SKIP: cp314-* # https://github.com/pypa/cibuildwheel/issues/2494 @@ -57,9 +58,9 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [macos-latest, macos-13, ubuntu-latest] + runs-on: [macos-latest, macos-15-intel, ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 @@ -69,17 +70,7 @@ jobs: if: contains(matrix.runs-on, 'macos') run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ - - name: Enable KVM for Android emulator - if: contains(matrix.runs-on, 'ubuntu') - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - run: pipx install patchelf - - - uses: pypa/cibuildwheel@v3.1 + - uses: pypa/cibuildwheel@v3.4 env: CIBW_PLATFORM: android with: diff --git a/pybind11/.github/workflows/upstream.yml b/pybind11/.github/workflows/upstream.yml index 3892600..51354d6 100644 --- a/pybind11/.github/workflows/upstream.yml +++ b/pybind11/.github/workflows/upstream.yml @@ -24,10 +24,10 @@ jobs: if: "contains(github.event.pull_request.labels.*.name, 'python dev')" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" allow-prereleases: true @@ -36,7 +36,7 @@ jobs: run: sudo apt-get install libboost-dev - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Run pip installs run: | @@ -66,6 +66,7 @@ jobs: run: cmake --build build11 --target pytest -j 2 - name: C++11 tests + timeout-minutes: 3 run: cmake --build build11 --target cpptest -j 2 - name: Interface test C++11 @@ -87,6 +88,7 @@ jobs: run: cmake --build build17 --target pytest - name: C++17 tests + timeout-minutes: 3 run: cmake --build build17 --target cpptest # Third build - C++17 mode with unstable ABI diff --git a/pybind11/.gitignore b/pybind11/.gitignore index 5875be3..b007bb1 100644 --- a/pybind11/.gitignore +++ b/pybind11/.gitignore @@ -26,6 +26,7 @@ MANIFEST /.ninja_* /*.ninja /docs/.build +__pycache__/ *.py[co] *.egg-info *~ diff --git a/pybind11/.pre-commit-config.yaml b/pybind11/.pre-commit-config.yaml index 8435fba..637dc3f 100644 --- a/pybind11/.pre-commit-config.yaml +++ b/pybind11/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: # Clang format the codebase automatically - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v20.1.8" + rev: "v22.1.2" hooks: - id: clang-format types_or: [c++, c, cuda] # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.15.9 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] @@ -40,7 +40,7 @@ repos: # Check static types with mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.17.1" + rev: "v1.20.0" hooks: - id: mypy args: [] @@ -62,7 +62,7 @@ repos: # Standard hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v5.0.0" + rev: "v6.0.0" hooks: - id: check-added-large-files - id: check-case-conflict @@ -80,7 +80,7 @@ repos: # Also code format the docs - repo: https://github.com/adamchainz/blacken-docs - rev: "1.19.1" + rev: "1.20.0" hooks: - id: blacken-docs additional_dependencies: @@ -88,7 +88,7 @@ repos: # Changes tabs to spaces - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: "v1.5.5" + rev: "v1.5.6" hooks: - id: remove-tabs exclude: (^docs/.*|\.patch)?$ @@ -112,15 +112,24 @@ repos: # Use tools/codespell_ignore_lines_from_errors.py # to rebuild .codespell-ignore-lines - repo: https://github.com/codespell-project/codespell - rev: "v2.4.1" + rev: "v2.4.2" hooks: - id: codespell - exclude: ".supp$" + exclude: "(.supp|^pyproject.toml)$" args: ["-x.codespell-ignore-lines", "-Lccompiler,intstruct"] +# Also check spelling +# Use mirror because pre-commit autoupdate confuses tags in the upstream repo. +# See https://github.com/crate-ci/typos/issues/390 +- repo: https://github.com/adhtruong/mirrors-typos + rev: "v1.45.0" + hooks: + - id: typos + args: [] + # Check for common shell mistakes - repo: https://github.com/shellcheck-py/shellcheck-py - rev: "v0.10.0.1" + rev: "v0.11.0.1" hooks: - id: shellcheck @@ -135,14 +144,14 @@ repos: # PyLint has native support - not always usable, but works for us - repo: https://github.com/PyCQA/pylint - rev: "v3.3.7" + rev: "v4.0.5" hooks: - id: pylint files: ^pybind11 # Check schemas on some of our YAML files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.37.1 hooks: - id: check-readthedocs - id: check-github-workflows diff --git a/pybind11/CMakeLists.txt b/pybind11/CMakeLists.txt index ba5f665..097b4eb 100644 --- a/pybind11/CMakeLists.txt +++ b/pybind11/CMakeLists.txt @@ -10,7 +10,7 @@ if(NOT CMAKE_VERSION VERSION_LESS "3.27") cmake_policy(GET CMP0148 _pybind11_cmp0148) endif() -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) if(_pybind11_cmp0148) cmake_policy(SET CMP0148 ${_pybind11_cmp0148}) @@ -180,6 +180,7 @@ if(PYBIND11_MASTER_PROJECT) endif() set(PYBIND11_HEADERS + include/pybind11/detail/argument_vector.h include/pybind11/detail/class.h include/pybind11/detail/common.h include/pybind11/detail/cpp_conduit.h @@ -187,6 +188,7 @@ set(PYBIND11_HEADERS include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h include/pybind11/detail/exception_translation.h include/pybind11/detail/function_record_pyobject.h + include/pybind11/detail/holder_caster_foreign_helpers.h include/pybind11/detail/init.h include/pybind11/detail/internals.h include/pybind11/detail/native_enum_data.h diff --git a/pybind11/README.rst b/pybind11/README.rst index f30fd61..26d618f 100644 --- a/pybind11/README.rst +++ b/pybind11/README.rst @@ -120,24 +120,23 @@ goodies: - With little extra effort, C++ types can be pickled and unpickled similar to regular Python objects. -Supported compilers -------------------- - -1. Clang/LLVM 3.3 or newer (for Apple Xcode's clang, this is 5.0.0 or - newer) -2. GCC 4.8 or newer -3. Microsoft Visual Studio 2022 or newer (2019 probably works, but was dropped in CI) -4. Intel classic C++ compiler 18 or newer (ICC 20.2 tested in CI) -5. Cygwin/GCC (previously tested on 2.5.1) -6. NVCC (CUDA 11.0 tested in CI) -7. NVIDIA PGI (20.9 tested in CI) - -Supported Platforms -------------------- - -* Windows, Linux, macOS, and iOS -* CPython 3.8+, Pyodide, PyPy, and GraalPy -* C++11, C++14, C++17, C++20, and C++23 +Supported platforms & compilers +------------------------------- + +pybind11 is exercised in continuous integration across a range of operating +systems, Python versions, C++ standards, and toolchains. For an up-to-date +view of the combinations we currently test, please see the +`pybind11 GitHub Actions `_ +logs. + +The test matrix naturally evolves over time as older platforms and compilers +fall out of use and new ones are added by the community. Closely related +versions of a tested compiler or platform will often work as well in practice, +but we cannot promise to validate every possible combination. If a +configuration you rely on is missing from the matrix or regresses, issues and +pull requests to extend coverage are very welcome. At the same time, we need +to balance the size of the test matrix with the available CI resources, +such as GitHub's limits on concurrent jobs under the free tier. About ----- diff --git a/pybind11/docs/advanced/classes.rst b/pybind11/docs/advanced/classes.rst index 14bfc0b..bfbaea6 100644 --- a/pybind11/docs/advanced/classes.rst +++ b/pybind11/docs/advanced/classes.rst @@ -972,9 +972,14 @@ Module-local class bindings =========================== When creating a binding for a class, pybind11 by default makes that binding -"global" across modules. What this means is that a type defined in one module -can be returned from any module resulting in the same Python type. For -example, this allows the following: +"global" across modules. What this means is that instances whose type is +defined with a ``py::class_`` statement in one module can be passed to or +returned from a function defined in any other module that is "ABI compatible" +with the first, i.e., that was built with sufficiently similar versions of +pybind11 and of the C++ compiler and C++ standard library. The internal data +structures that pybind11 uses to keep track of its types and instances are +shared just as they would be if everything were in the same module. +For example, this allows the following: .. code-block:: cpp @@ -1376,11 +1381,22 @@ You can do that using ``py::custom_type_setup``: .. code-block:: cpp - struct OwnsPythonObjects { - py::object value = py::none(); + struct ContainerOwnsPythonObjects { + std::vector list; + + void append(const py::object &obj) { list.emplace_back(obj); } + py::object at(py::ssize_t index) const { + if (index >= size() || index < 0) { + throw py::index_error("Index out of range"); + } + return list.at(py::size_t(index)); + } + py::ssize_t size() const { return py::ssize_t_cast(list.size()); } + void clear() { list.clear(); } }; - py::class_ cls( - m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) { + + py::class_ cls( + m, "ContainerOwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) { auto *type = &heap_type->ht_type; type->tp_flags |= Py_TPFLAGS_HAVE_GC; type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) { @@ -1389,20 +1405,28 @@ You can do that using ``py::custom_type_setup``: Py_VISIT(Py_TYPE(self_base)); #endif if (py::detail::is_holder_constructed(self_base)) { - auto &self = py::cast(py::handle(self_base)); - Py_VISIT(self.value.ptr()); + auto &self = py::cast(py::handle(self_base)); + for (auto &item : self.list) { + Py_VISIT(item.ptr()); + } } return 0; }; type->tp_clear = [](PyObject *self_base) { if (py::detail::is_holder_constructed(self_base)) { - auto &self = py::cast(py::handle(self_base)); - self.value = py::none(); + auto &self = py::cast(py::handle(self_base)); + for (auto &item : self.list) { + Py_CLEAR(item.ptr()); + } + self.list.clear(); } return 0; }; })); cls.def(py::init<>()); - cls.def_readwrite("value", &OwnsPythonObjects::value); + cls.def("append", &ContainerOwnsPythonObjects::append); + cls.def("at", &ContainerOwnsPythonObjects::at); + cls.def("size", &ContainerOwnsPythonObjects::size); + cls.def("clear", &ContainerOwnsPythonObjects::clear); .. versionadded:: 2.8 diff --git a/pybind11/docs/advanced/embedding.rst b/pybind11/docs/advanced/embedding.rst index 3ac0579..c41aec1 100644 --- a/pybind11/docs/advanced/embedding.rst +++ b/pybind11/docs/advanced/embedding.rst @@ -18,7 +18,7 @@ information, see :doc:`/compiling`. .. code-block:: cmake - cmake_minimum_required(VERSION 3.15...4.0) + cmake_minimum_required(VERSION 3.15...4.2) project(example) find_package(pybind11 REQUIRED) # or `add_subdirectory(pybind11)` @@ -302,7 +302,7 @@ Activating a Sub-interpreter Once a sub-interpreter is created, you can "activate" it on a thread (and acquire its GIL) by creating a :class:`subinterpreter_scoped_activate` -instance and passing it the sub-intepreter to be activated. The function +instance and passing it the sub-interpreter to be activated. The function will acquire the sub-interpreter's GIL and make the sub-interpreter the current active interpreter on the current thread for the lifetime of the instance. When the :class:`subinterpreter_scoped_activate` instance goes out @@ -492,4 +492,8 @@ Best Practices for sub-interpreter safety So you must still consider the thread safety of your C++ code. Remember, in Python 3.12 sub-interpreters must be destroyed on the same thread that they were created on. +- When using sub-interpreters in free-threaded python builds, note that creating and destroying + sub-interpreters may initiate a "stop-the-world". Be sure to detach long-running C++ threads + from Python thread state (similar to releasing the GIL) to avoid deadlocks. + - Familiarize yourself with :ref:`misc_concurrency`. diff --git a/pybind11/docs/advanced/functions.rst b/pybind11/docs/advanced/functions.rst index ff00c9c..c647ae0 100644 --- a/pybind11/docs/advanced/functions.rst +++ b/pybind11/docs/advanced/functions.rst @@ -84,8 +84,8 @@ The following table provides an overview of available policies: | :enum:`return_value_policy::reference_internal` | If the return value is an lvalue reference or a pointer, the parent object | | | (the implicit ``this``, or ``self`` argument of the called method or | | | property) is kept alive for at least the lifespan of the return value. | -| | **Otherwise this policy falls back to :enum:`return_value_policy::move` | -| | (see #5528).** Internally, this policy works just like | +| | **Otherwise this policy falls back to** :enum:`return_value_policy::move` | +| | (see #5528). Internally, this policy works just like | | | :enum:`return_value_policy::reference` but additionally applies a | | | ``keep_alive<0, 1>`` *call policy* (described in the next section) that | | | prevents the parent object from being garbage collected as long as the | diff --git a/pybind11/docs/basics.rst b/pybind11/docs/basics.rst index 1e68869..074d988 100644 --- a/pybind11/docs/basics.rst +++ b/pybind11/docs/basics.rst @@ -108,6 +108,8 @@ a file named :file:`example.cpp` with the following contents: #include + namespace py = pybind11; + int add(int i, int j) { return i + j; } diff --git a/pybind11/docs/changelog.md b/pybind11/docs/changelog.md index c8d6318..ecff970 100644 --- a/pybind11/docs/changelog.md +++ b/pybind11/docs/changelog.md @@ -13,6 +13,260 @@ Changes will be added here periodically from the "Suggested changelog entry" block in pull request descriptions. +## Version 3.0.4 (April 18, 2026) + +Bug fixes: + +- Fixed test builds with installed Eigen 5 by improving `Eigen3` CMake package detection. + [#6036](https://github.com/pybind/pybind11/pull/6036) + +- Fixed move semantics of `scoped_ostream_redirect` to preserve buffered output and avoid crashes when moved redirects restore stream buffers. + [#6033](https://github.com/pybind/pybind11/pull/6033) + +- Fixed `py::dynamic_attr()` traversal on Python 3.13+ to correctly propagate `PyObject_VisitManagedDict()` results. + [#6032](https://github.com/pybind/pybind11/pull/6032) + +- Fixed `std::shared_ptr` fallback casting to avoid unnecessary copy-constructor instantiation in `reference_internal` paths. + [#6028](https://github.com/pybind/pybind11/pull/6028) + +CI: + +- Updated `setup-uv` to the maintained GitHub Action tag scheme. + [#6035](https://github.com/pybind/pybind11/pull/6035) + +- Updated pre-commit hooks. + [#6029](https://github.com/pybind/pybind11/pull/6029) + +- Updated GitHub Actions dependencies, including `actions-setup-cmake` and `cibuildwheel`. + [#6027](https://github.com/pybind/pybind11/pull/6027) + + +## Version 3.0.3 (March 31, 2026) + +Bug fixes: + +- Fixed TSS key exhaustion in `implicitly_convertible()` when many implicit conversions are registered across large module sets. + [#6020](https://github.com/pybind/pybind11/pull/6020) + +- Fixed heap-buffer-overflow in `pythonbuf` with undersized buffers by enforcing a minimum buffer size. + [#6019](https://github.com/pybind/pybind11/pull/6019) + +- Fixed virtual-inheritance pointer offset crashes when dispatching inherited methods through virtual bases. + [#6017](https://github.com/pybind/pybind11/pull/6017) + +- Fixed `free(): invalid pointer` crashes during interpreter shutdown with `py::enum_<>` by duplicating late-added `def_property_static` argument strings. + [#6015](https://github.com/pybind/pybind11/pull/6015) + +- Fixed `function_record` heap-type deallocation to call `PyObject_Free()` and decref the type. + [#6010](https://github.com/pybind/pybind11/pull/6010) + +- Hardened `PYBIND11_MODULE_PYINIT` and `get_internals()` against module-initialization crashes. + [#6018](https://github.com/pybind/pybind11/pull/6018) + +- Fixed `static_pointer_cast` build failure with virtual inheritance in `holder_caster_foreign_helpers.h`. + [#6014](https://github.com/pybind/pybind11/pull/6014) + +- Fixed ambiguous `factory` template specialization that caused compilation failures with nvcc + GCC 14. + [#6011](https://github.com/pybind/pybind11/pull/6011) + +- Fixed crash in `def_readwrite` for non-smart-holder properties of smart-holder classes. + [#6008](https://github.com/pybind/pybind11/pull/6008) + +- Fixed memory leak for `py::dynamic_attr()` objects on Python 3.13+ by clearing managed `__dict__` contents during deallocation. + [#5999](https://github.com/pybind/pybind11/pull/5999) + +- Fixed binding of `noexcept` and ref-qualified (`&`, `&&`) methods inherited from unregistered base classes. + [#5992](https://github.com/pybind/pybind11/pull/5992) + +Internal: + +- Moved `tomlkit` dependency to the dev dependency group. + [#5990](https://github.com/pybind/pybind11/pull/5990) + +- Switched to newer public CPython APIs (`PyType_GetFlags` and public vectorcall APIs where available). + [#6005](https://github.com/pybind/pybind11/pull/6005) + +Tests: + +- Made an async callback test deterministic by replacing fixed sleep with bounded waiting. + [#5986](https://github.com/pybind/pybind11/pull/5986) + +CI: + +- Re-enabled Android tests in the cibuildwheel workflow. + [#6001](https://github.com/pybind/pybind11/pull/6001) + + +## Version 3.0.2 (February 16, 2026) + +New Features: + +- Added helper functions to `py::array` that return shape and strides as `std::span` when available. + [#5974](https://github.com/pybind/pybind11/pull/5974) + +Bug fixes: + +- Added fallback locking for Python 3.13t where `PyCriticalSection_BeginMutex` is unavailable. + [#5981](https://github.com/pybind/pybind11/pull/5981) + +- Fixed race condition in `py::make_key_iterator` with free-threaded Python. + [#5971](https://github.com/pybind/pybind11/pull/5971) + +- MSVC 19.16 and earlier were blocked from using `std::launder` due to internal compiler errors. + [#5968](https://github.com/pybind/pybind11/pull/5968) + +- Internals destructors were updated to check the owning interpreter before clearing Python objects. + [#5965](https://github.com/pybind/pybind11/pull/5965) + +- Internals shutdown handling was refined in two iterations before release: an initial finalization-time cleanup was followed by a safety adjustment to avoid late-shutdown `py::cast` segfaults. + [#5958](https://github.com/pybind/pybind11/pull/5958) + [#5972](https://github.com/pybind/pybind11/pull/5972) + +- Fixed ambiguous `str(handle)` construction for `object`-derived types like `kwargs` or `dict` by templatizing the constructor with SFINAE. + [#5949](https://github.com/pybind/pybind11/pull/5949) + +- Fixed concurrency consistency for `internals_pp_manager` under multiple-interpreters. + [#5947](https://github.com/pybind/pybind11/pull/5947) + +- Fixed MSVC LNK2001 in C++20 builds when /GL (whole program optimization) is enabled. + [#5939](https://github.com/pybind/pybind11/pull/5939) + +- Added per-interpreter storage for `gil_safe_call_once_and_store` to make it safe under multi-interpreters. + [#5933](https://github.com/pybind/pybind11/pull/5933) + +- A workaround for a GCC `-Warray-bounds` false positive in `argument_vector` was added. + [#5908](https://github.com/pybind/pybind11/pull/5908) + +- Corrected a mistake where support for `__index__` was added, but the type hints did not reflect acceptance of `SupportsIndex` objects. Also fixed a long-standing bug: the complex-caster did not accept `__index__` in `convert` mode. + [#5891](https://github.com/pybind/pybind11/pull/5891) + +- Fixed `*args/**kwargs` return types. Added type hinting to `py::make_tuple`. + [#5881](https://github.com/pybind/pybind11/pull/5881) + +- Fixed compiler error in `type_caster_generic` when casting a `T` implicitly convertible from `T*`. + [#5873](https://github.com/pybind/pybind11/pull/5873) + +- Updated `py::native_enum` bindings to unregister enum types on destruction, preventing a use-after-free when returning a destroyed enum instance. + [#5871](https://github.com/pybind/pybind11/pull/5871) + +- Fixed undefined behavior that occurred when importing pybind11 modules from non-main threads created by C API modules or embedded python interpreters. + [#5870](https://github.com/pybind/pybind11/pull/5870) + +- Fixed dangling pointer in `internals::registered_types_cpp_fast`. + [#5867](https://github.com/pybind/pybind11/pull/5867) + +- Added support for `std::shared_ptr` when loading module-local or conduit types from other modules. + [#5862](https://github.com/pybind/pybind11/pull/5862) + +- Fixed thread-safety issues if types were concurrently registered while `get_local_type_info()` was called in free threaded Python. + [#5856](https://github.com/pybind/pybind11/pull/5856) + +- Fixed py::float_ casting and py::int_ and py::float_ type hints. + [#5839](https://github.com/pybind/pybind11/pull/5839) + +- Fixed two `smart_holder` bugs in `shared_ptr` and `unique_ptr` adoption with multiple/virtual inheritance: + - `shared_ptr` to-Python caster was updated to register the correct subobject pointer (fixes #5786). + - `unique_ptr` adoption was updated to own the proper object start while aliasing subobject pointers for registration, which fixed MSVC crashes during destruction. + [#5836](https://github.com/pybind/pybind11/pull/5836) + +- Constrained `accessor::operator=` templates to avoid obscuring special members. + [#5832](https://github.com/pybind/pybind11/pull/5832) + +- Fixed crash that can occur when finalizers acquire and release the GIL. + [#5828](https://github.com/pybind/pybind11/pull/5828) + +- Fixed compiler detection in `pybind11/detail/pybind11_namespace_macros.h` for clang-cl on Windows, to address warning suppression macros. + [#5816](https://github.com/pybind/pybind11/pull/5816) + +- Fixed compatibility with CMake policy CMP0190 by not always requiring a Python interpreter when cross-compiling. + [#5829](https://github.com/pybind/pybind11/pull/5829) + +- Added a static assertion to disallow `keep_alive` and `call_guard` on properties. + [#5533](https://github.com/pybind/pybind11/pull/5533) + +Internal: + +- CMake policy limit was set to 4.1. + [#5944](https://github.com/pybind/pybind11/pull/5944) + +- Improved performance of function calls between Python and C++ by switching to the "vectorcall" calling protocol. + [#5948](https://github.com/pybind/pybind11/pull/5948) + +- Many C-style casts were replaced with C++-style casts. + [#5930](https://github.com/pybind/pybind11/pull/5930) + +- Added `cast_sources` abstraction to `type_caster_generic`. + [#5866](https://github.com/pybind/pybind11/pull/5866) + +- Improved the performance of from-Python conversions of legacy pybind11 enum objects bound by `py::enum_`. + [#5860](https://github.com/pybind/pybind11/pull/5860) + +- Reduced size overhead by deduplicating functions' readable signatures and type information. + [#5857](https://github.com/pybind/pybind11/pull/5857) + +- Used new Python 3.14 C APIs when available. + [#5854](https://github.com/pybind/pybind11/pull/5854) + +- Improved performance of function dispatch and type casting by porting two-level type info lookup strategy from nanobind. + [#5842](https://github.com/pybind/pybind11/pull/5842) + +- Updated `.gitignore` to exclude `__pycache__/` directories. + [#5838](https://github.com/pybind/pybind11/pull/5838) + +- Changed internals to use `thread_local` instead of `thread_specific_storage` for increased performance. + [#5834](https://github.com/pybind/pybind11/pull/5834) + +- Reduced function call overhead by using thread_local for loader_life_support when possible. + [#5830](https://github.com/pybind/pybind11/pull/5830) + +- Removed heap allocation for the C++ argument array when dispatching functions with 6 or fewer arguments. + [#5824](https://github.com/pybind/pybind11/pull/5824) + + +Documentation: + +- Fixed docstring for `long double` complex types to use `numpy.clongdouble` instead of the deprecated `numpy.longcomplex` (removed in NumPy 2.0). + [#5952](https://github.com/pybind/pybind11/pull/5952) + +- The "Supported compilers" and "Supported platforms" sections in the main `README.rst` were replaced with a new "Supported platforms & compilers" section that points to the CI test matrix as the living source of truth. + [#5910](https://github.com/pybind/pybind11/pull/5910) + +- Fixed documentation formatting. + [#5903](https://github.com/pybind/pybind11/pull/5903) + +- Updated upgrade notes for `py::native_enum`. + [#5885](https://github.com/pybind/pybind11/pull/5885) + +- Clarified in the docs to what extent bindings are global. + [#5859](https://github.com/pybind/pybind11/pull/5859) + + +Tests: + +- Fixed deadlock in a free-threading test by releasing the GIL while waiting on synchronization. + [#5973](https://github.com/pybind/pybind11/pull/5973) + +- Calls to `env.deprecated_call()` were replaced with direct calls to `pytest.deprecated_call()`. + [#5893](https://github.com/pybind/pybind11/pull/5893) + +- Updated pytest configuration to use `log_level` instead of `log_cli_level`. + [#5890](https://github.com/pybind/pybind11/pull/5890) + + +CI: + +- Added CI tests for windows-11-arm with clang/MSVC (currently python 3.13), windows-11-arm with clang/mingw (currently python 3.12). + [#5932](https://github.com/pybind/pybind11/pull/5932) + +- These clang-tidy rules were added: `readability-redundant-casting`, `readability-redundant-inline-specifier`, `readability-redundant-member-init` + [#5924](https://github.com/pybind/pybind11/pull/5924) + +- Replaced deprecated macos-13 runners with macos-15-intel in CI. + [#5916](https://github.com/pybind/pybind11/pull/5916) + +- Restored `runs-on: windows-latest` in CI. + [#5835](https://github.com/pybind/pybind11/pull/5835) + ## Version 3.0.1 (August 22, 2025) Bug fixes: @@ -167,7 +421,7 @@ New Features: [#5665](https://github.com/pybind/pybind11/pull/5665) and consolidate code [#5670](https://github.com/pybind/pybind11/pull/5670). -- Added API in `pybind11/subinterpreter.h` for embedding sub-intepreters (requires Python 3.12+). +- Added API in `pybind11/subinterpreter.h` for embedding sub-interpreters (requires Python 3.12+). [#5666](https://github.com/pybind/pybind11/pull/5666) - `py::native_enum` was added, for conversions between Python's native @@ -1213,7 +1467,7 @@ Performance and style: - Optimize Eigen sparse matrix casting by removing unnecessary temporary. [#4064](https://github.com/pybind/pybind11/pull/4064) - Avoid potential implicit copy/assignment constructors causing double - free in `strdup_gaurd`. + free in `strdup_guard`. [#3905](https://github.com/pybind/pybind11/pull/3905) - Enable clang-tidy checks `misc-definitions-in-headers`, `modernize-loop-convert`, and `modernize-use-nullptr`. diff --git a/pybind11/docs/compiling.rst b/pybind11/docs/compiling.rst index e74e3b2..b693bd5 100644 --- a/pybind11/docs/compiling.rst +++ b/pybind11/docs/compiling.rst @@ -18,7 +18,7 @@ A Python extension module can be created with just a few lines of code: .. code-block:: cmake - cmake_minimum_required(VERSION 3.15...4.0) + cmake_minimum_required(VERSION 3.15...4.2) project(example LANGUAGES CXX) set(PYBIND11_FINDPYTHON ON) @@ -447,7 +447,7 @@ See the `Config file`_ docstring for details of relevant CMake variables. .. code-block:: cmake - cmake_minimum_required(VERSION 3.15...4.0) + cmake_minimum_required(VERSION 3.15...4.2) project(example LANGUAGES CXX) find_package(pybind11 REQUIRED) @@ -492,7 +492,7 @@ FindPython, pybind11 will detect this and use the existing targets instead: .. code-block:: cmake - cmake_minimum_required(VERSION 3.15...4.0) + cmake_minimum_required(VERSION 3.15...4.2) project(example LANGUAGES CXX) find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) @@ -570,7 +570,7 @@ You can use these targets to build complex applications. For example, the .. code-block:: cmake - cmake_minimum_required(VERSION 3.15...4.0) + cmake_minimum_required(VERSION 3.15...4.2) project(example LANGUAGES CXX) find_package(pybind11 REQUIRED) # or add_subdirectory(pybind11) @@ -628,7 +628,7 @@ information about usage in C++, see :doc:`/advanced/embedding`. .. code-block:: cmake - cmake_minimum_required(VERSION 3.15...4.0) + cmake_minimum_required(VERSION 3.15...4.2) project(example LANGUAGES CXX) find_package(pybind11 REQUIRED) # or add_subdirectory(pybind11) diff --git a/pybind11/docs/requirements.txt b/pybind11/docs/requirements.txt index 0bbb01a..c19fbf2 100644 --- a/pybind11/docs/requirements.txt +++ b/pybind11/docs/requirements.txt @@ -23,8 +23,6 @@ idna==3.7 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==8.7.0 - # via sphinx jinja2==3.1.6 # via # myst-parser @@ -43,13 +41,13 @@ myst-parser==3.0.1 # via -r requirements.in packaging==24.0 # via sphinx -pygments==2.17.2 +pygments==2.20.0 # via # furo # sphinx pyyaml==6.0.2 # via myst-parser -requests==2.32.4 +requests==2.33.0 # via sphinx snowballstemmer==2.2.0 # via sphinx @@ -85,7 +83,5 @@ sphinxcontrib-serializinghtml==1.1.10 # via sphinx sphinxcontrib-svg2pdfconverter==1.2.2 # via -r requirements.in -urllib3==2.5.0 +urllib3==2.6.3 # via requests -zipp==3.23.0 - # via importlib-metadata diff --git a/pybind11/docs/upgrade.rst b/pybind11/docs/upgrade.rst index 9b373fc..966a319 100644 --- a/pybind11/docs/upgrade.rst +++ b/pybind11/docs/upgrade.rst @@ -48,6 +48,8 @@ C++ enumerations as native Python types โ€” typically standard-library ``enum.Enum`` or related subclasses. This provides improved integration with Python's enum system, compared to the older (now deprecated) ``py::enum_``. See `#5555 `_ for details. +Note that ``#include `` is not included automatically +and must be added explicitly. Functions exposed with pybind11 are now pickleable. This removes a long-standing obstacle when using pybind11-bound functions with Python features diff --git a/pybind11/include/pybind11/attr.h b/pybind11/include/pybind11/attr.h index 9b631fa..d337595 100644 --- a/pybind11/include/pybind11/attr.h +++ b/pybind11/include/pybind11/attr.h @@ -373,12 +373,12 @@ struct type_record { + (base_has_unique_ptr_holder ? "does not" : "does")); } - bases.append((PyObject *) base_info->type); + bases.append(reinterpret_cast(base_info->type)); #ifdef PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET dynamic_attr |= base_info->type->tp_dictoffset != 0; #else - dynamic_attr |= (base_info->type->tp_flags & Py_TPFLAGS_MANAGED_DICT) != 0; + dynamic_attr |= (PyType_GetFlags(base_info->type) & Py_TPFLAGS_MANAGED_DICT) != 0; #endif if (caster) { @@ -702,6 +702,12 @@ struct process_attributes { } }; +template +struct is_keep_alive : std::false_type {}; + +template +struct is_keep_alive> : std::true_type {}; + template using is_call_guard = is_instantiation; @@ -715,7 +721,9 @@ template ::value...)> constexpr bool expected_num_args(size_t nargs, bool has_args, bool has_kwargs) { PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(nargs, has_args, has_kwargs); - return named == 0 || (self + named + size_t(has_args) + size_t(has_kwargs)) == nargs; + return named == 0 + || (self + named + static_cast(has_args) + static_cast(has_kwargs)) + == nargs; } PYBIND11_NAMESPACE_END(detail) diff --git a/pybind11/include/pybind11/buffer_info.h b/pybind11/include/pybind11/buffer_info.h index 75aec0b..10fa825 100644 --- a/pybind11/include/pybind11/buffer_info.h +++ b/pybind11/include/pybind11/buffer_info.h @@ -66,10 +66,11 @@ struct buffer_info { bool readonly = false) : ptr(ptr), itemsize(itemsize), size(1), format(format), ndim(ndim), shape(std::move(shape_in)), strides(std::move(strides_in)), readonly(readonly) { - if (ndim != (ssize_t) shape.size() || ndim != (ssize_t) strides.size()) { + if (ndim != static_cast(shape.size()) + || ndim != static_cast(strides.size())) { pybind11_fail("buffer_info: ndim doesn't match shape and/or strides length"); } - for (size_t i = 0; i < (size_t) ndim; ++i) { + for (size_t i = 0; i < static_cast(ndim); ++i) { size *= shape[i]; } } @@ -195,7 +196,7 @@ struct compare_buffer_info { template struct compare_buffer_info::value>> { static bool compare(const buffer_info &b) { - return (size_t) b.itemsize == sizeof(T) + return static_cast(b.itemsize) == sizeof(T) && (b.format == format_descriptor::value || ((sizeof(T) == sizeof(long)) && b.format == (std::is_unsigned::value ? "L" : "l")) diff --git a/pybind11/include/pybind11/cast.h b/pybind11/include/pybind11/cast.h index c635791..9ebabce 100644 --- a/pybind11/include/pybind11/cast.h +++ b/pybind11/include/pybind11/cast.h @@ -10,8 +10,10 @@ #pragma once +#include "detail/argument_vector.h" #include "detail/common.h" #include "detail/descr.h" +#include "detail/holder_caster_foreign_helpers.h" #include "detail/native_enum_data.h" #include "detail/type_caster_base.h" #include "detail/typeid.h" @@ -92,37 +94,37 @@ class type_caster_enum_type { if (!underlying_caster.load(src.attr("value"), convert)) { pybind11_fail("native_enum internal consistency failure."); } - value = static_cast(static_cast(underlying_caster)); + native_value = static_cast(static_cast(underlying_caster)); + native_loaded = true; return true; } - if (!pybind11_enum_) { - pybind11_enum_.reset(new type_caster_base()); + + type_caster_base legacy_caster; + if (legacy_caster.load(src, convert)) { + legacy_ptr = static_cast(legacy_caster); + return true; } - return pybind11_enum_->load(src, convert); + return false; } template using cast_op_type = detail::cast_op_type; // NOLINTNEXTLINE(google-explicit-constructor) - operator EnumType *() { - if (!pybind11_enum_) { - return &value; - } - return pybind11_enum_->operator EnumType *(); - } + operator EnumType *() { return native_loaded ? &native_value : legacy_ptr; } // NOLINTNEXTLINE(google-explicit-constructor) operator EnumType &() { - if (!pybind11_enum_) { - return value; + if (!native_loaded && !legacy_ptr) { + throw reference_cast_error(); } - return pybind11_enum_->operator EnumType &(); + return native_loaded ? native_value : *legacy_ptr; } private: - std::unique_ptr> pybind11_enum_; - EnumType value; + EnumType native_value; // if loading a py::native_enum + bool native_loaded = false; + EnumType *legacy_ptr = nullptr; // if loading a py::enum_ }; template @@ -197,8 +199,7 @@ public: template >::value, \ - int> \ - = 0> \ + int> = 0> \ static ::pybind11::handle cast( \ T_ *src, ::pybind11::return_value_policy policy, ::pybind11::handle parent) { \ if (!src) \ @@ -345,9 +346,12 @@ struct type_caster::value && !is_std_char_t return PyLong_FromUnsignedLongLong((unsigned long long) src); } - PYBIND11_TYPE_CASTER(T, - io_name::value>( - "typing.SupportsInt", "int", "typing.SupportsFloat", "float")); + PYBIND11_TYPE_CASTER( + T, + io_name::value>("typing.SupportsInt | typing.SupportsIndex", + "int", + "typing.SupportsFloat | typing.SupportsIndex", + "float")); }; template @@ -389,7 +393,8 @@ class type_caster : public type_caster { } /* Check if this is a C++ type */ - const auto &bases = all_type_info((PyTypeObject *) type::handle_of(h).ptr()); + const auto &bases + = all_type_info(reinterpret_cast(type::handle_of(h).ptr())); if (bases.size() == 1) { // Only allowing loading from a single-value type value = values_and_holders(reinterpret_cast(h.ptr())).begin()->value_ptr(); return true; @@ -470,7 +475,7 @@ class type_caster { private: // Test if an object is a NumPy boolean (without fetching the type). - static inline bool is_numpy_bool(handle object) { + static bool is_numpy_bool(handle object) { const char *type_name = Py_TYPE(object.ptr())->tp_name; // Name changed to `numpy.bool` in NumPy 2, `numpy.bool_` is needed for 1.x support return std::strcmp("numpy.bool", type_name) == 0 @@ -536,7 +541,7 @@ struct string_caster { const auto *buffer = reinterpret_cast(PYBIND11_BYTES_AS_STRING(utfNbytes.ptr())); - size_t length = (size_t) PYBIND11_BYTES_SIZE(utfNbytes.ptr()) / sizeof(CharT); + size_t length = static_cast(PYBIND11_BYTES_SIZE(utfNbytes.ptr())) / sizeof(CharT); // Skip BOM for UTF-16/32 if (UTF_N > 8) { buffer++; @@ -906,6 +911,10 @@ struct copyable_holder_caster : public type_caster_base { } } + bool set_foreign_holder(handle src) { + return holder_caster_foreign_helpers::set_foreign_holder(src, (type *) value, &holder); + } + void load_value(value_and_holder &&v_h) { if (v_h.holder_constructed()) { value = v_h.value_ptr(); @@ -976,7 +985,7 @@ struct copyable_holder_caster< } explicit operator std::shared_ptr *() { - if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + if (sh_load_helper.was_populated) { pybind11_fail("Passing `std::shared_ptr *` from Python to C++ is not supported " "(inherently unsafe)."); } @@ -984,14 +993,14 @@ struct copyable_holder_caster< } explicit operator std::shared_ptr &() { - if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + if (sh_load_helper.was_populated) { shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, value); } return shared_ptr_storage; } std::weak_ptr potentially_slicing_weak_ptr() { - if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + if (sh_load_helper.was_populated) { // Reusing shared_ptr code to minimize code complexity. shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, @@ -1005,15 +1014,24 @@ struct copyable_holder_caster< static handle cast(const std::shared_ptr &src, return_value_policy policy, handle parent) { const auto *ptr = src.get(); - auto st = type_caster_base::src_and_type(ptr); - if (st.second == nullptr) { - return handle(); // no type info: error will be set already - } - if (st.second->holder_enum_v == detail::holder_enum_t::smart_holder) { + typename type_caster_base::cast_sources srcs{ptr}; + if (srcs.creates_smart_holder()) { return smart_holder_type_caster_support::smart_holder_from_shared_ptr( - src, policy, parent, st); + src, policy, parent, srcs.result); } - return type_caster_base::cast_holder(ptr, &src); + + auto *tinfo = srcs.result.tinfo; + if (tinfo != nullptr && tinfo->holder_enum_v == holder_enum_t::std_shared_ptr) { + return type_caster_base::cast_holder(srcs, &src); + } + + if (parent) { + return type_caster_generic::cast_non_owning( + srcs, return_value_policy::reference_internal, parent); + } + + throw cast_error("Unable to convert std::shared_ptr to Python when the bound type " + "does not use std::shared_ptr or py::smart_holder as its holder type"); } // This function will succeed even if the `responsible_parent` does not own the @@ -1040,6 +1058,11 @@ struct copyable_holder_caster< } } + bool set_foreign_holder(handle src) { + return holder_caster_foreign_helpers::set_foreign_holder( + src, (type *) value, &shared_ptr_storage); + } + void load_value(value_and_holder &&v_h) { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = v_h; @@ -1077,6 +1100,7 @@ struct copyable_holder_caster< value = cast.second(sub_caster.value); if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h; + sh_load_helper.was_populated = true; } else { shared_ptr_storage = std::shared_ptr(sub_caster.shared_ptr_storage, (type *) value); @@ -1183,21 +1207,12 @@ struct move_only_holder_caster< static handle cast(std::unique_ptr &&src, return_value_policy policy, handle parent) { auto *ptr = src.get(); - auto st = type_caster_base::src_and_type(ptr); - if (st.second == nullptr) { - return handle(); // no type info: error will be set already - } - if (st.second->holder_enum_v == detail::holder_enum_t::smart_holder) { + typename type_caster_base::cast_sources srcs{ptr}; + if (srcs.creates_smart_holder()) { return smart_holder_type_caster_support::smart_holder_from_unique_ptr( - std::move(src), policy, parent, st); + std::move(src), policy, parent, srcs.result); } - return type_caster_generic::cast(st.first, - return_value_policy::take_ownership, - {}, - st.second, - nullptr, - nullptr, - std::addressof(src)); + return type_caster_base::cast_holder(srcs, &src); } static handle @@ -1223,6 +1238,12 @@ struct move_only_holder_caster< return false; } + bool set_foreign_holder(handle) { + throw cast_error("Foreign instance cannot be converted to std::unique_ptr " + "because we don't know how to make it relinquish " + "ownership"); + } + void load_value(value_and_holder &&v_h) { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = v_h; @@ -1281,6 +1302,7 @@ struct move_only_holder_caster< value = cast.second(sub_caster.value); if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h; + sh_load_helper.was_populated = true; } else { pybind11_fail("Expected to be UNREACHABLE: " __FILE__ ":" PYBIND11_TOSTRING(__LINE__)); @@ -1400,7 +1422,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = io_name("typing.SupportsInt", "int"); + static constexpr auto name = const_name("int"); }; template <> struct handle_type_name { @@ -1412,7 +1434,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = io_name("typing.SupportsFloat", "float"); + static constexpr auto name = const_name("float"); }; template <> struct handle_type_name { @@ -1458,21 +1480,24 @@ template <> struct handle_type_name { static constexpr auto name = const_name("weakref.ReferenceType"); }; +// args/Args/kwargs/KWArgs have name as well as typehint included template <> struct handle_type_name { - static constexpr auto name = const_name("*args"); + static constexpr auto name = io_name("*args", "tuple"); }; template struct handle_type_name> { - static constexpr auto name = const_name("*args: ") + make_caster::name; + static constexpr auto name + = io_name("*args: ", "tuple[") + make_caster::name + io_name("", ", ...]"); }; template <> struct handle_type_name { - static constexpr auto name = const_name("**kwargs"); + static constexpr auto name = io_name("**kwargs", "dict[str, typing.Any]"); }; template struct handle_type_name> { - static constexpr auto name = const_name("**kwargs: ") + make_caster::name; + static constexpr auto name + = io_name("**kwargs: ", "dict[str, ") + make_caster::name + io_name("", "]"); }; template <> struct handle_type_name { @@ -1533,6 +1558,21 @@ struct pyobject_caster { template class type_caster::value>> : public pyobject_caster {}; +template <> +class type_caster : public pyobject_caster { +public: + bool load(handle src, bool /* convert */) { + if (isinstance(src)) { + value = reinterpret_borrow(src); + } else if (isinstance(src)) { + value = float_(reinterpret_borrow(src)); + } else { + return false; + } + return true; + } +}; + // Our conditions for enabling moving are quite restrictive: // At compile time: // - T needs to be a non-const, non-pointer, non-reference type @@ -1633,8 +1673,7 @@ PYBIND11_NAMESPACE_END(detail) template ::value && !detail::is_same_ignoring_cvref::value, - int> - = 0> + int> = 0> T cast(const handle &handle) { using namespace detail; constexpr bool is_enum_cast = type_uses_type_caster_enum_type>::value; @@ -1669,8 +1708,7 @@ template ::value && detail::is_same_ignoring_cvref::value, - int> - = 0> + int> = 0> T cast(Handle &&handle) { return handle.inc_ref().ptr(); } @@ -1679,8 +1717,7 @@ template ::value && detail::is_same_ignoring_cvref::value, - int> - = 0> + int> = 0> T cast(Object &&obj) { return obj.release().ptr(); } @@ -1883,13 +1920,20 @@ inline cast_error cast_error_unable_to_convert_call_arg(const std::string &name, } #endif +namespace typing { +template +class Tuple : public tuple { + using tuple::tuple; +}; +} // namespace typing + template -tuple make_tuple() { +typing::Tuple<> make_tuple() { return tuple(0); } template -tuple make_tuple(Args &&...args_) { +typing::Tuple make_tuple(Args &&...args_) { constexpr size_t size = sizeof...(Args); std::array args{{reinterpret_steal( detail::make_caster::cast(std::forward(args_), policy, nullptr))...}}; @@ -1908,7 +1952,12 @@ tuple make_tuple(Args &&...args_) { for (auto &arg_value : args) { PyTuple_SET_ITEM(result.ptr(), counter++, arg_value.release().ptr()); } + PYBIND11_WARNING_PUSH +#ifdef PYBIND11_DETECTED_CLANG_WITH_MISLEADING_CALL_STD_MOVE_EXPLICITLY_WARNING + PYBIND11_WARNING_DISABLE_CLANG("-Wreturn-std-move") +#endif return result; + PYBIND11_WARNING_POP } /// \ingroup annotations @@ -2037,6 +2086,9 @@ using is_pos_only = std::is_same, pos_only>; // forward declaration (definition in attr.h) struct function_record; +/// Inline size chosen mostly arbitrarily. +constexpr std::size_t arg_vector_small_size = 6; + /// Internal data associated with a single function call struct function_call { function_call(const function_record &f, handle p); // Implementation in attr.h @@ -2045,10 +2097,10 @@ struct function_call { const function_record &func; /// Arguments passed to the function: - std::vector args; + argument_vector args; /// The `convert` value the arguments should be loaded with - std::vector args_convert; + args_convert_vector args_convert; /// Extra references for the optional `py::args` and/or `py::kwargs` arguments (which, if /// present, are also in `args` but without a reference). @@ -2118,6 +2170,11 @@ class argument_loader { template bool load_impl_sequence(function_call &call, index_sequence) { + PYBIND11_WARNING_PUSH +#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 13 + // Work around a GCC -Warray-bounds false positive in argument_vector usage. + PYBIND11_WARNING_DISABLE_GCC("-Warray-bounds") +#endif #ifdef __cpp_fold_expressions if ((... || !std::get(argcasters).load(call.args[Is], call.args_convert[Is]))) { return false; @@ -2129,6 +2186,7 @@ class argument_loader { } } #endif + PYBIND11_WARNING_POP return true; } @@ -2140,86 +2198,126 @@ class argument_loader { std::tuple...> argcasters; }; -/// Helper class which collects only positional arguments for a Python function call. -/// A fancier version below can collect any argument, but this one is optimal for simple calls. +// [workaround(intel)] Separate function required here +// We need to put this into a separate function because the Intel compiler +// fails to compile enable_if_t...>::value> +// (tested with ICC 2021.1 Beta 20200827). +template +constexpr bool args_has_keyword_or_ds() { + return any_of...>::value; +} + +/// Helper class which collects positional, keyword, * and ** arguments for a Python function call template -class simple_collector { +class unpacking_collector { public: template - explicit simple_collector(Ts &&...values) - : m_args(pybind11::make_tuple(std::forward(values)...)) {} - - const tuple &args() const & { return m_args; } - dict kwargs() const { return {}; } + explicit unpacking_collector(Ts &&...values) + : m_names(reinterpret_steal( + handle())) // initialize to null to avoid useless allocation of 0-length tuple + { + /* + Python can sometimes utilize an extra space before the arguments to prepend `self`. + This is important enough that there is a special flag for it: + PY_VECTORCALL_ARGUMENTS_OFFSET. + All we have to do is allocate an extra space at the beginning of this array, and set the + flag. Note that the extra space is not passed directly in to vectorcall. + */ + m_args.reserve(sizeof...(values) + 1); + m_args.push_back_null(); + + if (args_has_keyword_or_ds()) { + list names_list; + + // collect_arguments guarantees this can't be constructed with kwargs before the last + // positional so we don't need to worry about Ts... being in anything but normal python + // order. + using expander = int[]; + (void) expander{0, (process(names_list, std::forward(values)), 0)...}; + + m_names = reinterpret_steal(PyList_AsTuple(names_list.ptr())); + } else { + auto not_used + = reinterpret_steal(handle()); // initialize as null (to avoid an allocation) - tuple args() && { return std::move(m_args); } + using expander = int[]; + (void) expander{0, (process(not_used, std::forward(values)), 0)...}; + } + } /// Call a Python function and pass the collected arguments object call(PyObject *ptr) const { - PyObject *result = PyObject_CallObject(ptr, m_args.ptr()); + size_t nargs = m_args.size() - 1; // -1 for PY_VECTORCALL_ARGUMENTS_OFFSET (see ctor) + if (m_names) { + nargs -= m_names.size(); + } + PyObject *result = +#if PY_VERSION_HEX >= 0x03090000 + PyObject_Vectorcall( +#else + _PyObject_Vectorcall( +#endif + ptr, m_args.data() + 1, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, m_names.ptr()); if (!result) { throw error_already_set(); } return reinterpret_steal(result); } -private: - tuple m_args; -}; - -/// Helper class which collects positional, keyword, * and ** arguments for a Python function call -template -class unpacking_collector { -public: - template - explicit unpacking_collector(Ts &&...values) { - // Tuples aren't (easily) resizable so a list is needed for collection, - // but the actual function call strictly requires a tuple. - auto args_list = list(); - using expander = int[]; - (void) expander{0, (process(args_list, std::forward(values)), 0)...}; - - m_args = std::move(args_list); + tuple args() const { + size_t nargs = m_args.size() - 1; // -1 for PY_VECTORCALL_ARGUMENTS_OFFSET (see ctor) + if (m_names) { + nargs -= m_names.size(); + } + tuple val(nargs); + for (size_t i = 0; i < nargs; ++i) { + // +1 for PY_VECTORCALL_ARGUMENTS_OFFSET (see ctor) + val[i] = reinterpret_borrow(m_args[i + 1]); + } + return val; } - const tuple &args() const & { return m_args; } - const dict &kwargs() const & { return m_kwargs; } - - tuple args() && { return std::move(m_args); } - dict kwargs() && { return std::move(m_kwargs); } - - /// Call a Python function and pass the collected arguments - object call(PyObject *ptr) const { - PyObject *result = PyObject_Call(ptr, m_args.ptr(), m_kwargs.ptr()); - if (!result) { - throw error_already_set(); + dict kwargs() const { + dict val; + if (m_names) { + size_t offset = m_args.size() - m_names.size(); + for (size_t i = 0; i < m_names.size(); ++i, ++offset) { + val[m_names[i]] = reinterpret_borrow(m_args[offset]); + } } - return reinterpret_steal(result); + return val; } private: + // normal argument, possibly needing conversion template - void process(list &args_list, T &&x) { - auto o = reinterpret_steal( - detail::make_caster::cast(std::forward(x), policy, {})); - if (!o) { + void process(list & /*names_list*/, T &&x) { + handle h = detail::make_caster::cast(std::forward(x), policy, {}); + if (!h) { #if !defined(PYBIND11_DETAILED_ERROR_MESSAGES) - throw cast_error_unable_to_convert_call_arg(std::to_string(args_list.size())); + throw cast_error_unable_to_convert_call_arg(std::to_string(m_args.size() - 1)); #else - throw cast_error_unable_to_convert_call_arg(std::to_string(args_list.size()), + throw cast_error_unable_to_convert_call_arg(std::to_string(m_args.size() - 1), type_id()); #endif } - args_list.append(std::move(o)); + m_args.push_back_steal(h.ptr()); // cast returns a new reference } - void process(list &args_list, detail::args_proxy ap) { + // * unpacking + void process(list & /*names_list*/, detail::args_proxy ap) { + if (!ap) { + return; + } for (auto a : ap) { - args_list.append(a); + m_args.push_back_borrow(a.ptr()); } } - void process(list & /*args_list*/, arg_v a) { + // named argument + // NOLINTNEXTLINE(performance-unnecessary-value-param) + void process(list &names_list, arg_v a) { + assert(names_list); if (!a.name) { #if !defined(PYBIND11_DETAILED_ERROR_MESSAGES) nameless_argument_error(); @@ -2227,7 +2325,8 @@ class unpacking_collector { nameless_argument_error(a.type); #endif } - if (m_kwargs.contains(a.name)) { + auto name = str(a.name); + if (names_list.contains(name)) { #if !defined(PYBIND11_DETAILED_ERROR_MESSAGES) multiple_values_error(); #else @@ -2241,22 +2340,27 @@ class unpacking_collector { throw cast_error_unable_to_convert_call_arg(a.name, a.type); #endif } - m_kwargs[a.name] = std::move(a.value); + names_list.append(std::move(name)); + m_args.push_back_borrow(a.value.ptr()); } - void process(list & /*args_list*/, detail::kwargs_proxy kp) { + // ** unpacking + void process(list &names_list, detail::kwargs_proxy kp) { if (!kp) { return; } - for (auto k : reinterpret_borrow(kp)) { - if (m_kwargs.contains(k.first)) { + assert(names_list); + for (auto &&k : reinterpret_borrow(kp)) { + auto name = str(k.first); + if (names_list.contains(name)) { #if !defined(PYBIND11_DETAILED_ERROR_MESSAGES) multiple_values_error(); #else - multiple_values_error(str(k.first)); + multiple_values_error(name); #endif } - m_kwargs[k.first] = k.second; + names_list.append(std::move(name)); + m_args.push_back_borrow(k.second.ptr()); } } @@ -2282,39 +2386,20 @@ class unpacking_collector { } private: - tuple m_args; - dict m_kwargs; + ref_small_vector m_args; + tuple m_names; }; -// [workaround(intel)] Separate function required here -// We need to put this into a separate function because the Intel compiler -// fails to compile enable_if_t...>::value> -// (tested with ICC 2021.1 Beta 20200827). -template -constexpr bool args_are_all_positional() { - return all_of...>::value; -} - -/// Collect only positional arguments for a Python function call -template ()>> -simple_collector collect_arguments(Args &&...args) { - return simple_collector(std::forward(args)...); -} - -/// Collect all arguments, including keywords and unpacking (only instantiated when needed) -template ()>> +/// Collect all arguments, including keywords and unpacking +template unpacking_collector collect_arguments(Args &&...args) { // Following argument order rules for generalized unpacking according to PEP 448 - static_assert(constexpr_last() - < constexpr_first() - && constexpr_last() - < constexpr_first(), - "Invalid function call: positional args must precede keywords and ** unpacking; " - "* unpacking must precede ** unpacking"); + static_assert( + constexpr_last() < constexpr_first(), + "Invalid function call: positional args must precede keywords and */** unpacking;"); + static_assert(constexpr_last() + < constexpr_first(), + "Invalid function call: * unpacking must precede ** unpacking"); return unpacking_collector(std::forward(args)...); } diff --git a/pybind11/include/pybind11/chrono.h b/pybind11/include/pybind11/chrono.h index d9bcc4b..668e458 100644 --- a/pybind11/include/pybind11/chrono.h +++ b/pybind11/include/pybind11/chrono.h @@ -64,8 +64,7 @@ class duration_caster { return src; } static const std::chrono::duration & - get_duration(const std::chrono::duration &&) - = delete; + get_duration(const std::chrono::duration &&) = delete; // If this is a time_point get the time_since_epoch template diff --git a/pybind11/include/pybind11/complex.h b/pybind11/include/pybind11/complex.h index 8a831c1..0b6f493 100644 --- a/pybind11/include/pybind11/complex.h +++ b/pybind11/include/pybind11/complex.h @@ -54,7 +54,23 @@ class type_caster> { if (!convert && !PyComplex_Check(src.ptr())) { return false; } - Py_complex result = PyComplex_AsCComplex(src.ptr()); + handle src_or_index = src; + // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. + // The same logic is used in numeric_caster for ints and floats +#if defined(PYPY_VERSION) + object index; + if (PYBIND11_INDEX_CHECK(src.ptr())) { + index = reinterpret_steal(PyNumber_Index(src.ptr())); + if (!index) { + PyErr_Clear(); + if (!convert) + return false; + } else { + src_or_index = index; + } + } +#endif + Py_complex result = PyComplex_AsCComplex(src_or_index.ptr()); if (result.real == -1.0 && PyErr_Occurred()) { PyErr_Clear(); return false; @@ -68,7 +84,10 @@ class type_caster> { return PyComplex_FromDoubles((double) src.real(), (double) src.imag()); } - PYBIND11_TYPE_CASTER(std::complex, const_name("complex")); + PYBIND11_TYPE_CASTER( + std::complex, + io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex", + "complex")); }; PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/detail/argument_vector.h b/pybind11/include/pybind11/detail/argument_vector.h new file mode 100644 index 0000000..6e2c2ec --- /dev/null +++ b/pybind11/include/pybind11/detail/argument_vector.h @@ -0,0 +1,417 @@ +/* + pybind11/detail/argument_vector.h: small_vector-like containers to + avoid heap allocation of arguments during function call dispatch. + + Copyright (c) Meta Platforms, Inc. and affiliates. + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include + +#include "common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +PYBIND11_WARNING_DISABLE_MSVC(4127) + +PYBIND11_NAMESPACE_BEGIN(detail) + +// Shared implementation utility for our small_vector-like containers. +// We support C++11 and C++14, so we cannot use +// std::variant. Union with the tag packed next to the inline +// array's size is smaller anyway, allowing 1 extra handle of +// inline storage for free. Compare the layouts (1 line per +// size_t/void*, assuming a 64-bit machine): +// With variant, total is N + 2 for N >= 2: +// - variant tag (cannot be packed with the array size) +// - array size (or first pointer of 3 in std::vector) +// - N pointers of inline storage (or 2 remaining pointers of std::vector) +// Custom union, total is N + 1 for N >= 3: +// - variant tag & array size if applicable +// - N pointers of inline storage (or 3 pointers of std::vector) +// +// NOTE: this is a low-level representational convenience; the two +// use cases of this union are materially different and in particular +// have different semantics for inline_array::size. All that is being +// shared is the memory management behavior. +template +union inline_array_or_vector { + struct inline_array { + bool is_inline = true; + std::uint32_t size = 0; + std::array arr; + }; + struct heap_vector { + bool is_inline = false; + std::vector vec; + + heap_vector() = default; + heap_vector(std::size_t count, VectorT value) : vec(count, value) {} + }; + + inline_array iarray; + heap_vector hvector; + + inline_array_or_vector() : iarray() {} + + ~inline_array_or_vector() { + if (is_inline()) { + iarray.~inline_array(); + } else { + hvector.~heap_vector(); + } + } + + // Disable copy ctor and assignment. + inline_array_or_vector(const inline_array_or_vector &) = delete; + inline_array_or_vector &operator=(const inline_array_or_vector &) = delete; + + inline_array_or_vector(inline_array_or_vector &&rhs) noexcept { + if (rhs.is_inline()) { + new (&iarray) inline_array(std::move(rhs.iarray)); + } else { + new (&hvector) heap_vector(std::move(rhs.hvector)); + } + assert(is_inline() == rhs.is_inline()); + } + + inline_array_or_vector &operator=(inline_array_or_vector &&rhs) noexcept { + if (this == &rhs) { + return *this; + } + + if (is_inline()) { + iarray.~inline_array(); + } else { + hvector.~heap_vector(); + } + + if (rhs.is_inline()) { + new (&iarray) inline_array(std::move(rhs.iarray)); + } else { + new (&hvector) heap_vector(std::move(rhs.hvector)); + } + return *this; + } + + bool is_inline() const { + // It is undefined behavior to access the inactive member of a + // union directly. However, it is well-defined to reinterpret_cast any + // pointer into a pointer to char and examine it as an array + // of bytes. See + // https://dev-discuss.pytorch.org/t/unionizing-for-profit-how-to-exploit-the-power-of-unions-in-c/444#the-memcpy-loophole-4 + bool result = false; + static_assert(offsetof(inline_array, is_inline) == 0, + "untagged union implementation relies on this"); + static_assert(offsetof(heap_vector, is_inline) == 0, + "untagged union implementation relies on this"); + std::memcpy(&result, reinterpret_cast(this), sizeof(bool)); + return result; + } +}; + +template +struct small_vector { +public: + small_vector() = default; + + // Disable copy ctor and assignment. + small_vector(const small_vector &) = delete; + small_vector &operator=(const small_vector &) = delete; + small_vector(small_vector &&) noexcept = default; + small_vector &operator=(small_vector &&) noexcept = default; + + std::size_t size() const { + if (is_inline()) { + return m_repr.iarray.size; + } + return m_repr.hvector.vec.size(); + } + + T const *data() const { + if (is_inline()) { + return m_repr.iarray.arr.data(); + } + return m_repr.hvector.vec.data(); + } + + T &operator[](std::size_t idx) { + assert(idx < size()); + if (is_inline()) { + return m_repr.iarray.arr[idx]; + } + return m_repr.hvector.vec[idx]; + } + + T const &operator[](std::size_t idx) const { + assert(idx < size()); + if (is_inline()) { + return m_repr.iarray.arr[idx]; + } + return m_repr.hvector.vec[idx]; + } + + void push_back(const T &x) { emplace_back(x); } + + void push_back(T &&x) { emplace_back(std::move(x)); } + + template + void emplace_back(Args &&...x) { + if (is_inline()) { + auto &ha = m_repr.iarray; + if (ha.size == InlineSize) { + move_to_heap_vector_with_reserved_size(InlineSize + 1); + m_repr.hvector.vec.emplace_back(std::forward(x)...); + } else { + ha.arr[ha.size++] = T(std::forward(x)...); + } + } else { + m_repr.hvector.vec.emplace_back(std::forward(x)...); + } + } + + void reserve(std::size_t sz) { + if (is_inline()) { + if (sz > InlineSize) { + move_to_heap_vector_with_reserved_size(sz); + } + } else { + reserve_slow_path(sz); + } + } + +private: + using repr_type = inline_array_or_vector; + repr_type m_repr; + + PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) { + assert(is_inline()); + auto &ha = m_repr.iarray; + using heap_vector = typename repr_type::heap_vector; + heap_vector hv; + hv.vec.reserve(reserved_size); + static_assert(std::is_nothrow_move_constructible::value, + "this conversion is not exception safe"); + static_assert(std::is_nothrow_move_constructible::value, + "this conversion is not exception safe"); + std::move(ha.arr.begin(), ha.arr.begin() + ha.size, std::back_inserter(hv.vec)); + new (&m_repr.hvector) heap_vector(std::move(hv)); + } + + PYBIND11_NOINLINE void reserve_slow_path(std::size_t sz) { m_repr.hvector.vec.reserve(sz); } + + bool is_inline() const { return m_repr.is_inline(); } +}; + +// Container to avoid heap allocation for kRequestedInlineSize or fewer booleans. +template +struct small_vector { +private: +public: + small_vector() = default; + + // Disable copy ctor and assignment. + small_vector(const small_vector &) = delete; + small_vector &operator=(const small_vector &) = delete; + small_vector(small_vector &&) noexcept = default; + small_vector &operator=(small_vector &&) noexcept = default; + + small_vector(std::size_t count, bool value) { + if (count > kInlineSize) { + new (&m_repr.hvector) typename repr_type::heap_vector(count, value); + } else { + auto &inline_arr = m_repr.iarray; + inline_arr.arr.fill(value ? static_cast(-1) : 0); + inline_arr.size = static_cast(count); + } + } + + std::size_t size() const { + if (is_inline()) { + return m_repr.iarray.size; + } + return m_repr.hvector.vec.size(); + } + + void reserve(std::size_t sz) { + if (is_inline()) { + if (sz > kInlineSize) { + move_to_heap_vector_with_reserved_size(sz); + } + } else { + m_repr.hvector.vec.reserve(sz); + } + } + + bool operator[](std::size_t idx) const { + if (is_inline()) { + return inline_index(idx); + } + assert(idx < m_repr.hvector.vec.size()); + return m_repr.hvector.vec[idx]; + } + + void push_back(bool b) { + if (is_inline()) { + auto &ha = m_repr.iarray; + if (ha.size == kInlineSize) { + move_to_heap_vector_with_reserved_size(kInlineSize + 1); + push_back_slow_path(b); + } else { + assert(ha.size < kInlineSize); + const auto wbi = word_and_bit_index(ha.size++); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + if (b) { + ha.arr[wbi.word] |= (static_cast(1) << wbi.bit); + } else { + ha.arr[wbi.word] &= ~(static_cast(1) << wbi.bit); + } + assert(operator[](ha.size - 1) == b); + } + } else { + push_back_slow_path(b); + } + } + + void set(std::size_t idx, bool value = true) { + if (is_inline()) { + auto &ha = m_repr.iarray; + assert(ha.size < kInlineSize); + const auto wbi = word_and_bit_index(idx); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + if (value) { + ha.arr[wbi.word] |= (static_cast(1) << wbi.bit); + } else { + ha.arr[wbi.word] &= ~(static_cast(1) << wbi.bit); + } + } else { + m_repr.hvector.vec[idx] = value; + } + } + + void swap(small_vector &rhs) noexcept { std::swap(m_repr, rhs.m_repr); } + +private: + struct WordAndBitIndex { + std::size_t word; + std::size_t bit; + }; + + static WordAndBitIndex word_and_bit_index(std::size_t idx) { + return WordAndBitIndex{idx / kBitsPerWord, idx % kBitsPerWord}; + } + + bool inline_index(std::size_t idx) const { + const auto wbi = word_and_bit_index(idx); + assert(wbi.word < kWords); + assert(wbi.bit < kBitsPerWord); + return m_repr.iarray.arr[wbi.word] & (static_cast(1) << wbi.bit); + } + + PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) { + auto &inline_arr = m_repr.iarray; + using heap_vector = typename repr_type::heap_vector; + heap_vector hv; + hv.vec.reserve(reserved_size); + for (std::size_t ii = 0; ii < inline_arr.size; ++ii) { + hv.vec.push_back(inline_index(ii)); + } + new (&m_repr.hvector) heap_vector(std::move(hv)); + } + + PYBIND11_NOINLINE void push_back_slow_path(bool b) { m_repr.hvector.vec.push_back(b); } + + static constexpr auto kBitsPerWord = 8 * sizeof(std::size_t); + static constexpr auto kWords = (kRequestedInlineSize + kBitsPerWord - 1) / kBitsPerWord; + static constexpr auto kInlineSize = kWords * kBitsPerWord; + + using repr_type = inline_array_or_vector; + repr_type m_repr; + + bool is_inline() const { return m_repr.is_inline(); } +}; + +// Container to avoid heap allocation for N or fewer arguments. +template +using argument_vector = small_vector; + +// Container to avoid heap allocation for N or fewer booleans. +template +using args_convert_vector = small_vector; + +/// A small_vector of PyObject* that holds references and releases them on destruction. +/// This provides explicit ownership semantics without relying on py::object's +/// destructor, and avoids the need for reinterpret_cast when passing to vectorcall. +template +class ref_small_vector { +public: + ref_small_vector() = default; + + ~ref_small_vector() { + for (std::size_t i = 0; i < m_ptrs.size(); ++i) { + Py_XDECREF(m_ptrs[i]); + } + } + + // Disable copy (prevent accidental double-decref) + ref_small_vector(const ref_small_vector &) = delete; + ref_small_vector &operator=(const ref_small_vector &) = delete; + + // Move is allowed + ref_small_vector(ref_small_vector &&other) noexcept : m_ptrs(std::move(other.m_ptrs)) { + // other.m_ptrs is now empty, so its destructor won't decref anything + } + + ref_small_vector &operator=(ref_small_vector &&other) noexcept { + if (this != &other) { + // Decref our current contents + for (std::size_t i = 0; i < m_ptrs.size(); ++i) { + Py_XDECREF(m_ptrs[i]); + } + m_ptrs = std::move(other.m_ptrs); + } + return *this; + } + + /// Add a pointer, taking ownership (no incref, will decref on destruction) + void push_back_steal(PyObject *p) { m_ptrs.push_back(p); } + + /// Add a pointer, borrowing (increfs now, will decref on destruction) + void push_back_borrow(PyObject *p) { + Py_XINCREF(p); + m_ptrs.push_back(p); + } + + /// Add a null pointer (for PY_VECTORCALL_ARGUMENTS_OFFSET slot) + void push_back_null() { m_ptrs.push_back(nullptr); } + + void reserve(std::size_t sz) { m_ptrs.reserve(sz); } + + std::size_t size() const { return m_ptrs.size(); } + + PyObject *operator[](std::size_t idx) const { return m_ptrs[idx]; } + + PyObject *const *data() const { return m_ptrs.data(); } + +private: + small_vector m_ptrs; +}; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/detail/class.h b/pybind11/include/pybind11/detail/class.h index cd7e87f..8b9d0b8 100644 --- a/pybind11/include/pybind11/detail/class.h +++ b/pybind11/include/pybind11/detail/class.h @@ -71,7 +71,7 @@ inline PyTypeObject *make_static_property_type() { issue no Python C API calls which could potentially invoke the garbage collector (the GC will call type_traverse(), which will in turn find the newly constructed type in an invalid state) */ - auto *heap_type = (PyHeapTypeObject *) PyType_Type.tp_alloc(&PyType_Type, 0); + auto *heap_type = reinterpret_cast(PyType_Type.tp_alloc(&PyType_Type, 0)); if (!heap_type) { pybind11_fail("make_static_property_type(): error allocating type!"); } @@ -98,7 +98,7 @@ inline PyTypeObject *make_static_property_type() { pybind11_fail("make_static_property_type(): failure in PyType_Ready()!"); } - setattr((PyObject *) type, "__module__", str(PYBIND11_DUMMY_MODULE_NAME)); + setattr(reinterpret_cast(type), "__module__", str(PYBIND11_DUMMY_MODULE_NAME)); PYBIND11_SET_OLDPY_QUALNAME(type, name_obj); return type; @@ -207,7 +207,7 @@ extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, P /// Cleanup the type-info for a pybind11-registered type. extern "C" inline void pybind11_meta_dealloc(PyObject *obj) { - with_internals([obj](internals &internals) { + with_internals_if_internals([obj](internals &internals) { auto *type = (PyTypeObject *) obj; // A pybind11-registered type will: @@ -221,10 +221,19 @@ extern "C" inline void pybind11_meta_dealloc(PyObject *obj) { auto tindex = std::type_index(*tinfo->cpptype); internals.direct_conversions.erase(tindex); + auto &local_internals = get_local_internals(); if (tinfo->module_local) { - get_local_internals().registered_types_cpp.erase(tindex); + local_internals.registered_types_cpp.erase(tinfo->cpptype); } else { internals.registered_types_cpp.erase(tindex); +#if PYBIND11_INTERNALS_VERSION >= 12 + internals.registered_types_cpp_fast.erase(tinfo->cpptype); + for (const std::type_info *alias : tinfo->alias_chain) { + auto num_erased = internals.registered_types_cpp_fast.erase(alias); + (void) num_erased; + assert(num_erased > 0); + } +#endif } internals.registered_types_py.erase(tinfo->type); @@ -256,7 +265,7 @@ inline PyTypeObject *make_default_metaclass() { issue no Python C API calls which could potentially invoke the garbage collector (the GC will call type_traverse(), which will in turn find the newly constructed type in an invalid state) */ - auto *heap_type = (PyHeapTypeObject *) PyType_Type.tp_alloc(&PyType_Type, 0); + auto *heap_type = reinterpret_cast(PyType_Type.tp_alloc(&PyType_Type, 0)); if (!heap_type) { pybind11_fail("make_default_metaclass(): error allocating metaclass!"); } @@ -282,7 +291,7 @@ inline PyTypeObject *make_default_metaclass() { pybind11_fail("make_default_metaclass(): failure in PyType_Ready()!"); } - setattr((PyObject *) type, "__module__", str(PYBIND11_DUMMY_MODULE_NAME)); + setattr(reinterpret_cast(type), "__module__", str(PYBIND11_DUMMY_MODULE_NAME)); PYBIND11_SET_OLDPY_QUALNAME(type, name_obj); return type; @@ -297,7 +306,7 @@ inline void traverse_offset_bases(void *valueptr, instance *self, bool (*f)(void * /*parentptr*/, instance * /*self*/)) { for (handle h : reinterpret_borrow(tinfo->type->tp_bases)) { - if (auto *parent_tinfo = get_type_info((PyTypeObject *) h.ptr())) { + if (auto *parent_tinfo = get_type_info(reinterpret_cast(h.ptr()))) { for (auto &c : parent_tinfo->implicit_casts) { if (c.first == tinfo->cpptype) { auto *parentptr = c.second(valueptr); @@ -314,8 +323,9 @@ inline void traverse_offset_bases(void *valueptr, #ifdef Py_GIL_DISABLED inline void enable_try_inc_ref(PyObject *obj) { - // TODO: Replace with PyUnstable_Object_EnableTryIncRef when available. - // See https://github.com/python/cpython/issues/128844 +# if PY_VERSION_HEX >= 0x030E00A4 + PyUnstable_EnableTryIncRef(obj); +# else if (_Py_IsImmortal(obj)) { return; } @@ -330,10 +340,12 @@ inline void enable_try_inc_ref(PyObject *obj) { return; } } +# endif } #endif inline bool register_instance_impl(void *ptr, instance *self) { + assert(ptr); #ifdef Py_GIL_DISABLED enable_try_inc_ref(reinterpret_cast(self)); #endif @@ -341,6 +353,7 @@ inline bool register_instance_impl(void *ptr, instance *self) { return true; // unused, but gives the same signature as the deregister func } inline bool deregister_instance_impl(void *ptr, instance *self) { + assert(ptr); return with_instance_map(ptr, [&](instance_map &instances) { auto range = instances.equal_range(ptr); for (auto it = range.first; it != range.second; ++it) { @@ -490,6 +503,17 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) { PyObject_GC_UnTrack(self); } +#if PY_VERSION_HEX >= 0x030D0000 + // PyObject_ClearManagedDict() is available from Python 3.13+. It must be + // called before tp_free() because on Python 3.14+ tp_free no longer + // implicitly clears the managed dict, which would abandon the refcounts of + // objects stored in __dict__ of py::dynamic_attr() types, causing permanent + // memory leaks. + if (PyType_HasFeature(type, Py_TPFLAGS_MANAGED_DICT)) { + PyObject_ClearManagedDict(self); + } +#endif + clear_instance(self); type->tp_free(self); @@ -517,7 +541,7 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) { issue no Python C API calls which could potentially invoke the garbage collector (the GC will call type_traverse(), which will in turn find the newly constructed type in an invalid state) */ - auto *heap_type = (PyHeapTypeObject *) metaclass->tp_alloc(metaclass, 0); + auto *heap_type = reinterpret_cast(metaclass->tp_alloc(metaclass, 0)); if (!heap_type) { pybind11_fail("make_object_base_type(): error allocating type!"); } @@ -544,17 +568,20 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) { pybind11_fail("PyType_Ready failed in make_object_base_type(): " + error_string()); } - setattr((PyObject *) type, "__module__", str(PYBIND11_DUMMY_MODULE_NAME)); + setattr(reinterpret_cast(type), "__module__", str(PYBIND11_DUMMY_MODULE_NAME)); PYBIND11_SET_OLDPY_QUALNAME(type, name_obj); assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)); - return (PyObject *) heap_type; + return reinterpret_cast(heap_type); } /// dynamic_attr: Allow the garbage collector to traverse the internal instance `__dict__`. extern "C" inline int pybind11_traverse(PyObject *self, visitproc visit, void *arg) { #if PY_VERSION_HEX >= 0x030D0000 - PyObject_VisitManagedDict(self, visit, arg); + int ret = PyObject_VisitManagedDict(self, visit, arg); + if (ret) { + return ret; + } #else PyObject *&dict = *_PyObject_GetDictPtr(self); Py_VISIT(dict); @@ -733,7 +760,7 @@ inline PyObject *make_new_python_type(const type_record &rec) { /* Allocate memory for docstring (Python will free this later on) */ size_t size = std::strlen(rec.doc) + 1; #if PY_VERSION_HEX >= 0x030D0000 - tp_doc = (char *) PyMem_MALLOC(size); + tp_doc = static_cast(PyMem_MALLOC(size)); #else tp_doc = (char *) PyObject_MALLOC(size); #endif @@ -748,10 +775,10 @@ inline PyObject *make_new_python_type(const type_record &rec) { issue no Python C API calls which could potentially invoke the garbage collector (the GC will call type_traverse(), which will in turn find the newly constructed type in an invalid state) */ - auto *metaclass - = rec.metaclass.ptr() ? (PyTypeObject *) rec.metaclass.ptr() : internals.default_metaclass; + auto *metaclass = rec.metaclass.ptr() ? reinterpret_cast(rec.metaclass.ptr()) + : internals.default_metaclass; - auto *heap_type = (PyHeapTypeObject *) metaclass->tp_alloc(metaclass, 0); + auto *heap_type = reinterpret_cast(metaclass->tp_alloc(metaclass, 0)); if (!heap_type) { pybind11_fail(std::string(rec.name) + ": Unable to create type object!"); } @@ -764,7 +791,7 @@ inline PyObject *make_new_python_type(const type_record &rec) { auto *type = &heap_type->ht_type; type->tp_name = full_name; type->tp_doc = tp_doc; - type->tp_base = type_incref((PyTypeObject *) base); + type->tp_base = type_incref(reinterpret_cast(base)); type->tp_basicsize = static_cast(sizeof(instance)); if (!bases.empty()) { type->tp_bases = bases.release().ptr(); @@ -805,18 +832,18 @@ inline PyObject *make_new_python_type(const type_record &rec) { /* Register type with the parent scope */ if (rec.scope) { - setattr(rec.scope, rec.name, (PyObject *) type); + setattr(rec.scope, rec.name, reinterpret_cast(type)); } else { Py_INCREF(type); // Keep it alive forever (reference leak) } if (module_) { // Needed by pydoc - setattr((PyObject *) type, "__module__", module_); + setattr(reinterpret_cast(type), "__module__", module_); } PYBIND11_SET_OLDPY_QUALNAME(type, qualname); - return (PyObject *) type; + return reinterpret_cast(type); } PYBIND11_NAMESPACE_END(detail) diff --git a/pybind11/include/pybind11/detail/common.h b/pybind11/include/pybind11/detail/common.h index 314dba4..38cb01d 100644 --- a/pybind11/include/pybind11/detail/common.h +++ b/pybind11/include/pybind11/detail/common.h @@ -19,7 +19,7 @@ /* -- start version constants -- */ #define PYBIND11_VERSION_MAJOR 3 #define PYBIND11_VERSION_MINOR 0 -#define PYBIND11_VERSION_MICRO 1 +#define PYBIND11_VERSION_MICRO 4 // ALPHA = 0xA, BETA = 0xB, GAMMA = 0xC (release candidate), FINAL = 0xF (stable release) // - The release level is set to "alpha" for development versions. // Use 0xA0 (LEVEL=0xA, SERIAL=0) for development versions. @@ -27,7 +27,7 @@ #define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL #define PYBIND11_VERSION_RELEASE_SERIAL 0 // String version of (micro, release level, release serial), e.g.: 0a0, 0b1, 0rc1, 0 -#define PYBIND11_VERSION_PATCH 1 +#define PYBIND11_VERSION_PATCH 4 /* -- end version constants -- */ #if !defined(Py_PACK_FULL_VERSION) @@ -87,7 +87,7 @@ # endif #endif -#if defined(__cpp_lib_launder) && !(defined(_MSC_VER) && (_MSC_VER < 1914)) +#if defined(__cpp_lib_launder) && !(defined(_MSC_VER) && (_MSC_VER < 1920)) // See PR #5968 # define PYBIND11_STD_LAUNDER std::launder # define PYBIND11_HAS_STD_LAUNDER 1 #else @@ -103,6 +103,10 @@ # define PYBIND11_DTOR_CONSTEXPR #endif +#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include() +# define PYBIND11_HAS_STD_BARRIER 1 +#endif + // Compiler version assertions #if defined(__INTEL_COMPILER) # if __INTEL_COMPILER < 1800 @@ -163,6 +167,14 @@ # define PYBIND11_NOINLINE __attribute__((noinline)) inline #endif +#if defined(_MSC_VER) +# define PYBIND11_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) +# define PYBIND11_ALWAYS_INLINE __attribute__((__always_inline__)) inline +#else +# define PYBIND11_ALWAYS_INLINE inline +#endif + #if defined(__MINGW32__) // For unknown reasons all PYBIND11_DEPRECATED member trigger a warning when declared // whether it is used or not @@ -248,6 +260,10 @@ # define PYBIND11_HAS_U8STRING 1 #endif +#if defined(PYBIND11_CPP20) && defined(__cpp_lib_span) && __cpp_lib_span >= 202002L +# define PYBIND11_HAS_SPAN 1 +#endif + // See description of PR #4246: #if !defined(PYBIND11_NO_ASSERT_GIL_HELD_INCREF_DECREF) && !defined(NDEBUG) \ && !defined(PYPY_VERSION) && !defined(PYBIND11_ASSERT_GIL_HELD_INCREF_DECREF) @@ -322,6 +338,13 @@ #define PYBIND11_BYTES_AS_STRING PyBytes_AsString #define PYBIND11_BYTES_SIZE PyBytes_Size #define PYBIND11_LONG_CHECK(o) PyLong_Check(o) +// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`, +// while CPython only considers the existence of `nb_index`/`__index__`. +#if !defined(PYPY_VERSION) +# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o) +#else +# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__") +#endif #define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o) #define PYBIND11_LONG_FROM_SIGNED(o) PyLong_FromSsize_t((ssize_t) (o)) #define PYBIND11_LONG_FROM_UNSIGNED(o) PyLong_FromSize_t((size_t) (o)) @@ -430,30 +453,35 @@ Note that this is run once for each (sub-)interpreter the module is imported int possibly concurrently. The PyModuleDef is allowed to be static, but the PyObject* resulting from PyModuleDef_Init should be treated like any other PyObject (so not shared across interpreters). */ -#define PYBIND11_MODULE_PYINIT(name, pre_init, ...) \ +#define PYBIND11_MODULE_PYINIT(name, ...) \ static int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject *); \ PYBIND11_PLUGIN_IMPL(name) { \ PYBIND11_CHECK_PYTHON_VERSION \ - pre_init; \ - PYBIND11_ENSURE_INTERNALS_READY \ - static ::pybind11::detail::slots_array mod_def_slots = ::pybind11::detail::init_slots( \ - &PYBIND11_CONCAT(pybind11_exec_, name), ##__VA_ARGS__); \ - static PyModuleDef def{/* m_base */ PyModuleDef_HEAD_INIT, \ - /* m_name */ PYBIND11_TOSTRING(name), \ - /* m_doc */ nullptr, \ - /* m_size */ 0, \ - /* m_methods */ nullptr, \ - /* m_slots */ mod_def_slots.data(), \ - /* m_traverse */ nullptr, \ - /* m_clear */ nullptr, \ - /* m_free */ nullptr}; \ - return PyModuleDef_Init(&def); \ + try { \ + pybind11::detail::ensure_internals(); \ + static ::pybind11::detail::slots_array mod_def_slots \ + = ::pybind11::detail::init_slots(&PYBIND11_CONCAT(pybind11_exec_, name), \ + ##__VA_ARGS__); \ + static PyModuleDef def{/* m_base */ PyModuleDef_HEAD_INIT, \ + /* m_name */ PYBIND11_TOSTRING(name), \ + /* m_doc */ nullptr, \ + /* m_size */ 0, \ + /* m_methods */ nullptr, \ + /* m_slots */ mod_def_slots.data(), \ + /* m_traverse */ nullptr, \ + /* m_clear */ nullptr, \ + /* m_free */ nullptr}; \ + return PyModuleDef_Init(&def); \ + } \ + PYBIND11_CATCH_INIT_EXCEPTIONS \ + return nullptr; \ } #define PYBIND11_MODULE_EXEC(name, variable) \ static void PYBIND11_CONCAT(pybind11_init_, name)(::pybind11::module_ &); \ int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject * pm) { \ try { \ + pybind11::detail::ensure_internals(); \ auto m = pybind11::reinterpret_borrow<::pybind11::module_>(pm); \ if (!pybind11::detail::get_cached_module(m.attr("__spec__").attr("name"))) { \ PYBIND11_CONCAT(pybind11_init_, name)(m); \ @@ -507,8 +535,7 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across \endrst */ #define PYBIND11_MODULE(name, variable, ...) \ - PYBIND11_MODULE_PYINIT( \ - name, (pybind11::detail::get_num_interpreters_seen() += 1), ##__VA_ARGS__) \ + PYBIND11_MODULE_PYINIT(name, ##__VA_ARGS__) \ PYBIND11_MODULE_EXEC(name, variable) // pop gnu-zero-variadic-macro-arguments @@ -579,14 +606,10 @@ enum class return_value_policy : uint8_t { PYBIND11_NAMESPACE_BEGIN(detail) -inline static constexpr int log2(size_t n, int k = 0) { - return (n <= 1) ? k : log2(n >> 1, k + 1); -} +static constexpr int log2(size_t n, int k = 0) { return (n <= 1) ? k : log2(n >> 1, k + 1); } // Returns the size as a multiple of sizeof(void *), rounded up. -inline static constexpr size_t size_in_ptrs(size_t s) { - return 1 + ((s - 1) >> log2(sizeof(void *))); -} +static constexpr size_t size_in_ptrs(size_t s) { return 1 + ((s - 1) >> log2(sizeof(void *))); } /** * The space to allocate for simple layout instance holders (see below) in multiple of the size of @@ -1016,6 +1039,17 @@ struct is_instantiation> : std::true_type {}; template using is_shared_ptr = is_instantiation; +/// Detects whether static_cast(Base*) is valid, i.e. the inheritance is non-virtual. +/// Used to detect virtual bases: if this is false, pointer adjustments require the implicit_casts +/// chain rather than reinterpret_cast. +template +struct is_static_downcastable : std::false_type {}; +template +struct is_static_downcastable(std::declval()))>> + : std::true_type {}; + /// Check if T looks like an input iterator template struct is_input_iterator : std::false_type {}; @@ -1038,14 +1072,30 @@ struct strip_function_object { using type = typename remove_class::type; }; +// Strip noexcept from a free function type (C++17: noexcept is part of the type). +template +struct remove_noexcept { + using type = T; +}; +#ifdef __cpp_noexcept_function_type +template +struct remove_noexcept { + using type = R(A...); +}; +#endif +template +using remove_noexcept_t = typename remove_noexcept::type; + // Extracts the function signature from a function, function pointer or lambda. +// Strips noexcept from the result so that factory/pickle_factory partial specializations +// (which match plain Return(Args...)) work correctly with noexcept callables (issue #2234). template > -using function_signature_t = conditional_t< +using function_signature_t = remove_noexcept_t::value, F, typename conditional_t::value || std::is_member_pointer::value, std::remove_pointer, - strip_function_object>::type>; + strip_function_object>::type>>; /// Returns true if the type looks like a lambda: that is, isn't a function, pointer or member /// pointer. Note that this can catch all sorts of other things, too; this is intended to be used @@ -1194,6 +1244,36 @@ struct overload_cast_impl { -> decltype(pmf) { return pmf; } + + // Define const/non-const member-pointer selector pairs for qualifier combinations. + // The `qualifiers` parameter is used in type position, where extra parentheses are invalid. + // NOLINTBEGIN(bugprone-macro-parentheses) +#define PYBIND11_OVERLOAD_CAST_MEMBER_PTR(qualifiers) \ + template \ + constexpr auto operator()(Return (Class::*pmf)(Args...) qualifiers, std::false_type = {}) \ + const noexcept -> decltype(pmf) { \ + return pmf; \ + } \ + template \ + constexpr auto operator()(Return (Class::*pmf)(Args...) const qualifiers, std::true_type) \ + const noexcept -> decltype(pmf) { \ + return pmf; \ + } + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(&) + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(&&) + +#ifdef __cpp_noexcept_function_type + template + constexpr auto operator()(Return (*pf)(Args...) noexcept) const noexcept -> decltype(pf) { + return pf; + } + + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(noexcept) + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(& noexcept) + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(&& noexcept) +#endif +#undef PYBIND11_OVERLOAD_CAST_MEMBER_PTR + // NOLINTEND(bugprone-macro-parentheses) }; PYBIND11_NAMESPACE_END(detail) @@ -1293,8 +1373,7 @@ template #if defined(_MSC_VER) && _MSC_VER < 1920 // MSVC 2017 constexpr #endif - inline void - silence_unused_warnings(Args &&...) { + inline void silence_unused_warnings(Args &&...) { } // MSVC warning C4100: Unreferenced formal parameter diff --git a/pybind11/include/pybind11/detail/cpp_conduit.h b/pybind11/include/pybind11/detail/cpp_conduit.h index a06b9b2..49c199e 100644 --- a/pybind11/include/pybind11/detail/cpp_conduit.h +++ b/pybind11/include/pybind11/detail/cpp_conduit.h @@ -21,13 +21,13 @@ inline bool type_is_managed_by_our_internals(PyTypeObject *type_obj) { return bool(internals.registered_types_py.find(type_obj) != internals.registered_types_py.end()); #else - return bool(type_obj->tp_new == pybind11_object_new); + return (type_obj->tp_new == pybind11_object_new); #endif } inline bool is_instance_method_of_type(PyTypeObject *type_obj, PyObject *attr_name) { PyObject *descr = _PyType_Lookup(type_obj, attr_name); - return bool((descr != nullptr) && PyInstanceMethod_Check(descr)); + return ((descr != nullptr) && PyInstanceMethod_Check(descr)); } inline object try_get_cpp_conduit_method(PyObject *obj) { diff --git a/pybind11/include/pybind11/detail/function_record_pyobject.h b/pybind11/include/pybind11/detail/function_record_pyobject.h index d9c5bad..94d27ad 100644 --- a/pybind11/include/pybind11/detail/function_record_pyobject.h +++ b/pybind11/include/pybind11/detail/function_record_pyobject.h @@ -91,6 +91,7 @@ static PyType_Spec function_record_PyType_Spec function_record_PyType_Slots}; inline PyTypeObject *get_function_record_PyTypeObject() { + PYBIND11_LOCK_INTERNALS(get_internals()); PyTypeObject *&py_type_obj = detail::get_local_internals().function_record_py_type; if (!py_type_obj) { PyObject *py_obj = PyType_FromSpec(&function_record_PyType_Spec); @@ -125,7 +126,7 @@ inline bool is_function_record_PyObject(PyObject *obj) { inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { if (is_function_record_PyObject(obj)) { - return ((detail::function_record_PyObject *) obj)->cpp_func_rec; + return (reinterpret_cast(obj))->cpp_func_rec; } return nullptr; } @@ -136,7 +137,7 @@ inline object function_record_PyObject_New() { throw error_already_set(); } py_func_rec->cpp_func_rec = nullptr; // For clarity/purity. Redundant in practice. - return reinterpret_steal((PyObject *) py_func_rec); + return reinterpret_steal(reinterpret_cast(py_func_rec)); } PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) diff --git a/pybind11/include/pybind11/detail/holder_caster_foreign_helpers.h b/pybind11/include/pybind11/detail/holder_caster_foreign_helpers.h new file mode 100644 index 0000000..cae571b --- /dev/null +++ b/pybind11/include/pybind11/detail/holder_caster_foreign_helpers.h @@ -0,0 +1,104 @@ +/* + pybind11/detail/holder_caster_foreign_helpers.h: Logic to implement + set_foreign_holder() in copyable_ and movable_holder_caster. + + Copyright (c) 2025 Hudson River Trading LLC + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include + +#include "common.h" + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +struct holder_caster_foreign_helpers { + struct py_deleter { + void operator()(const void *) const noexcept { + // Don't run the deleter if the interpreter has been shut down + if (Py_IsInitialized() == 0) { + return; + } + gil_scoped_acquire guard; + Py_DECREF(o); + } + + PyObject *o; + }; + + // Downcast shared_ptr from the enable_shared_from_this base to the target type. + // SFINAE probe: use static_pointer_cast when the static downcast is valid (common case), + // fall back to dynamic_pointer_cast when it isn't (virtual inheritance โ€” issue #5989). + // We can't use dynamic_pointer_cast unconditionally because it requires polymorphic types; + // we can't use is_polymorphic to choose because that's orthogonal to virtual inheritance. + // (The implementation uses the "tag dispatch via overload priority" trick.) + template + static auto esft_downcast(const std::shared_ptr &existing, int /*preferred*/) + -> decltype(static_cast(std::declval()), std::shared_ptr()) { + return std::static_pointer_cast(existing); + } + + template + static std::shared_ptr esft_downcast(const std::shared_ptr &existing, + ... /*fallback*/) { + return std::dynamic_pointer_cast(existing); + } + + template + static auto set_via_shared_from_this(type *value, std::shared_ptr *holder_out) + -> decltype(value->shared_from_this(), bool()) { + // object derives from enable_shared_from_this; + // try to reuse an existing shared_ptr if one is known + if (auto existing = try_get_shared_from_this(value)) { + *holder_out = esft_downcast(existing, 0); + return true; + } + return false; + } + + template + static bool set_via_shared_from_this(void *, std::shared_ptr *) { + return false; + } + + template + static bool set_foreign_holder(handle src, type *value, std::shared_ptr *holder_out) { + // We only support using std::shared_ptr for foreign T, and + // it's done by creating a new shared_ptr control block that + // owns a reference to the original Python object. + if (value == nullptr) { + *holder_out = {}; + return true; + } + if (set_via_shared_from_this(value, holder_out)) { + return true; + } + *holder_out = std::shared_ptr(value, py_deleter{src.inc_ref().ptr()}); + return true; + } + + template + static bool + set_foreign_holder(handle src, const type *value, std::shared_ptr *holder_out) { + std::shared_ptr holder_mut; + if (set_foreign_holder(src, const_cast(value), &holder_mut)) { + *holder_out = holder_mut; + return true; + } + return false; + } + + template + static bool set_foreign_holder(handle, type *, ...) { + throw cast_error("Unable to cast foreign type to held instance -- " + "only std::shared_ptr is supported in this case"); + } +}; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/detail/init.h b/pybind11/include/pybind11/detail/init.h index 9589d74..a1083f8 100644 --- a/pybind11/include/pybind11/detail/init.h +++ b/pybind11/include/pybind11/detail/init.h @@ -304,11 +304,10 @@ struct constructor { extra...); } - template < - typename Class, - typename... Extra, - enable_if_t, Args...>::value, int> - = 0> + template , Args...>::value, + int> = 0> static void execute(Class &cl, const Extra &...extra) { cl.def( "__init__", @@ -325,11 +324,10 @@ struct constructor { extra...); } - template < - typename Class, - typename... Extra, - enable_if_t, Args...>::value, int> - = 0> + template , Args...>::value, + int> = 0> static void execute(Class &cl, const Extra &...extra) { cl.def( "__init__", @@ -345,11 +343,10 @@ struct constructor { // Implementing class for py::init_alias<...>() template struct alias_constructor { - template < - typename Class, - typename... Extra, - enable_if_t, Args...>::value, int> - = 0> + template , Args...>::value, + int> = 0> static void execute(Class &cl, const Extra &...extra) { cl.def( "__init__", @@ -370,8 +367,15 @@ template ` = `void_type()`, which the dual-factory +// specialization can also decompose as `AReturn(AArgs...)` with `AReturn=void_type` +// and `AArgs={}`. template -struct factory { +struct factory { remove_reference_t class_factory; // NOLINTNEXTLINE(google-explicit-constructor) @@ -476,9 +480,9 @@ void setstate(value_and_holder &v_h, std::pair &&result, bool need_alias) return; } // Our tests never run into an unset dict, but being careful here for now (see #5658) - auto dict = getattr((PyObject *) v_h.inst, "__dict__", none()); + auto dict = getattr(reinterpret_cast(v_h.inst), "__dict__", none()); if (dict.is_none()) { - setattr((PyObject *) v_h.inst, "__dict__", d); + setattr(reinterpret_cast(v_h.inst), "__dict__", d); } else { // Keep the original object dict and just update it if (PyDict_Update(dict.ptr(), d.ptr()) < 0) { @@ -501,9 +505,15 @@ template struct pickle_factory { - static_assert(std::is_same, intrinsic_t>::value, - "The type returned by `__getstate__` must be the same " - "as the argument accepted by `__setstate__`"); + using Ret = intrinsic_t; + using Arg = intrinsic_t; + + // Subclasses are now allowed for support between type hint and generic versions of types + // (e.g.) typing::List <--> list + static_assert(std::is_same::value || std::is_base_of::value + || std::is_base_of::value, + "The type returned by `__getstate__` must be the same or subclass of the " + "argument accepted by `__setstate__`"); remove_reference_t get; remove_reference_t set; diff --git a/pybind11/include/pybind11/detail/internals.h b/pybind11/include/pybind11/detail/internals.h index 4ea3748..a49cdaa 100644 --- a/pybind11/include/pybind11/detail/internals.h +++ b/pybind11/include/pybind11/detail/internals.h @@ -59,15 +59,13 @@ using ExceptionTranslator = void (*)(std::exception_ptr); # define PYBIND11_TLS_KEY_INIT(var) \ _Pragma("clang diagnostic push") /**/ \ _Pragma("clang diagnostic ignored \"-Wmissing-field-initializers\"") /**/ \ - Py_tss_t var \ - = Py_tss_NEEDS_INIT; \ + Py_tss_t var = Py_tss_NEEDS_INIT; \ _Pragma("clang diagnostic pop") #elif defined(__GNUC__) && !defined(__INTEL_COMPILER) # define PYBIND11_TLS_KEY_INIT(var) \ _Pragma("GCC diagnostic push") /**/ \ _Pragma("GCC diagnostic ignored \"-Wmissing-field-initializers\"") /**/ \ - Py_tss_t var \ - = Py_tss_NEEDS_INIT; \ + Py_tss_t var = Py_tss_NEEDS_INIT; \ _Pragma("GCC diagnostic pop") #else # define PYBIND11_TLS_KEY_INIT(var) Py_tss_t var = Py_tss_NEEDS_INIT; @@ -103,7 +101,7 @@ class thread_specific_storage { // However, in GraalPy (as of v24.2 or older), TSS is implemented by Java and this call // requires a living Python interpreter. #ifdef GRAALVM_PYTHON - if (!Py_IsInitialized() || _Py_IsFinalizing()) { + if (Py_IsInitialized() == 0 || _Py_IsFinalizing() != 0) { return; } #endif @@ -143,6 +141,38 @@ inline PyTypeObject *make_default_metaclass(); inline PyObject *make_object_base_type(PyTypeObject *metaclass); inline void translate_exception(std::exception_ptr p); +inline PyThreadState *get_thread_state_unchecked() { +#if defined(PYPY_VERSION) || defined(GRAALVM_PYTHON) + return PyThreadState_GET(); +#elif PY_VERSION_HEX < 0x030D0000 + return _PyThreadState_UncheckedGet(); +#else + return PyThreadState_GetUnchecked(); +#endif +} + +inline PyInterpreterState *get_interpreter_state_unchecked() { + auto *tstate = get_thread_state_unchecked(); + return tstate ? tstate->interp : nullptr; +} + +inline object get_python_state_dict() { + object state_dict; +#if defined(PYPY_VERSION) || defined(GRAALVM_PYTHON) + state_dict = reinterpret_borrow(PyEval_GetBuiltins()); +#else + auto *istate = get_interpreter_state_unchecked(); + if (istate) { + state_dict = reinterpret_borrow(PyInterpreterState_GetDict(istate)); + } +#endif + if (!state_dict) { + raise_from(PyExc_SystemError, "pybind11::detail::get_python_state_dict() FAILED"); + throw error_already_set(); + } + return state_dict; +} + // Python loads modules by default with dlopen with the RTLD_LOCAL flag; under libc++ and possibly // other STLs, this means `typeid(A)` from one module won't equal `typeid(A)` from another module // even when `A` is the same, non-hidden-visibility type (e.g. from a common include). Under @@ -176,11 +206,17 @@ struct type_equal_to { }; #endif +// For now, we don't bother adding a fancy hash for pointers and just +// let the standard library use the identity hash function if that's +// what it wants to do (e.g., as in libstdc++). +template +using fast_type_map = std::unordered_map; + template using type_map = std::unordered_map; struct override_hash { - inline size_t operator()(const std::pair &v) const { + size_t operator()(const std::pair &v) const { size_t value = std::hash()(v.first); value ^= std::hash()(v.second) + 0x9e3779b9 + (value << 6) + (value >> 2); return value; @@ -192,6 +228,7 @@ using instance_map = std::unordered_multimap; #ifdef Py_GIL_DISABLED // Wrapper around PyMutex to provide BasicLockable semantics class pymutex { + friend class pycritical_section; PyMutex mutex; public: @@ -200,6 +237,37 @@ class pymutex { void unlock() { PyMutex_Unlock(&mutex); } }; +class pycritical_section { + pymutex &mutex; +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 + PyCriticalSection cs; +# endif + +public: + explicit pycritical_section(pymutex &m) : mutex(m) { + // PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 + PyCriticalSection_BeginMutex(&cs, &mutex.mutex); +# else + // Fall back to direct mutex locking for older free-threaded Python versions + mutex.lock(); +# endif + } + ~pycritical_section() { +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 + PyCriticalSection_End(&cs); +# else + mutex.unlock(); +# endif + } + + // Non-copyable and non-movable to prevent double-unlock + pycritical_section(const pycritical_section &) = delete; + pycritical_section &operator=(const pycritical_section &) = delete; + pycritical_section(pycritical_section &&) = delete; + pycritical_section &operator=(pycritical_section &&) = delete; +}; + // Instance map shards are used to reduce mutex contention in free-threaded Python. struct instance_map_shard { instance_map registered_instances; @@ -236,6 +304,15 @@ struct internals { pymutex mutex; pymutex exception_translator_mutex; #endif +#if PYBIND11_INTERNALS_VERSION >= 12 + // non-normative but fast "hint" for registered_types_cpp. Meant + // to be used as the first level of a two-level lookup: successful + // lookups are correct, but unsuccessful lookups need to try + // registered_types_cpp and then backfill this map if they find + // anything. + fast_type_map registered_types_cpp_fast; +#endif + // std::type_index -> pybind11's type information type_map registered_types_cpp; // PyTypeObject* -> base type_info(s) @@ -260,7 +337,9 @@ struct internals { PyObject *instance_base = nullptr; // Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined: thread_specific_storage tstate; - thread_specific_storage loader_life_support_tls; +#if PYBIND11_INTERNALS_VERSION <= 11 + thread_specific_storage loader_life_support_tls; // OBSOLETE (PR #5830) +#endif // Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined: PyInterpreterState *istate = nullptr; @@ -268,11 +347,8 @@ struct internals { internals() : static_property_type(make_static_property_type()), - default_metaclass(make_default_metaclass()) { - PyThreadState *cur_tstate = PyThreadState_Get(); - tstate = cur_tstate; - - istate = cur_tstate->interp; + default_metaclass(make_default_metaclass()), istate(get_interpreter_state_unchecked()) { + tstate.set(nullptr); // See PR #5870 registered_exception_translators.push_front(&translate_exception); #ifdef Py_GIL_DISABLED // Scale proportional to the number of cores. 2x is a heuristic to reduce contention. @@ -301,7 +377,11 @@ struct internals { // impact any other modules, because the only things accessing the local internals is the // module that contains them. struct local_internals { - type_map registered_types_cpp; + // It should be safe to use fast_type_map here because this entire + // data structure is scoped to our single module, and thus a single + // DSO and single instance of type_info for any particular type. + fast_type_map registered_types_cpp; + std::forward_list registered_exception_translators; PyTypeObject *function_record_py_type = nullptr; }; @@ -336,6 +416,20 @@ struct type_info { void *get_buffer_data = nullptr; void *(*module_local_load)(PyObject *, const type_info *) = nullptr; holder_enum_t holder_enum_v = holder_enum_t::undefined; + +#if PYBIND11_INTERNALS_VERSION >= 12 + // When a type appears in multiple DSOs, + // internals::registered_types_cpp_fast will have multiple distinct + // keys (the std::type_info from each DSO) mapped to the same + // detail::type_info*. We need to keep track of these aliases so that we clean + // them up when our type is deallocated. A linked list is appropriate + // because it is expected to be 1) usually empty and 2) + // when it's not empty, usually very small. See also `struct + // nb_alias_chain` added in + // https://github.com/wjakob/nanobind/commit/b515b1f7f2f4ecc0357818e6201c94a9f4cbfdc2 + std::forward_list alias_chain; +#endif + /* A simple type never occurs as a (direct or indirect) parent * of a class that makes use of multiple inheritance. * A type can be simple even if it has non-simple ancestors as long as it has no descendants. @@ -347,6 +441,24 @@ struct type_info { bool module_local : 1; }; +/// Information stored in a capsule on py::native_enum() types. Since we don't +/// create a type_info record for native enums, we must store here any +/// information we will need about the enum at runtime. +/// +/// If you make backward-incompatible changes to this structure, you must +/// change the `attribute_name()` so that native enums from older version of +/// pybind11 don't have their records reinterpreted. Better would be to keep +/// the changes backward-compatible (i.e., only add new fields at the end) +/// and detect/indicate their presence using the currently-unused `version`. +struct native_enum_record { + const std::type_info *cpptype; + uint32_t size_bytes; + bool is_signed; + const uint8_t version = 1; + + static const char *attribute_name() { return "__pybind11_native_enum__"; } +}; + #define PYBIND11_INTERNALS_ID \ "__pybind11_internals_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \ PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE PYBIND11_PLATFORM_ABI_ID "__" @@ -355,21 +467,12 @@ struct type_info { "__pybind11_module_local_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \ PYBIND11_COMPILER_TYPE_LEADING_UNDERSCORE PYBIND11_PLATFORM_ABI_ID "__" -inline PyThreadState *get_thread_state_unchecked() { -#if defined(PYPY_VERSION) || defined(GRAALVM_PYTHON) - return PyThreadState_GET(); -#elif PY_VERSION_HEX < 0x030D0000 - return _PyThreadState_UncheckedGet(); -#else - return PyThreadState_GetUnchecked(); -#endif -} - -/// We use this counter to figure out if there are or have been multiple subinterpreters active at -/// any point. This must never decrease while any interpreter may be running in any thread! -inline std::atomic &get_num_interpreters_seen() { - static std::atomic counter(0); - return counter; +/// We use this to figure out if there are or have been multiple subinterpreters active at any +/// point. This must never go from true to false while any interpreter may be running in any +/// thread! +inline std::atomic_bool &has_seen_non_main_interpreter() { + static std::atomic_bool multi(false); + return multi; } template (PyEval_GetBuiltins()); -#else -# if PY_VERSION_HEX < 0x03090000 - PyInterpreterState *istate = _PyInterpreterState_Get(); -# else - PyInterpreterState *istate = PyInterpreterState_Get(); -# endif - if (istate) { - state_dict = reinterpret_borrow(PyInterpreterState_GetDict(istate)); +// Sentinel value for the `dtor` parameter of `atomic_get_or_create_in_state_dict`. +// Indicates no destructor was explicitly provided (distinct from nullptr, which means "leak"). +#define PYBIND11_DTOR_USE_DELETE (reinterpret_cast(1)) + +// Get or create per-storage capsule in the current interpreter's state dict. +// - The storage is interpreter-dependent: different interpreters will have different storage. +// This is important when using multiple-interpreters, to avoid sharing unshareable objects +// between interpreters. +// - There is one storage per `key` in an interpreter and it is accessible between all extensions +// in the same interpreter. +// - The life span of the storage is tied to the interpreter: it will be kept alive until the +// interpreter shuts down. +// +// Use test-and-set pattern with `PyDict_SetDefault` for thread-safe concurrent access. +// WARNING: There can be multiple threads creating the storage at the same time, while only one +// will succeed in inserting its capsule into the dict. Therefore, the deleter will be +// used to clean up the storage of the unused capsules. +// +// Returns: pair of (pointer to storage, bool indicating if newly created). +// The bool follows std::map::insert convention: true = created, false = existed. +// `dtor`: optional destructor called when the interpreter shuts down. +// - If not provided: the storage will be deleted using `delete`. +// - If nullptr: the storage will be leaked (useful for singletons that outlive the interpreter). +// - If a function: that function will be called with the capsule object. +template +std::pair atomic_get_or_create_in_state_dict(const char *key, + void (*dtor)(PyObject *) + = PYBIND11_DTOR_USE_DELETE) { + error_scope err_scope; // preserve any existing Python error states + + auto state_dict = reinterpret_borrow(get_python_state_dict()); + PyObject *capsule_obj = nullptr; + bool created = false; + + // Try to get existing storage (fast path). + capsule_obj = dict_getitemstring(state_dict.ptr(), key); + if (capsule_obj == nullptr) { + if (PyErr_Occurred()) { + throw error_already_set(); + } + // Storage doesn't exist yet, create a new one. + // Use unique_ptr for exception safety: if capsule creation throws, the storage is + // automatically deleted. + auto storage_ptr = std::unique_ptr(new Payload{}); + auto new_capsule + = capsule(storage_ptr.get(), + // The destructor will be called when the capsule is GC'ed. + // If the insert below fails (entry already in the dict), then this + // destructor will be called on the newly created capsule at the end of this + // function, and we want to just release this memory. + /*destructor=*/[](void *v) { delete static_cast(v); }); + // At this point, the capsule object is created successfully. + // Release the unique_ptr and let the capsule object own the storage to avoid double-free. + (void) storage_ptr.release(); + + // Use `PyDict_SetDefault` for atomic test-and-set: + // - If key doesn't exist, inserts our capsule and returns it. + // - If key exists (another thread inserted first), returns the existing value. + // This is thread-safe because `PyDict_SetDefault` will hold a lock on the dict. + // + // NOTE: Here we use `PyDict_SetDefault` instead of `PyDict_SetDefaultRef` because the + // capsule is kept alive until interpreter shutdown, so we do not need to handle + // incref and decref here. + capsule_obj = dict_setdefaultstring(state_dict.ptr(), key, new_capsule.ptr()); + if (capsule_obj == nullptr) { + throw error_already_set(); + } + created = (capsule_obj == new_capsule.ptr()); + // - If key already existed, our `new_capsule` is not inserted, it will be destructed when + // going out of scope here, and will call the destructor set above. + // - Otherwise, our `new_capsule` is now in the dict, and it owns the storage and the state + // dict will incref it. We need to set the caller's destructor on it, which will be + // called when the interpreter shuts down. + if (created && dtor != PYBIND11_DTOR_USE_DELETE) { + if (PyCapsule_SetDestructor(capsule_obj, dtor) < 0) { + throw error_already_set(); + } + } } -#endif - if (!state_dict) { - raise_from(PyExc_SystemError, "pybind11::detail::get_python_state_dict() FAILED"); + + // Get the storage pointer from the capsule. + void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr); + if (!raw_ptr) { + raise_from(PyExc_SystemError, + "pybind11::detail::atomic_get_or_create_in_state_dict() FAILED"); throw error_already_set(); } - return state_dict; + return std::pair(static_cast(raw_ptr), created); } +#undef PYBIND11_DTOR_USE_DELETE + template class internals_pp_manager { public: using on_fetch_function = void(InternalsType *); - internals_pp_manager(char const *id, on_fetch_function *on_fetch) - : holder_id_(id), on_fetch_(on_fetch) {} + + static internals_pp_manager &get_instance(char const *id, on_fetch_function *on_fetch) { + static internals_pp_manager instance(id, on_fetch); + return instance; + } /// Get the current pointer-to-pointer, allocating it if it does not already exist. May /// acquire the GIL. Will never return nullptr. std::unique_ptr *get_pp() { #ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT - if (get_num_interpreters_seen() > 1) { + if (has_seen_non_main_interpreter()) { // Whenever the interpreter changes on the current thread we need to invalidate the // internals_pp so that it can be pulled from the interpreter's state dict. That is // slow, so we use the current PyThreadState to check if it is necessary. auto *tstate = get_thread_state_unchecked(); - if (!tstate || tstate->interp != last_istate_.get()) { + if (!tstate || tstate->interp != last_istate_tls()) { gil_scoped_acquire_simple gil; if (!tstate) { tstate = get_thread_state_unchecked(); } - last_istate_ = tstate->interp; - internals_tls_p_ = get_or_create_pp_in_state_dict(); + last_istate_tls() = tstate->interp; + internals_p_tls() = get_or_create_pp_in_state_dict(); } - return internals_tls_p_.get(); + return internals_p_tls(); } #endif if (!internals_singleton_pp_) { @@ -534,9 +711,9 @@ class internals_pp_manager { /// Drop all the references we're currently holding. void unref() { #ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT - if (get_num_interpreters_seen() > 1) { - last_istate_.reset(); - internals_tls_p_.reset(); + if (has_seen_non_main_interpreter()) { + last_istate_tls() = nullptr; + internals_p_tls() = nullptr; return; } #endif @@ -545,56 +722,111 @@ class internals_pp_manager { void destroy() { #ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT - if (get_num_interpreters_seen() > 1) { + if (has_seen_non_main_interpreter()) { auto *tstate = get_thread_state_unchecked(); // this could be called without an active interpreter, just use what was cached - if (!tstate || tstate->interp == last_istate_.get()) { - auto tpp = internals_tls_p_.get(); - if (tpp) { - delete tpp; + if (!tstate || tstate->interp == last_istate_tls()) { + auto tpp = internals_p_tls(); + { + std::lock_guard lock(pp_set_mutex_); + pps_have_created_content_.erase(tpp); // untrack deleted pp } + delete tpp; // may call back into Python } unref(); return; } #endif - delete internals_singleton_pp_; + { + std::lock_guard lock(pp_set_mutex_); + pps_have_created_content_.erase(internals_singleton_pp_); // untrack deleted pp + } + delete internals_singleton_pp_; // may call back into Python unref(); } -private: - std::unique_ptr *get_or_create_pp_in_state_dict() { - error_scope err_scope; - dict state_dict = get_python_state_dict(); - auto internals_obj - = reinterpret_steal(dict_getitemstringref(state_dict.ptr(), holder_id_)); - std::unique_ptr *pp = nullptr; - if (internals_obj) { - void *raw_ptr = PyCapsule_GetPointer(internals_obj.ptr(), /*name=*/nullptr); - if (!raw_ptr) { - raise_from(PyExc_SystemError, - "pybind11::detail::internals_pp_manager::get_pp_from_dict() FAILED"); - throw error_already_set(); + void create_pp_content_once(std::unique_ptr *const pp) { + // Assume the GIL is held here. May call back into Python. We cannot hold the lock with our + // mutex here. So there may be multiple threads creating the content at the same time. Only + // one will install its content to pp below. Others will be freed when going out of scope. + auto tmp = std::unique_ptr(new InternalsType()); + + { + // Lock scope must not include Python calls, which may require the GIL and cause + // deadlocks. + std::lock_guard lock(pp_set_mutex_); + + if (*pp) { + // Already created in another thread. + return; } - pp = reinterpret_cast *>(raw_ptr); - if (on_fetch_ && pp) { - on_fetch_(pp->get()); + + // At this point, pp->get() is nullptr. + // The content is either not yet created, or was previously destroyed via pp->reset(). + + // Detect re-creation of internals after destruction during interpreter shutdown. + // If pybind11 code (e.g., tp_traverse/tp_clear calling py::cast) runs after internals + // have been destroyed, a new empty internals would be created, causing type lookup + // failures. See also get_or_create_pp_in_state_dict() comments. + if (pps_have_created_content_.find(pp) != pps_have_created_content_.end()) { + pybind11_fail( + "pybind11::detail::internals_pp_manager::create_pp_content_once() " + "FAILED: reentrant call detected while fetching pybind11 internals!"); } - } else { - pp = new std::unique_ptr; - // NOLINTNEXTLINE(bugprone-casting-through-void) - state_dict[holder_id_] = capsule(reinterpret_cast(pp)); + + // Each interpreter can only create its internals once. + pps_have_created_content_.insert(pp); + // Install the created content. + pp->swap(tmp); + } + } + +private: + internals_pp_manager(char const *id, on_fetch_function *on_fetch) + : holder_id_(id), on_fetch_(on_fetch) {} + + std::unique_ptr *get_or_create_pp_in_state_dict() { + // The `unique_ptr` is intentionally leaked on interpreter shutdown. + // Once an instance is created, it will never be deleted until the process exits (compare + // to interpreter shutdown in multiple-interpreter scenarios). + // We cannot guarantee the destruction order of capsules in the interpreter state dict on + // interpreter shutdown, so deleting internals too early could cause undefined behavior + // when other pybind11 objects access `get_internals()` during finalization (which would + // recreate empty internals). See also create_pp_content_once() above. + // See https://github.com/pybind/pybind11/pull/5958#discussion_r2717645230. + auto result = atomic_get_or_create_in_state_dict>( + holder_id_, /*dtor=*/nullptr /* leak the capsule content */); + auto *pp = result.first; + bool created = result.second; + // Only call on_fetch_ when fetching existing internals, not when creating new ones. + if (!created && on_fetch_ && pp) { + on_fetch_(pp->get()); } return pp; } - char const *holder_id_ = nullptr; - on_fetch_function *on_fetch_ = nullptr; #ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT - thread_specific_storage last_istate_; - thread_specific_storage> internals_tls_p_; + static PyInterpreterState *&last_istate_tls() { + static thread_local PyInterpreterState *last_istate = nullptr; + return last_istate; + } + + static std::unique_ptr *&internals_p_tls() { + static thread_local std::unique_ptr *internals_p = nullptr; + return internals_p; + } #endif - std::unique_ptr *internals_singleton_pp_; + + char const *holder_id_ = nullptr; + on_fetch_function *on_fetch_ = nullptr; + // Pointer-to-pointer to the singleton internals for the first seen interpreter (may not be the + // main interpreter) + std::unique_ptr *internals_singleton_pp_ = nullptr; + + // Track pointer-to-pointers whose internals have been created, to detect re-entrancy. + // Use instance member over static due to singleton pattern of this class. + std::unordered_set *> pps_have_created_content_; + std::mutex pp_set_mutex_; }; // If We loaded the internals through `state_dict`, our `error_already_set` @@ -623,22 +855,28 @@ inline internals_pp_manager &get_internals_pp_manager() { #else # define ON_FETCH_FN &check_internals_local_exception_translator #endif - static internals_pp_manager internals_pp_manager(PYBIND11_INTERNALS_ID, - ON_FETCH_FN); + return internals_pp_manager::get_instance(PYBIND11_INTERNALS_ID, ON_FETCH_FN); #undef ON_FETCH_FN - return internals_pp_manager; } /// Return a reference to the current `internals` data PYBIND11_NOINLINE internals &get_internals() { auto &ppmgr = get_internals_pp_manager(); - auto &internals_ptr = *ppmgr.get_pp(); + auto *pp = ppmgr.get_pp(); + if (!pp) { + pybind11_fail("get_internals: get_pp() returned nullptr"); + } + auto &internals_ptr = *pp; if (!internals_ptr) { // Slow path, something needs fetched from the state dict or created gil_scoped_acquire_simple gil; error_scope err_scope; - internals_ptr.reset(new internals()); + ppmgr.create_pp_content_once(&internals_ptr); + + if (!internals_ptr) { + pybind11_fail("get_internals: create_pp_content_once() produced nullptr"); + } if (!internals_ptr->instance_base) { // This calls get_internals, so cannot be called from within the internals constructor // called above because internals_ptr must be set before get_internals is called again @@ -648,15 +886,46 @@ PYBIND11_NOINLINE internals &get_internals() { return *internals_ptr; } +/// Return the PyObject* for the internals capsule (borrowed reference). +/// Returns nullptr if the capsule doesn't exist yet. +inline PyObject *get_internals_capsule() { + auto state_dict = reinterpret_borrow(get_python_state_dict()); + return dict_getitemstring(state_dict.ptr(), PYBIND11_INTERNALS_ID); +} + +/// Return the key used for local_internals in the state dict. +/// This function ensures a consistent key is used across all call sites within the same +/// compilation unit. The key includes the address of a static variable to make it unique per +/// module (DSO), matching the behavior of get_local_internals_pp_manager(). +inline const std::string &get_local_internals_key() { + static const std::string key + = PYBIND11_MODULE_LOCAL_ID + std::to_string(reinterpret_cast(&key)); + return key; +} + +/// Return the PyObject* for the local_internals capsule (borrowed reference). +/// Returns nullptr if the capsule doesn't exist yet. +inline PyObject *get_local_internals_capsule() { + const auto &key = get_local_internals_key(); + auto state_dict = reinterpret_borrow(get_python_state_dict()); + return dict_getitemstring(state_dict.ptr(), key.c_str()); +} + +inline void ensure_internals() { + pybind11::detail::get_internals_pp_manager().unref(); +#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT + if (PyInterpreterState_Get() != PyInterpreterState_Main()) { + has_seen_non_main_interpreter() = true; + } +#endif + pybind11::detail::get_internals(); +} + inline internals_pp_manager &get_local_internals_pp_manager() { - // Use the address of this static itself as part of the key, so that the value is uniquely tied + // Use the address of a static variable as part of the key, so that the value is uniquely tied // to where the module is loaded in memory - static const std::string this_module_idstr - = PYBIND11_MODULE_LOCAL_ID - + std::to_string(reinterpret_cast(&this_module_idstr)); - static internals_pp_manager local_internals_pp_manager( - this_module_idstr.c_str(), nullptr); - return local_internals_pp_manager; + return internals_pp_manager::get_instance(get_local_internals_key().c_str(), + nullptr); } /// Works like `get_internals`, but for things which are locally registered. @@ -664,13 +933,16 @@ inline local_internals &get_local_internals() { auto &ppmgr = get_local_internals_pp_manager(); auto &internals_ptr = *ppmgr.get_pp(); if (!internals_ptr) { - internals_ptr.reset(new local_internals()); + gil_scoped_acquire_simple gil; + error_scope err_scope; + + ppmgr.create_pp_content_once(&internals_ptr); } return *internals_ptr; } #ifdef Py_GIL_DISABLED -# define PYBIND11_LOCK_INTERNALS(internals) std::unique_lock lock((internals).mutex) +# define PYBIND11_LOCK_INTERNALS(internals) pycritical_section lock((internals).mutex) #else # define PYBIND11_LOCK_INTERNALS(internals) #endif @@ -682,13 +954,24 @@ inline auto with_internals(const F &cb) -> decltype(cb(get_internals())) { return cb(internals); } +template +inline void with_internals_if_internals(const F &cb) { + auto &ppmgr = get_internals_pp_manager(); + auto &internals_ptr = *ppmgr.get_pp(); + if (internals_ptr) { + auto &internals = *internals_ptr; + PYBIND11_LOCK_INTERNALS(internals); + cb(internals); + } +} + template inline auto with_exception_translators(const F &cb) -> decltype(cb(get_internals().registered_exception_translators, get_local_internals().registered_exception_translators)) { auto &internals = get_internals(); #ifdef Py_GIL_DISABLED - std::unique_lock lock((internals).exception_translator_mutex); + pycritical_section lock((internals).exception_translator_mutex); #endif auto &local_internals = get_local_internals(); return cb(internals.registered_exception_translators, diff --git a/pybind11/include/pybind11/detail/native_enum_data.h b/pybind11/include/pybind11/detail/native_enum_data.h index a8f7675..6770378 100644 --- a/pybind11/include/pybind11/detail/native_enum_data.h +++ b/pybind11/include/pybind11/detail/native_enum_data.h @@ -22,17 +22,36 @@ native_enum_missing_finalize_error_message(const std::string &enum_name_encoded) return "pybind11::native_enum<...>(\"" + enum_name_encoded + "\", ...): MISSING .finalize()"; } +// Internals for pybind11::native_enum; one native_enum_data object exists +// inside each pybind11::native_enum and lives only for the duration of the +// native_enum binding statement. class native_enum_data { public: - native_enum_data(const object &parent_scope, + native_enum_data(handle parent_scope_, const char *enum_name, const char *native_type_name, const char *class_doc, - const std::type_index &enum_type_index) + const native_enum_record &enum_record_) : enum_name_encoded{enum_name}, native_type_name_encoded{native_type_name}, - enum_type_index{enum_type_index}, parent_scope(parent_scope), enum_name{enum_name}, + enum_type_index{*enum_record_.cpptype}, + parent_scope(reinterpret_borrow(parent_scope_)), enum_name{enum_name}, native_type_name{native_type_name}, class_doc(class_doc), export_values_flag{false}, - finalize_needed{false} {} + finalize_needed{false} { + // Create the enum record capsule. It will be installed on the enum + // type object during finalize(). Its destructor removes the enum + // mapping from our internals, so that we won't try to convert to an + // enum type that's been destroyed. + enum_record = capsule( + new native_enum_record{enum_record_}, + native_enum_record::attribute_name(), + +[](void *record_) { + auto *record = static_cast(record_); + with_internals([&](internals &internals) { + internals.native_enum_type_map.erase(*record->cpptype); + }); + delete record; + }); + } void finalize(); @@ -71,6 +90,7 @@ class native_enum_data { str enum_name; str native_type_name; std::string class_doc; + capsule enum_record; protected: list members; @@ -81,12 +101,6 @@ class native_enum_data { bool finalize_needed : 1; }; -inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index, - PyObject *py_enum) { - with_internals( - [&](internals &internals) { internals.native_enum_type_map[enum_type_index] = py_enum; }); -} - inline handle global_internals_native_enum_type_map_get_item(const std::type_index &enum_type_index) { return with_internals([&](internals &internals) { @@ -202,7 +216,11 @@ inline void native_enum_data::finalize() { for (auto doc : member_docs) { py_enum[doc[int_(0)]].attr("__doc__") = doc[int_(1)]; } - global_internals_native_enum_type_map_set_item(enum_type_index, py_enum.release().ptr()); + + py_enum.attr(native_enum_record::attribute_name()) = enum_record; + with_internals([&](internals &internals) { + internals.native_enum_type_map[enum_type_index] = py_enum.ptr(); + }); } PYBIND11_NAMESPACE_END(detail) diff --git a/pybind11/include/pybind11/detail/pybind11_namespace_macros.h b/pybind11/include/pybind11/detail/pybind11_namespace_macros.h index bd18c8c..6f74bf8 100644 --- a/pybind11/include/pybind11/detail/pybind11_namespace_macros.h +++ b/pybind11/include/pybind11/detail/pybind11_namespace_macros.h @@ -16,12 +16,7 @@ // possible using these macros. Please also be sure to push/pop with the pybind11 macros. Please // only use compiler specifics if you need to check specific versions, e.g. Apple Clang vs. vanilla // Clang. -#if defined(_MSC_VER) -# define PYBIND11_COMPILER_MSVC -# define PYBIND11_PRAGMA(...) __pragma(__VA_ARGS__) -# define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(warning(push)) -# define PYBIND11_WARNING_POP PYBIND11_PRAGMA(warning(pop)) -#elif defined(__INTEL_COMPILER) +#if defined(__INTEL_COMPILER) # define PYBIND11_COMPILER_INTEL # define PYBIND11_PRAGMA(...) _Pragma(#__VA_ARGS__) # define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(warning push) @@ -36,6 +31,11 @@ # define PYBIND11_PRAGMA(...) _Pragma(#__VA_ARGS__) # define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(GCC diagnostic push) # define PYBIND11_WARNING_POP PYBIND11_PRAGMA(GCC diagnostic pop) +#elif defined(_MSC_VER) // Must be after the clang branch because clang-cl also defines _MSC_VER +# define PYBIND11_COMPILER_MSVC +# define PYBIND11_PRAGMA(...) __pragma(__VA_ARGS__) +# define PYBIND11_WARNING_PUSH PYBIND11_PRAGMA(warning(push)) +# define PYBIND11_WARNING_POP PYBIND11_PRAGMA(warning(pop)) #endif #ifdef PYBIND11_COMPILER_MSVC diff --git a/pybind11/include/pybind11/detail/struct_smart_holder.h b/pybind11/include/pybind11/detail/struct_smart_holder.h index 5b65b4a..5d7c31c 100644 --- a/pybind11/include/pybind11/detail/struct_smart_holder.h +++ b/pybind11/include/pybind11/detail/struct_smart_holder.h @@ -339,22 +339,42 @@ struct smart_holder { template static smart_holder from_unique_ptr(std::unique_ptr &&unq_ptr, - void *void_ptr = nullptr) { + void *mi_subobject_ptr = nullptr) { smart_holder hld; hld.rtti_uqp_del = &typeid(D); hld.vptr_is_using_std_default_delete = uqp_del_is_std_default_delete(); - guarded_delete gd{nullptr, false}; - if (hld.vptr_is_using_std_default_delete) { - gd = make_guarded_std_default_delete(true); - } else { - gd = make_guarded_custom_deleter(std::move(unq_ptr.get_deleter()), true); - } - if (void_ptr != nullptr) { - hld.vptr.reset(void_ptr, std::move(gd)); + + // Build the owning control block on the *real object start* (T*). + guarded_delete gd + = hld.vptr_is_using_std_default_delete + ? make_guarded_std_default_delete(true) + : make_guarded_custom_deleter(std::move(unq_ptr.get_deleter()), true); + // Critical: construct owner with pointer we intend to delete + std::shared_ptr owner(unq_ptr.get(), std::move(gd)); + // Relinquish ownership only after successful construction of owner + (void) unq_ptr.release(); + + // Publish either the MI/VI subobject pointer (if provided) or the full object. + // Why this is needed: + // * The `owner` shared_ptr must always manage the true object start (T*). + // That ensures the deleter is invoked on a valid object header, so the + // virtual destructor can dispatch safely (critical on MSVC with virtual + // inheritance, where base subobjects are not at offset 0). + // * However, pybind11 needs to *register* and expose the subobject pointer + // appropriate for the type being bound. + // This pointer may differ from the T* object start under multiple/virtual + // inheritance. + // This is achieved by using an aliasing shared_ptr: + // - `owner` retains lifetime of the actual T* object start for deletion. + // - `vptr` points at the adjusted subobject (mi_subobject_ptr), giving + // Python the correct identity/registration address. + // If no subobject pointer is passed, we simply publish the full object. + if (mi_subobject_ptr) { + hld.vptr = std::shared_ptr(owner, mi_subobject_ptr); } else { - hld.vptr.reset(unq_ptr.get(), std::move(gd)); + hld.vptr = std::static_pointer_cast(owner); } - (void) unq_ptr.release(); + hld.is_populated = true; return hld; } diff --git a/pybind11/include/pybind11/detail/type_caster_base.h b/pybind11/include/pybind11/detail/type_caster_base.h index 1b23c5c..8fbf700 100644 --- a/pybind11/include/pybind11/detail/type_caster_base.h +++ b/pybind11/include/pybind11/detail/type_caster_base.h @@ -42,24 +42,40 @@ PYBIND11_NAMESPACE_BEGIN(detail) /// Adding a patient will keep it alive up until the enclosing function returns. class loader_life_support { private: + // Thread-local top-of-stack for loader_life_support frames (linked via parent). + // Observation: loader_life_support needs to be thread-local, + // but we don't need to go to extra effort to keep it + // per-interpreter (i.e., by putting it in internals) since + // individual function calls are already isolated to a single + // interpreter, even though they could potentially call into a + // different interpreter later in the same call chain. This + // saves a significant cost per function call spent in + // loader_life_support destruction. + // Note for future C++17 simplification: + // inline static thread_local loader_life_support *tls_current_frame = nullptr; + static loader_life_support *&tls_current_frame() { + static thread_local loader_life_support *frame_ptr = nullptr; + return frame_ptr; + } + loader_life_support *parent = nullptr; std::unordered_set keep_alive; public: /// A new patient frame is created when a function is entered loader_life_support() { - auto &stack_top = get_internals().loader_life_support_tls; - parent = stack_top.get(); - stack_top = this; + auto &frame = tls_current_frame(); + parent = frame; + frame = this; } /// ... and destroyed after it returns ~loader_life_support() { - auto &stack_top = get_internals().loader_life_support_tls; - if (stack_top.get() != this) { + auto &frame = tls_current_frame(); + if (frame != this) { pybind11_fail("loader_life_support: internal error"); } - stack_top = parent; + frame = parent; for (auto *item : keep_alive) { Py_DECREF(item); } @@ -68,7 +84,7 @@ class loader_life_support { /// This can only be used inside a pybind11-bound function, either by `argument_loader` /// at argument preparation time or by `py::cast()` at execution time. PYBIND11_NOINLINE static void add_patient(handle h) { - loader_life_support *frame = get_internals().loader_life_support_tls.get(); + loader_life_support *frame = tls_current_frame(); if (!frame) { // NOTE: It would be nice to include the stack frames here, as this indicates // use of pybind11::cast<> outside the normal call framework, finding such @@ -109,7 +125,7 @@ PYBIND11_NOINLINE void all_type_info_populate(PyTypeObject *t, std::vector check; for (handle parent : reinterpret_borrow(t->tp_bases)) { - check.push_back((PyTypeObject *) parent.ptr()); + check.push_back(reinterpret_cast(parent.ptr())); } auto const &type_dict = get_internals().registered_types_py; for (size_t i = 0; i < check.size(); i++) { @@ -152,7 +168,7 @@ PYBIND11_NOINLINE void all_type_info_populate(PyTypeObject *t, std::vector(type->tp_bases)) { - check.push_back((PyTypeObject *) parent.ptr()); + check.push_back(reinterpret_cast(parent.ptr())); } } } @@ -189,35 +205,73 @@ PYBIND11_NOINLINE detail::type_info *get_type_info(PyTypeObject *type) { return bases.front(); } -inline detail::type_info *get_local_type_info(const std::type_index &tp) { - auto &locals = get_local_internals().registered_types_cpp; - auto it = locals.find(tp); +inline detail::type_info *get_local_type_info_lock_held(const std::type_info &tp) { + const auto &locals = get_local_internals().registered_types_cpp; + auto it = locals.find(&tp); if (it != locals.end()) { return it->second; } return nullptr; } -inline detail::type_info *get_global_type_info(const std::type_index &tp) { - return with_internals([&](internals &internals) { - detail::type_info *type_info = nullptr; - auto &types = internals.registered_types_cpp; - auto it = types.find(tp); - if (it != types.end()) { - type_info = it->second; - } - return type_info; - }); +inline detail::type_info *get_local_type_info(const std::type_info &tp) { + // NB: internals and local_internals share a single mutex + PYBIND11_LOCK_INTERNALS(get_internals()); + return get_local_type_info_lock_held(tp); +} + +inline detail::type_info *get_global_type_info_lock_held(const std::type_info &tp) { + // This is a two-level lookup. Hopefully we find the type info in + // registered_types_cpp_fast, but if not we try + // registered_types_cpp and fill registered_types_cpp_fast for + // next time. + detail::type_info *type_info = nullptr; + auto &internals = get_internals(); +#if PYBIND11_INTERNALS_VERSION >= 12 + auto &fast_types = internals.registered_types_cpp_fast; +#endif + auto &types = internals.registered_types_cpp; +#if PYBIND11_INTERNALS_VERSION >= 12 + auto fast_it = fast_types.find(&tp); + if (fast_it != fast_types.end()) { +# ifndef NDEBUG + auto types_it = types.find(std::type_index(tp)); + assert(types_it != types.end()); + assert(types_it->second == fast_it->second); +# endif + return fast_it->second; + } +#endif // PYBIND11_INTERNALS_VERSION >= 12 + + auto it = types.find(std::type_index(tp)); + if (it != types.end()) { +#if PYBIND11_INTERNALS_VERSION >= 12 + // We found the type in the slow map but not the fast one, so + // some other DSO added it (otherwise it would be in the fast + // map under &tp) and therefore we must be an alias. Record + // that. + it->second->alias_chain.push_front(&tp); + fast_types.emplace(&tp, it->second); +#endif + type_info = it->second; + } + return type_info; +} + +inline detail::type_info *get_global_type_info(const std::type_info &tp) { + PYBIND11_LOCK_INTERNALS(get_internals()); + return get_global_type_info_lock_held(tp); } /// Return the type info for a given C++ type; on lookup failure can either throw or return /// nullptr. -PYBIND11_NOINLINE detail::type_info *get_type_info(const std::type_index &tp, +PYBIND11_NOINLINE detail::type_info *get_type_info(const std::type_info &tp, bool throw_if_missing = false) { - if (auto *ltype = get_local_type_info(tp)) { + PYBIND11_LOCK_INTERNALS(get_internals()); + if (auto *ltype = get_local_type_info_lock_held(tp)) { return ltype; } - if (auto *gtype = get_global_type_info(tp)) { + if (auto *gtype = get_global_type_info_lock_held(tp)) { return gtype; } @@ -232,14 +286,14 @@ PYBIND11_NOINLINE detail::type_info *get_type_info(const std::type_index &tp, PYBIND11_NOINLINE handle get_type_handle(const std::type_info &tp, bool throw_if_missing) { detail::type_info *type_info = get_type_info(tp, throw_if_missing); - return handle(type_info ? ((PyObject *) type_info->type) : nullptr); + return handle(type_info ? (reinterpret_cast(type_info->type)) : nullptr); } inline bool try_incref(PyObject *obj) { // Tries to increment the reference count of an object if it's not zero. - // TODO: Use PyUnstable_TryIncref when available. - // See https://github.com/python/cpython/issues/128844 -#ifdef Py_GIL_DISABLED +#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E00A4 + return PyUnstable_TryIncRef(obj); +#elif defined(Py_GIL_DISABLED) // See // https://github.com/python/cpython/blob/d05140f9f77d7dfc753dd1e5ac3a5962aaa03eff/Include/internal/pycore_object.h#L761 uint32_t local = _Py_atomic_load_uint32_relaxed(&obj->ob_ref_local); @@ -452,7 +506,7 @@ PYBIND11_NOINLINE void instance::allocate_layout() { // efficient for small allocations like the one we're doing here; // for larger allocations they are just wrappers around malloc. // TODO: is this still true for pure Python 3.6? - nonsimple.values_and_holders = (void **) PyMem_Calloc(space, sizeof(void *)); + nonsimple.values_and_holders = static_cast(PyMem_Calloc(space, sizeof(void *))); if (!nonsimple.values_and_holders) { throw std::bad_alloc(); } @@ -483,7 +537,7 @@ PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_i for (auto it = range.first; it != range.second; ++it) { for (const auto &vh : values_and_holders(it->second)) { if (vh.type == type) { - return handle((PyObject *) it->second); + return handle(reinterpret_cast(it->second)); } } } @@ -491,6 +545,74 @@ PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_i }); } +// Information about how type_caster_generic::cast() can obtain its source object +struct cast_sources { + // A type-erased pointer and the type it points to + struct raw_source { + const void *cppobj; + const std::type_info *cpptype; + }; + + // A C++ pointer and the Python type info we will convert it to; + // we expect that cppobj points to something of type tinfo->cpptype + struct resolved_source { + const void *cppobj; + const type_info *tinfo; + }; + + // Use the given pointer with its compile-time type, possibly downcast + // via polymorphic_type_hook() + template + explicit cast_sources(const itype *ptr); + + // Use the given pointer and type + // NOLINTNEXTLINE(google-explicit-constructor) + cast_sources(const raw_source &orig) : original(orig) { result = resolve(); } + + // Use the given object and pybind11 type info. NB: if tinfo is null, + // this does not provide enough information to use a foreign type or + // to render a useful error message + cast_sources(const void *obj, const detail::type_info *tinfo) + : original{obj, tinfo ? tinfo->cpptype : nullptr}, result{obj, tinfo} {} + + // The object passed to cast(), with its static type. + // original.type must not be null if resolve() will be called. + // original.obj may be null if we're converting nullptr to a Python None + raw_source original; + + // A more-derived version of `original` provided by a + // polymorphic_type_hook. downcast.type may be null if this is not + // a relevant concept for the current cast. + raw_source downcast{}; + + // The source to use for this cast, and the corresponding pybind11 + // type_info. If the type_info is null, then pybind11 doesn't know + // about this type. + resolved_source result; + + // Returns true if the cast will use a pybind11 type that uses + // a smart holder. + bool creates_smart_holder() const { + return result.tinfo != nullptr + && result.tinfo->holder_enum_v == detail::holder_enum_t::smart_holder; + } + +private: + resolved_source resolve() { + if (downcast.cpptype) { + if (same_type(*original.cpptype, *downcast.cpptype)) { + downcast.cpptype = nullptr; + } else if (const auto *tpi = get_type_info(*downcast.cpptype)) { + return {downcast.cppobj, tpi}; + } + } + if (const auto *tpi = get_type_info(*original.cpptype)) { + return {original.cppobj, tpi}; + } + return {nullptr, nullptr}; + } +}; + // Forward declarations void keep_alive_impl(handle nurse, handle patient); inline PyObject *make_new_instance(PyTypeObject *type); @@ -553,16 +675,18 @@ template handle smart_holder_from_unique_ptr(std::unique_ptr &&src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { if (policy == return_value_policy::copy) { throw cast_error("return_value_policy::copy is invalid for unique_ptr."); } if (!src) { return none().release(); } - void *src_raw_void_ptr = const_cast(st.first); - assert(st.second != nullptr); - const detail::type_info *tinfo = st.second; + // cs.cppobj is the subobject pointer appropriate for tinfo (may differ from src.get() + // under MI/VI). Use this for Python identity/registration, but keep ownership on T*. + void *src_raw_void_ptr = const_cast(cs.cppobj); + assert(cs.tinfo != nullptr); + const detail::type_info *tinfo = cs.tinfo; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { auto *self_life_support = tinfo->get_trampoline_self_life_support(src.get()); if (self_life_support != nullptr) { @@ -609,20 +733,20 @@ template handle smart_holder_from_unique_ptr(std::unique_ptr &&src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { return smart_holder_from_unique_ptr( std::unique_ptr(const_cast(src.release()), std::move(src.get_deleter())), // Const2Mutbl policy, parent, - st); + cs); } template handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { switch (policy) { case return_value_policy::automatic: case return_value_policy::automatic_reference: @@ -641,10 +765,11 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return none().release(); } - auto src_raw_ptr = src.get(); - assert(st.second != nullptr); - void *src_raw_void_ptr = static_cast(src_raw_ptr); - const detail::type_info *tinfo = st.second; + // cs.cppobj is the subobject pointer appropriate for tinfo (may differ from src.get() + // under MI/VI). Use this for Python identity/registration, but keep ownership on T*. + void *src_raw_void_ptr = const_cast(cs.cppobj); + assert(cs.tinfo != nullptr); + const detail::type_info *tinfo = cs.tinfo; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder. // PYBIND11:REMINDER: MISSING: keep_alive. @@ -657,8 +782,7 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); valueptr = src_raw_void_ptr; - auto smhldr - = smart_holder::from_shared_ptr(std::shared_ptr(src, const_cast(st.first))); + auto smhldr = smart_holder::from_shared_ptr(std::shared_ptr(src, src_raw_void_ptr)); tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); if (policy == return_value_policy::reference_internal) { @@ -672,11 +796,11 @@ template handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return_value_policy policy, handle parent, - const std::pair &st) { + const cast_sources::resolved_source &cs) { return smart_holder_from_shared_ptr(std::const_pointer_cast(src), // Const2Mutbl policy, parent, - st); + cs); } struct shared_ptr_parent_life_support { @@ -869,21 +993,51 @@ class type_caster_generic { bool load(handle src, bool convert) { return load_impl(src, convert); } - PYBIND11_NOINLINE static handle cast(const void *_src, + static handle cast(const void *src, + return_value_policy policy, + handle parent, + const detail::type_info *tinfo, + void *(*copy_constructor)(const void *), + void *(*move_constructor)(const void *), + const void *existing_holder = nullptr) { + cast_sources srcs{src, tinfo}; + return cast(srcs, policy, parent, copy_constructor, move_constructor, existing_holder); + } + + static handle cast_non_owning(const cast_sources &srcs, + return_value_policy policy, + handle parent, + const void *existing_holder = nullptr) { + // Reference-like policies alias an existing C++ object instead of creating + // a new one, so copy/move constructor callbacks must remain null here. + assert(policy == return_value_policy::reference + || policy == return_value_policy::reference_internal + || policy == return_value_policy::automatic_reference); + return cast(srcs, policy, parent, nullptr, nullptr, existing_holder); + } + + PYBIND11_NOINLINE static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent, - const detail::type_info *tinfo, void *(*copy_constructor)(const void *), void *(*move_constructor)(const void *), const void *existing_holder = nullptr) { - if (!tinfo) { // no type info: error will be set already + if (!srcs.result.tinfo) { + // No pybind11 type info. Raise an exception. + std::string tname = srcs.downcast.cpptype ? srcs.downcast.cpptype->name() + : srcs.original.cpptype ? srcs.original.cpptype->name() + : ""; + detail::clean_type_id(tname); + std::string msg = "Unregistered type : " + tname; + set_error(PyExc_TypeError, msg.c_str()); return handle(); } - void *src = const_cast(_src); + void *src = const_cast(srcs.result.cppobj); if (src == nullptr) { return none().release(); } + const type_info *tinfo = srcs.result.tinfo; if (handle registered_inst = find_registered_python_instance(src, tinfo)) { return registered_inst; @@ -1018,6 +1172,7 @@ class type_caster_generic { return false; } void check_holder_compat() {} + bool set_foreign_holder(handle) { return true; } PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) { auto caster = type_caster_generic(ti); @@ -1056,14 +1211,14 @@ class type_caster_generic { // logic (without having to resort to virtual inheritance). template PYBIND11_NOINLINE bool load_impl(handle src, bool convert) { + auto &this_ = static_cast(*this); if (!src) { return false; } if (!typeinfo) { - return try_load_foreign_module_local(src); + return try_load_foreign_module_local(src) && this_.set_foreign_holder(src); } - auto &this_ = static_cast(*this); this_.check_holder_compat(); PyTypeObject *srctype = Py_TYPE(src.ptr()); @@ -1129,13 +1284,13 @@ class type_caster_generic { if (typeinfo->module_local) { if (auto *gtype = get_global_type_info(*typeinfo->cpptype)) { typeinfo = gtype; - return load(src, false); + return load_impl(src, false); } } // Global typeinfo has precedence over foreign module_local if (try_load_foreign_module_local(src)) { - return true; + return this_.set_foreign_holder(src); } // Custom converters didn't take None, now we convert None to nullptr. @@ -1149,31 +1304,12 @@ class type_caster_generic { } if (convert && cpptype && this_.try_cpp_conduit(src)) { - return true; + return this_.set_foreign_holder(src); } return false; } - // Called to do type lookup and wrap the pointer and type in a pair when a dynamic_cast - // isn't needed or can't be used. If the type is unknown, sets the error and returns a pair - // with .second = nullptr. (p.first = nullptr is not an error: it becomes None). - PYBIND11_NOINLINE static std::pair - src_and_type(const void *src, - const std::type_info &cast_type, - const std::type_info *rtti_type = nullptr) { - if (auto *tpi = get_type_info(cast_type)) { - return {src, const_cast(tpi)}; - } - - // Not found, set error: - std::string tname = rtti_type ? rtti_type->name() : cast_type.name(); - detail::clean_type_id(tname); - std::string msg = "Unregistered type : " + tname; - set_error(PyExc_TypeError, msg.c_str()); - return {nullptr, nullptr}; - } - const type_info *typeinfo = nullptr; const std::type_info *cpptype = nullptr; void *value = nullptr; @@ -1468,6 +1604,20 @@ struct polymorphic_type_hook : public polymorphic_type_hook_base {}; PYBIND11_NAMESPACE_BEGIN(detail) +template +cast_sources::cast_sources(const itype *ptr) : original{ptr, &typeid(itype)} { + // If this is a base pointer to a derived type, and the derived type is + // registered with pybind11, we want to make the full derived object + // available. In the typical case where itype is polymorphic, we get the + // correct derived pointer (which may be != base pointer) by a dynamic_cast + // to most derived type. If itype is not polymorphic, a user-provided + // specialization of polymorphic_type_hook can do the same thing. + // If there is no downcast to perform, then the default hook will leave + // derived.type set to nullptr, which causes us to ignore derived.obj. + downcast.cppobj = polymorphic_type_hook::get(ptr, downcast.cpptype); + result = resolve(); +} + /// Generic type caster for objects stored on the heap template class type_caster_base : public type_caster_generic { @@ -1479,6 +1629,13 @@ class type_caster_base : public type_caster_generic { type_caster_base() : type_caster_base(typeid(type)) {} explicit type_caster_base(const std::type_info &info) : type_caster_generic(info) {} + // Wrap the generic cast_sources to be only constructible from the type + // that's correct in this context, so you can't use type_caster_base + // to convert an unrelated B* to Python. + struct cast_sources : detail::cast_sources { + explicit cast_sources(const itype *ptr) : detail::cast_sources(ptr) {} + }; + static handle cast(const itype &src, return_value_policy policy, handle parent) { if (policy == return_value_policy::automatic || policy == return_value_policy::automatic_reference) { @@ -1491,50 +1648,25 @@ class type_caster_base : public type_caster_generic { return cast(std::addressof(src), return_value_policy::move, parent); } - // Returns a (pointer, type_info) pair taking care of necessary type lookup for a - // polymorphic type (using RTTI by default, but can be overridden by specializing - // polymorphic_type_hook). If the instance isn't derived, returns the base version. - static std::pair src_and_type(const itype *src) { - const auto &cast_type = typeid(itype); - const std::type_info *instance_type = nullptr; - const void *vsrc = polymorphic_type_hook::get(src, instance_type); - if (instance_type && !same_type(cast_type, *instance_type)) { - // This is a base pointer to a derived type. If the derived type is registered - // with pybind11, we want to make the full derived object available. - // In the typical case where itype is polymorphic, we get the correct - // derived pointer (which may be != base pointer) by a dynamic_cast to - // most derived type. If itype is not polymorphic, we won't get here - // except via a user-provided specialization of polymorphic_type_hook, - // and the user has promised that no this-pointer adjustment is - // required in that case, so it's OK to use static_cast. - if (const auto *tpi = get_type_info(*instance_type)) { - return {vsrc, tpi}; - } - } - // Otherwise we have either a nullptr, an `itype` pointer, or an unknown derived pointer, - // so don't do a cast - return type_caster_generic::src_and_type(src, cast_type, instance_type); + static handle cast(const itype *src, return_value_policy policy, handle parent) { + return cast(cast_sources{src}, policy, parent); } - static handle cast(const itype *src, return_value_policy policy, handle parent) { - auto st = src_and_type(src); - return type_caster_generic::cast(st.first, + static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent) { + return type_caster_generic::cast(srcs, policy, parent, - st.second, - make_copy_constructor(src), - make_move_constructor(src)); + make_copy_constructor((const itype *) nullptr), + make_move_constructor((const itype *) nullptr)); } static handle cast_holder(const itype *src, const void *holder) { - auto st = src_and_type(src); - return type_caster_generic::cast(st.first, - return_value_policy::take_ownership, - {}, - st.second, - nullptr, - nullptr, - holder); + return cast_holder(cast_sources{src}, holder); + } + + static handle cast_holder(const cast_sources &srcs, const void *holder) { + auto policy = return_value_policy::take_ownership; + return type_caster_generic::cast(srcs, policy, {}, nullptr, nullptr, holder); } template @@ -1580,7 +1712,7 @@ inline std::string quote_cpp_type_name(const std::string &cpp_type_name) { PYBIND11_NOINLINE std::string type_info_description(const std::type_info &ti) { if (auto *type_data = get_type_info(ti)) { - handle th((PyObject *) type_data->type); + handle th(reinterpret_cast(type_data->type)); return th.attr("__module__").cast() + '.' + th.attr("__qualname__").cast(); } diff --git a/pybind11/include/pybind11/detail/value_and_holder.h b/pybind11/include/pybind11/detail/value_and_holder.h index 87c92f8..b24551e 100644 --- a/pybind11/include/pybind11/detail/value_and_holder.h +++ b/pybind11/include/pybind11/detail/value_and_holder.h @@ -54,7 +54,8 @@ struct value_and_holder { } else if (v) { inst->nonsimple.status[index] |= instance::status_holder_constructed; } else { - inst->nonsimple.status[index] &= (std::uint8_t) ~instance::status_holder_constructed; + inst->nonsimple.status[index] + &= static_cast(~instance::status_holder_constructed); } } bool instance_registered() const { @@ -69,7 +70,8 @@ struct value_and_holder { } else if (v) { inst->nonsimple.status[index] |= instance::status_instance_registered; } else { - inst->nonsimple.status[index] &= (std::uint8_t) ~instance::status_instance_registered; + inst->nonsimple.status[index] + &= static_cast(~instance::status_instance_registered); } } }; diff --git a/pybind11/include/pybind11/eigen/tensor.h b/pybind11/include/pybind11/eigen/tensor.h index 50e8b50..e5c8441 100644 --- a/pybind11/include/pybind11/eigen/tensor.h +++ b/pybind11/include/pybind11/eigen/tensor.h @@ -124,10 +124,9 @@ struct eigen_tensor_helper< template struct get_tensor_descriptor { static constexpr auto details - = const_name(", \"flags.writeable\"", "") + const_name - < static_cast(Type::Layout) - == static_cast(Eigen::RowMajor) - > (", \"flags.c_contiguous\"", ", \"flags.f_contiguous\""); + = const_name(", \"flags.writeable\"", "") + + const_name(Type::Layout) == static_cast(Eigen::RowMajor)>( + ", \"flags.c_contiguous\"", ", \"flags.f_contiguous\""); static constexpr auto value = const_name("typing.Annotated[") + io_name("numpy.typing.ArrayLike, ", "numpy.typing.NDArray[") diff --git a/pybind11/include/pybind11/embed.h b/pybind11/include/pybind11/embed.h index a820bfb..c05887c 100644 --- a/pybind11/include/pybind11/embed.h +++ b/pybind11/include/pybind11/embed.h @@ -58,7 +58,7 @@ PYBIND11_WARNING_PUSH PYBIND11_WARNING_DISABLE_CLANG("-Wgnu-zero-variadic-macro-arguments") #define PYBIND11_EMBEDDED_MODULE(name, variable, ...) \ - PYBIND11_MODULE_PYINIT(name, {}, ##__VA_ARGS__) \ + PYBIND11_MODULE_PYINIT(name, ##__VA_ARGS__) \ ::pybind11::detail::embedded_module PYBIND11_CONCAT(pybind11_module_, name)( \ PYBIND11_TOSTRING(name), PYBIND11_CONCAT(PyInit_, name)); \ PYBIND11_MODULE_EXEC(name, variable) @@ -202,7 +202,7 @@ inline void initialize_interpreter(bool init_signal_handlers = true, #endif // There is exactly one interpreter alive currently. - detail::get_num_interpreters_seen() = 1; + detail::has_seen_non_main_interpreter() = false; } /** \rst @@ -242,12 +242,12 @@ inline void initialize_interpreter(bool init_signal_handlers = true, \endrst */ inline void finalize_interpreter() { // get rid of any thread-local interpreter cache that currently exists - if (detail::get_num_interpreters_seen() > 1) { + if (detail::has_seen_non_main_interpreter()) { detail::get_internals_pp_manager().unref(); detail::get_local_internals_pp_manager().unref(); - // We know there can be no other interpreter alive now, so we can lower the count - detail::get_num_interpreters_seen() = 1; + // We know there can be no other interpreter alive now + detail::has_seen_non_main_interpreter() = false; } // Re-fetch the internals pointer-to-pointer (but not the internals itself, which might not @@ -265,8 +265,8 @@ inline void finalize_interpreter() { // avoid undefined behaviors when initializing another interpreter detail::get_local_internals_pp_manager().destroy(); - // We know there is no interpreter alive now, so we can reset the count - detail::get_num_interpreters_seen() = 0; + // We know there is no interpreter alive now, so we can reset the multi-flag + detail::has_seen_non_main_interpreter() = false; } /** \rst diff --git a/pybind11/include/pybind11/gil.h b/pybind11/include/pybind11/gil.h index 4222a03..9e799b3 100644 --- a/pybind11/include/pybind11/gil.h +++ b/pybind11/include/pybind11/gil.h @@ -120,7 +120,11 @@ class gil_scoped_acquire { pybind11_fail("scoped_acquire::dec_ref(): internal error!"); } # endif + // Make sure that PyThreadState_Clear is not recursively called by finalizers. + // See issue #5827 + ++tstate->gilstate_counter; PyThreadState_Clear(tstate); + --tstate->gilstate_counter; if (active) { PyThreadState_DeleteCurrent(); } diff --git a/pybind11/include/pybind11/gil_safe_call_once.h b/pybind11/include/pybind11/gil_safe_call_once.h index 44e68f0..770ed49 100644 --- a/pybind11/include/pybind11/gil_safe_call_once.h +++ b/pybind11/include/pybind11/gil_safe_call_once.h @@ -3,17 +3,31 @@ #pragma once #include "detail/common.h" +#include "detail/internals.h" #include "gil.h" #include #include -#ifdef Py_GIL_DISABLED +#if defined(Py_GIL_DISABLED) || defined(PYBIND11_HAS_SUBINTERPRETER_SUPPORT) # include #endif +#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT +# include +# include +# include +#endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) +#if defined(Py_GIL_DISABLED) || defined(PYBIND11_HAS_SUBINTERPRETER_SUPPORT) +using atomic_bool = std::atomic_bool; +#else +using atomic_bool = bool; +#endif +PYBIND11_NAMESPACE_END(detail) + // Use the `gil_safe_call_once_and_store` class below instead of the naive // // static auto imported_obj = py::module_::import("module_name"); // BAD, DO NOT USE! @@ -48,12 +62,23 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) // functions, which is usually the case. // // For in-depth background, see docs/advanced/deadlock.md +#ifndef PYBIND11_HAS_SUBINTERPRETER_SUPPORT +// Subinterpreter support is disabled. +// In this case, we can store the result globally, because there is only a single interpreter. +// +// The life span of the stored result is the entire process lifetime. It is leaked on process +// termination to avoid destructor calls after the Python interpreter was finalized. template class gil_safe_call_once_and_store { public: // PRECONDITION: The GIL must be held when `call_once_and_store_result()` is called. + // + // NOTE: The second parameter (finalize callback) is intentionally unused when subinterpreter + // support is disabled. In that case, storage is process-global and intentionally leaked to + // avoid calling destructors after the Python interpreter has been finalized. template - gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn) { + gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn, + void (*)(T &) /*unused*/ = nullptr) { if (!is_initialized_) { // This read is guarded by the GIL. // Multiple threads may enter here, because the GIL is released in the next line and // CPython API calls in the `fn()` call below may release and reacquire the GIL. @@ -74,29 +99,175 @@ class gil_safe_call_once_and_store { T &get_stored() { assert(is_initialized_); PYBIND11_WARNING_PUSH -#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 5 +# if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 5 // Needed for gcc 4.8.5 PYBIND11_WARNING_DISABLE_GCC("-Wstrict-aliasing") -#endif +# endif return *reinterpret_cast(storage_); PYBIND11_WARNING_POP } constexpr gil_safe_call_once_and_store() = default; + // The instance is a global static, so its destructor runs when the process + // is terminating. Therefore, do nothing here because the Python interpreter + // may have been finalized already. PYBIND11_DTOR_CONSTEXPR ~gil_safe_call_once_and_store() = default; + // Disable copy and move operations. + gil_safe_call_once_and_store(const gil_safe_call_once_and_store &) = delete; + gil_safe_call_once_and_store(gil_safe_call_once_and_store &&) = delete; + gil_safe_call_once_and_store &operator=(const gil_safe_call_once_and_store &) = delete; + gil_safe_call_once_and_store &operator=(gil_safe_call_once_and_store &&) = delete; + private: + // The global static storage (per-process) when subinterpreter support is disabled. alignas(T) char storage_[sizeof(T)] = {}; - std::once_flag once_flag_ = {}; -#ifdef Py_GIL_DISABLED - std::atomic_bool -#else - bool -#endif - is_initialized_{false}; + std::once_flag once_flag_; + // The `is_initialized_`-`storage_` pair is very similar to `std::optional`, // but the latter does not have the triviality properties of former, // therefore `std::optional` is not a viable alternative here. + detail::atomic_bool is_initialized_{false}; }; +#else +// Subinterpreter support is enabled. +// In this case, we should store the result per-interpreter instead of globally, because each +// subinterpreter has its own separate state. The cached result may not shareable across +// interpreters (e.g., imported modules and their members). + +PYBIND11_NAMESPACE_BEGIN(detail) + +template +struct call_once_storage { + alignas(T) char storage[sizeof(T)] = {}; + std::once_flag once_flag; + void (*finalize)(T &) = nullptr; + std::atomic_bool is_initialized{false}; + + call_once_storage() = default; + ~call_once_storage() { + if (is_initialized) { + if (finalize != nullptr) { + finalize(*reinterpret_cast(storage)); + } else { + reinterpret_cast(storage)->~T(); + } + } + } + call_once_storage(const call_once_storage &) = delete; + call_once_storage(call_once_storage &&) = delete; + call_once_storage &operator=(const call_once_storage &) = delete; + call_once_storage &operator=(call_once_storage &&) = delete; +}; + +PYBIND11_NAMESPACE_END(detail) + +// Prefix for storage keys in the interpreter state dict. +# define PYBIND11_CALL_ONCE_STORAGE_KEY_PREFIX PYBIND11_INTERNALS_ID "_call_once_storage__" + +// The life span of the stored result is the entire interpreter lifetime. An additional +// `finalize_fn` can be provided to clean up the stored result when the interpreter is destroyed. +template +class gil_safe_call_once_and_store { +public: + // PRECONDITION: The GIL must be held when `call_once_and_store_result()` is called. + template + gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn, + void (*finalize_fn)(T &) = nullptr) { + if (!is_last_storage_valid()) { + // Multiple threads may enter here, because the GIL is released in the next line and + // CPython API calls in the `fn()` call below may release and reacquire the GIL. + gil_scoped_release gil_rel; // Needed to establish lock ordering. + // There can be multiple threads going through here. + storage_type *value = nullptr; + { + gil_scoped_acquire gil_acq; // Restore lock ordering. + // This function is thread-safe under free-threading. + value = get_or_create_storage_in_state_dict(); + } + assert(value != nullptr); + std::call_once(value->once_flag, [&] { + // Only one thread will ever enter here. + gil_scoped_acquire gil_acq; + // fn may release, but will reacquire, the GIL. + ::new (value->storage) T(fn()); + value->finalize = finalize_fn; + value->is_initialized = true; + last_storage_ptr_ = reinterpret_cast(value->storage); + is_initialized_by_at_least_one_interpreter_ = true; + }); + // All threads will observe `is_initialized_by_at_least_one_interpreter_` as true here. + } + // Intentionally not returning `T &` to ensure the calling code is self-documenting. + return *this; + } + + // This must only be called after `call_once_and_store_result()` was called. + T &get_stored() { + T *result = last_storage_ptr_; + if (!is_last_storage_valid()) { + gil_scoped_acquire gil_acq; + auto *value = get_or_create_storage_in_state_dict(); + result = last_storage_ptr_ = reinterpret_cast(value->storage); + } + assert(result != nullptr); + return *result; + } + + constexpr gil_safe_call_once_and_store() = default; + // The instance is a global static, so its destructor runs when the process + // is terminating. Therefore, do nothing here because the Python interpreter + // may have been finalized already. + PYBIND11_DTOR_CONSTEXPR ~gil_safe_call_once_and_store() = default; + + // Disable copy and move operations because the memory address is used as key. + gil_safe_call_once_and_store(const gil_safe_call_once_and_store &) = delete; + gil_safe_call_once_and_store(gil_safe_call_once_and_store &&) = delete; + gil_safe_call_once_and_store &operator=(const gil_safe_call_once_and_store &) = delete; + gil_safe_call_once_and_store &operator=(gil_safe_call_once_and_store &&) = delete; + +private: + using storage_type = detail::call_once_storage; + + // Indicator of fast path for single-interpreter case. + bool is_last_storage_valid() const { + return is_initialized_by_at_least_one_interpreter_ + && !detail::has_seen_non_main_interpreter(); + } + + // Get the unique key for this storage instance in the interpreter's state dict. + // The return type should not be `py::str` because PyObject is interpreter-dependent. + std::string get_storage_key() const { + // The instance is expected to be global static, so using its address as unique identifier. + // The typical usage is like: + // + // PYBIND11_CONSTINIT static gil_safe_call_once_and_store storage; + // + return PYBIND11_CALL_ONCE_STORAGE_KEY_PREFIX + + std::to_string(reinterpret_cast(this)); + } + + // Get or create per-storage capsule in the current interpreter's state dict. + // The storage is interpreter-dependent and will not be shared across interpreters. + storage_type *get_or_create_storage_in_state_dict() { + return detail::atomic_get_or_create_in_state_dict(get_storage_key().c_str()) + .first; + } + + // No storage needed when subinterpreter support is enabled. + // The actual storage is stored in the per-interpreter state dict via + // `get_or_create_storage_in_state_dict()`. + + // Fast local cache to avoid repeated lookups when there are no multiple interpreters. + // This is only valid if there is a single interpreter. Otherwise, it is not used. + // WARNING: We cannot use thread local cache similar to `internals_pp_manager::internals_p_tls` + // because the thread local storage cannot be explicitly invalidated when interpreters + // are destroyed (unlike `internals_pp_manager` which has explicit hooks for that). + T *last_storage_ptr_ = nullptr; + // This flag is true if the value has been initialized by any interpreter (may not be the + // current one). + detail::atomic_bool is_initialized_by_at_least_one_interpreter_{false}; +}; +#endif PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/iostream.h b/pybind11/include/pybind11/iostream.h index 1878089..df7fa3c 100644 --- a/pybind11/include/pybind11/iostream.h +++ b/pybind11/include/pybind11/iostream.h @@ -117,13 +117,36 @@ class pythonbuf : public std::streambuf { int sync() override { return _sync(); } public: + // Minimum buffer size must accommodate the largest incomplete UTF-8 sequence + // (3 bytes) plus one position reserved for overflow(), i.e. 4 bytes total. + static constexpr size_t minimum_buffer_size = 4; + explicit pythonbuf(const object &pyostream, size_t buffer_size = 1024) - : buf_size(buffer_size), d_buffer(new char[buf_size]), pywrite(pyostream.attr("write")), + : buf_size(buffer_size < minimum_buffer_size // ternary avoids C++14 std::max ODR-use of + // static constexpr + ? minimum_buffer_size + : buffer_size), + d_buffer(new char[buf_size]), pywrite(pyostream.attr("write")), pyflush(pyostream.attr("flush")) { setp(d_buffer.get(), d_buffer.get() + buf_size - 1); } - pythonbuf(pythonbuf &&) = default; + pythonbuf(pythonbuf &&other) noexcept + : buf_size(other.buf_size), d_buffer(std::move(other.d_buffer)), + pywrite(std::move(other.pywrite)), pyflush(std::move(other.pyflush)) { + const auto pending = (other.pbase() != nullptr && other.pptr() != nullptr) + ? static_cast(other.pptr() - other.pbase()) + : 0; + if (d_buffer != nullptr) { + // Rebuild the put area from the transferred storage. + setp(d_buffer.get(), d_buffer.get() + buf_size - 1); + pbump(pending); + } else { + setp(nullptr, nullptr); + } + // Prevent the moved-from destructor from flushing through moved-out handles. + other.setp(nullptr, nullptr); + } /// Sync before destroy ~pythonbuf() override { _sync(); } @@ -161,6 +184,7 @@ class scoped_ostream_redirect { std::streambuf *old; std::ostream &costream; detail::pythonbuf buffer; + bool active = true; public: explicit scoped_ostream_redirect(std::ostream &costream = std::cout, @@ -170,10 +194,22 @@ class scoped_ostream_redirect { old = costream.rdbuf(&buffer); } - ~scoped_ostream_redirect() { costream.rdbuf(old); } + ~scoped_ostream_redirect() { + if (active) { + costream.rdbuf(old); + } + } scoped_ostream_redirect(const scoped_ostream_redirect &) = delete; - scoped_ostream_redirect(scoped_ostream_redirect &&other) = default; + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + scoped_ostream_redirect(scoped_ostream_redirect &&other) + : old(other.old), costream(other.costream), buffer(std::move(other.buffer)), + active(other.active) { + if (active) { + costream.rdbuf(&buffer); // Re-point stream to our buffer + other.active = false; + } + } scoped_ostream_redirect &operator=(const scoped_ostream_redirect &) = delete; scoped_ostream_redirect &operator=(scoped_ostream_redirect &&) = delete; }; diff --git a/pybind11/include/pybind11/native_enum.h b/pybind11/include/pybind11/native_enum.h index 5537218..af166d0 100644 --- a/pybind11/include/pybind11/native_enum.h +++ b/pybind11/include/pybind11/native_enum.h @@ -22,12 +22,12 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_type::type; - native_enum(const object &parent_scope, + native_enum(handle parent_scope, const char *name, const char *native_type_name, const char *class_doc = "") : detail::native_enum_data( - parent_scope, name, native_type_name, class_doc, std::type_index(typeid(EnumType))) { + parent_scope, name, native_type_name, class_doc, make_record()) { if (detail::get_local_type_info(typeid(EnumType)) != nullptr || detail::get_global_type_info(typeid(EnumType)) != nullptr) { pybind11_fail( @@ -62,6 +62,15 @@ class native_enum : public detail::native_enum_data { native_enum(const native_enum &) = delete; native_enum &operator=(const native_enum &) = delete; + +private: + static detail::native_enum_record make_record() { + detail::native_enum_record ret; + ret.cpptype = &typeid(EnumType); + ret.size_bytes = sizeof(EnumType); + ret.is_signed = std::is_signed::value; + return ret; + } }; PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/numpy.h b/pybind11/include/pybind11/numpy.h index 7f62157..10c0c94 100644 --- a/pybind11/include/pybind11/numpy.h +++ b/pybind11/include/pybind11/numpy.h @@ -29,6 +29,10 @@ #include #include +#ifdef PYBIND11_HAS_SPAN +# include +#endif + #if defined(PYBIND11_NUMPY_1_ONLY) # error "PYBIND11_NUMPY_1_ONLY is no longer supported (see PR #5595)." #endif @@ -416,7 +420,7 @@ struct npy_format_descriptor_name::value>> { || std::is_same::value > (const_name("numpy.complex") + const_name(), - const_name("numpy.longcomplex")); + const_name("numpy.clongdouble")); }; template @@ -1143,6 +1147,13 @@ class array : public buffer { /// Dimensions of the array const ssize_t *shape() const { return detail::array_proxy(m_ptr)->dimensions; } +#ifdef PYBIND11_HAS_SPAN + /// Dimensions of the array as a span + std::span shape_span() const { + return std::span(shape(), static_cast(ndim())); + } +#endif + /// Dimension along a given axis ssize_t shape(ssize_t dim) const { if (dim >= ndim()) { @@ -1154,6 +1165,13 @@ class array : public buffer { /// Strides of the array const ssize_t *strides() const { return detail::array_proxy(m_ptr)->strides; } +#ifdef PYBIND11_HAS_SPAN + /// Strides of the array as a span + std::span strides_span() const { + return std::span(strides(), static_cast(ndim())); + } +#endif + /// Stride along a given axis ssize_t strides(ssize_t dim) const { if (dim >= ndim()) { @@ -1860,7 +1878,7 @@ class common_iterator { using value_type = container_type::value_type; using size_type = container_type::size_type; - common_iterator() : m_strides() {} + common_iterator() = default; common_iterator(void *ptr, const container_type &strides, const container_type &shape) : p_ptr(reinterpret_cast(ptr)), m_strides(strides.size()) { @@ -2309,4 +2327,86 @@ Helper vectorize(Return (Class::*f)(Args...) const) { return Helper(std::mem_fn(f)); } +// Intentionally no &&/const&& overloads: vectorized method calls operate on the bound Python +// instance and should not consume/move-from self. +// Vectorize a class method (non-const, lvalue ref-qualified): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) &) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, lvalue ref-qualified): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const &) { + return Helper(std::mem_fn(f)); +} + +#ifdef __cpp_noexcept_function_type +// Vectorize a class method (non-const, noexcept): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, noexcept): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (non-const, lvalue ref-qualified, noexcept): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) & noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, lvalue ref-qualified, noexcept): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const & noexcept) { + return Helper(std::mem_fn(f)); +} +#endif + PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/pybind11.h b/pybind11/include/pybind11/pybind11.h index 117feca..b5a97db 100644 --- a/pybind11/include/pybind11/pybind11.h +++ b/pybind11/include/pybind11/pybind11.h @@ -43,6 +43,12 @@ PYBIND11_WARNING_DISABLE_CLANG("-Wgnu-zero-variadic-macro-arguments") # include #endif +#if defined(__cpp_if_constexpr) && __cpp_if_constexpr >= 201606 +# define PYBIND11_MAYBE_CONSTEXPR constexpr +#else +# define PYBIND11_MAYBE_CONSTEXPR +#endif + PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) /* https://stackoverflow.com/questions/46798456/handling-gccs-noexcept-type-warning @@ -120,7 +126,8 @@ inline std::string generate_function_signature(const char *type_caster_name_fiel const auto c = *pc; if (c == '{') { // Write arg name for everything except *args and **kwargs. - is_starred = *(pc + 1) == '*'; + // Detect {@*args...} or {@**kwargs...} + is_starred = *(pc + 1) == '@' && *(pc + 2) == '*'; if (is_starred) { continue; } @@ -158,7 +165,7 @@ inline std::string generate_function_signature(const char *type_caster_name_fiel pybind11_fail("Internal error while parsing type signature (1)"); } if (auto *tinfo = detail::get_type_info(*t)) { - handle th((PyObject *) tinfo->type); + handle th(reinterpret_cast(tinfo->type)); signature += th.attr("__module__").cast() + "." + th.attr("__qualname__").cast(); } else if (auto th = detail::global_internals_native_enum_type_map_get_item(*t)) { @@ -248,6 +255,49 @@ inline std::string generate_type_signature() { # define PYBIND11_COMPAT_STRDUP strdup #endif +#define PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR \ + detail::const_name("(") + cast_in::arg_names + detail::const_name(") -> ") + cast_out::name + +// We factor out readable function signatures to a specific template +// so that they don't get duplicated across different instantiations of +// cpp_function::initialize (which is templated on more types). +template +class ReadableFunctionSignature { +public: + using sig_type = decltype(PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR); + +private: + // We have to repeat PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR in decltype() + // because C++11 doesn't allow functions to return `auto`. (We don't + // know the type because it's some variant of detail::descr with + // unknown N.) + static constexpr sig_type sig() { return PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR; } + +public: + static constexpr sig_type kSig = sig(); + // We can only stash the result of detail::descr::types() in a + // constexpr variable if we aren't on MSVC (see + // PYBIND11_DESCR_CONSTEXPR). +#if !defined(_MSC_VER) + using types_type = decltype(sig_type::types()); + static constexpr types_type kTypes = sig_type::types(); +#endif +}; +#undef PYBIND11_READABLE_FUNCTION_SIGNATURE_EXPR + +// Prior to C++17, we don't have inline variables, so we have to +// provide an out-of-line definition of the class member. +#if !defined(PYBIND11_CPP17) +template +constexpr typename ReadableFunctionSignature::sig_type + ReadableFunctionSignature::kSig; +# if !defined(_MSC_VER) +template +constexpr typename ReadableFunctionSignature::types_type + ReadableFunctionSignature::kTypes; +# endif +#endif + PYBIND11_NAMESPACE_END(detail) /// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object @@ -319,6 +369,96 @@ class cpp_function : public function { extra...); } + /// Construct a cpp_function from a class method (non-const, rvalue ref-qualifier) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) &&, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, rvalue ref-qualifier) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const &&, const Extra &...extra) { + initialize( + [f](const Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + +#ifdef __cpp_noexcept_function_type + /// Construct a cpp_function from a class method (non-const, no ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (non-const, lvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) & noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, no ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const noexcept, const Extra &...extra) { + initialize([f](const Class *c, + Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, lvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const & noexcept, const Extra &...extra) { + initialize([f](const Class *c, + Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (non-const, rvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) && noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, rvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const && noexcept, const Extra &...extra) { + initialize( + [f](const Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } +#endif + /// Return the function name object name() const { return attr("__name__"); } @@ -481,9 +621,14 @@ class cpp_function : public function { /* Generate a readable signature describing the function's arguments and return value types */ - static constexpr auto signature - = const_name("(") + cast_in::arg_names + const_name(") -> ") + cast_out::name; - PYBIND11_DESCR_CONSTEXPR auto types = decltype(signature)::types(); + static constexpr const auto &signature + = detail::ReadableFunctionSignature::kSig; +#if !defined(_MSC_VER) + static constexpr const auto &types + = detail::ReadableFunctionSignature::kTypes; +#else + PYBIND11_DESCR_CONSTEXPR auto types = std::decay::type::types(); +#endif /* Register the function with Python from generic (non-templated) code */ // Pass on the ownership over the `unique_rec` to `initialize_generic`. `rec` stays valid. @@ -593,7 +738,7 @@ class cpp_function : public function { rec->signature = guarded_strdup(signature.c_str()); rec->args.shrink_to_fit(); - rec->nargs = (std::uint16_t) args; + rec->nargs = static_cast(args); if (rec->sibling && PYBIND11_INSTANCE_METHOD_CHECK(rec->sibling.ptr())) { rec->sibling = PYBIND11_INSTANCE_METHOD_GET_FUNCTION(rec->sibling.ptr()); @@ -629,10 +774,10 @@ class cpp_function : public function { rec->def->ml_name = rec->name; rec->def->ml_meth = reinterpret_cast(reinterpret_cast(dispatcher)); - rec->def->ml_flags = METH_VARARGS | METH_KEYWORDS; + rec->def->ml_flags = METH_FASTCALL | METH_KEYWORDS; object py_func_rec = detail::function_record_PyObject_New(); - ((detail::function_record_PyObject *) py_func_rec.ptr())->cpp_func_rec + (reinterpret_cast(py_func_rec.ptr()))->cpp_func_rec = unique_rec.release(); guarded_strdup.release(); @@ -666,8 +811,8 @@ class cpp_function : public function { // chain. chain_start = rec; rec->next = chain; - auto *py_func_rec - = (detail::function_record_PyObject *) PyCFunction_GET_SELF(m_ptr); + auto *py_func_rec = reinterpret_cast( + PyCFunction_GET_SELF(m_ptr)); py_func_rec->cpp_func_rec = unique_rec.release(); guarded_strdup.release(); } else { @@ -727,7 +872,7 @@ class cpp_function : public function { } } - auto *func = (PyCFunctionObject *) m_ptr; + auto *func = reinterpret_cast(m_ptr); // Install docstring if it's non-empty (when at least one option is enabled) auto *doc = signatures.empty() ? nullptr : PYBIND11_COMPAT_STRDUP(signatures.c_str()); std::free(const_cast(PYBIND11_PYCFUNCTION_GET_DOC(func))); @@ -762,9 +907,9 @@ class cpp_function : public function { // so they cannot be freed. Once the function has been created, they can. // Check `make_function_record` for more details. if (free_strings) { - std::free((char *) rec->name); - std::free((char *) rec->doc); - std::free((char *) rec->signature); + std::free(rec->name); + std::free(rec->doc); + std::free(rec->signature); for (auto &arg : rec->args) { std::free(const_cast(arg.name)); std::free(const_cast(arg.descr)); @@ -792,7 +937,8 @@ class cpp_function : public function { } /// Main dispatch logic for calls to functions bound using pybind11 - static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) { + static PyObject * + dispatcher(PyObject *self, PyObject *const *args_in_arr, size_t nargsf, PyObject *kwnames_in) { using namespace detail; const function_record *overloads = function_record_ptr_from_PyObject(self); assert(overloads != nullptr); @@ -802,9 +948,9 @@ class cpp_function : public function { /* Need to know how many arguments + keyword arguments there are to pick the right overload */ - const auto n_args_in = (size_t) PyTuple_GET_SIZE(args_in); + const auto n_args_in = static_cast(PyVectorcall_NARGS(nargsf)); - handle parent = n_args_in > 0 ? PyTuple_GET_ITEM(args_in, 0) : nullptr, + handle parent = n_args_in > 0 ? args_in_arr[0] : nullptr, result = PYBIND11_TRY_NEXT_OVERLOAD; auto self_value_and_holder = value_and_holder(); @@ -816,7 +962,8 @@ class cpp_function : public function { return nullptr; } - auto *const tinfo = get_type_info((PyTypeObject *) overloads->scope.ptr()); + auto *const tinfo + = get_type_info(reinterpret_cast(overloads->scope.ptr())); auto *const pi = reinterpret_cast(parent.ptr()); self_value_and_holder = pi->get_value_and_holder(tinfo, true); @@ -892,7 +1039,7 @@ class cpp_function : public function { self_value_and_holder.type->dealloc(self_value_and_holder); } - call.init_self = PyTuple_GET_ITEM(args_in, 0); + call.init_self = args_in_arr[0]; call.args.emplace_back(reinterpret_cast(&self_value_and_holder)); call.args_convert.push_back(false); ++args_copied; @@ -903,17 +1050,24 @@ class cpp_function : public function { for (; args_copied < args_to_copy; ++args_copied) { const argument_record *arg_rec = args_copied < func.args.size() ? &func.args[args_copied] : nullptr; - if (kwargs_in && arg_rec && arg_rec->name - && dict_getitemstring(kwargs_in, arg_rec->name)) { + + /* if the argument is listed in the call site's kwargs, but the argument is + also fulfilled positionally, then the call can't match this overload. for + example, the call site is: foo(0, key=1) but our overload is foo(key:int) then + this call can't be for us, because it would be invalid. + */ + if (kwnames_in && arg_rec && arg_rec->name + && keyword_index(kwnames_in, arg_rec->name) >= 0) { bad_arg = true; break; } - handle arg(PyTuple_GET_ITEM(args_in, args_copied)); + handle arg(args_in_arr[args_copied]); if (arg_rec && !arg_rec->none && arg.is_none()) { bad_arg = true; break; } + call.args.push_back(arg); call.args_convert.push_back(arg_rec ? arg_rec->convert : true); } @@ -925,20 +1079,12 @@ class cpp_function : public function { // to copy the rest into a py::args argument. size_t positional_args_copied = args_copied; - // We'll need to copy this if we steal some kwargs for defaults - dict kwargs = reinterpret_borrow(kwargs_in); - // 1.5. Fill in any missing pos_only args from defaults if they exist if (args_copied < func.nargs_pos_only) { for (; args_copied < func.nargs_pos_only; ++args_copied) { const auto &arg_rec = func.args[args_copied]; - handle value; - if (arg_rec.value) { - value = arg_rec.value; - } - if (value) { - call.args.push_back(value); + call.args.push_back(arg_rec.value); call.args_convert.push_back(arg_rec.convert); } else { break; @@ -951,46 +1097,42 @@ class cpp_function : public function { } // 2. Check kwargs and, failing that, defaults that may help complete the list + small_vector used_kwargs( + kwnames_in ? static_cast(PyTuple_GET_SIZE(kwnames_in)) : 0, false); + size_t used_kwargs_count = 0; if (args_copied < num_args) { - bool copied_kwargs = false; - for (; args_copied < num_args; ++args_copied) { const auto &arg_rec = func.args[args_copied]; handle value; - if (kwargs_in && arg_rec.name) { - value = dict_getitemstring(kwargs.ptr(), arg_rec.name); + if (kwnames_in && arg_rec.name) { + ssize_t i = keyword_index(kwnames_in, arg_rec.name); + if (i >= 0) { + value = args_in_arr[n_args_in + static_cast(i)]; + used_kwargs.set(static_cast(i), true); + used_kwargs_count++; + } } - if (value) { - // Consume a kwargs value - if (!copied_kwargs) { - kwargs = reinterpret_steal(PyDict_Copy(kwargs.ptr())); - copied_kwargs = true; - } - if (PyDict_DelItemString(kwargs.ptr(), arg_rec.name) == -1) { - throw error_already_set(); - } - } else if (arg_rec.value) { + if (!value) { value = arg_rec.value; + if (!value) { + break; + } } if (!arg_rec.none && value.is_none()) { break; } - if (value) { - // If we're at the py::args index then first insert a stub for it to be - // replaced later - if (func.has_args && call.args.size() == func.nargs_pos) { - call.args.push_back(none()); - } - - call.args.push_back(value); - call.args_convert.push_back(arg_rec.convert); - } else { - break; + // If we're at the py::args index then first insert a stub for it to be + // replaced later + if (func.has_args && call.args.size() == func.nargs_pos) { + call.args.push_back(none()); } + + call.args.push_back(value); + call.args_convert.push_back(arg_rec.convert); } if (args_copied < num_args) { @@ -1000,47 +1142,50 @@ class cpp_function : public function { } // 3. Check everything was consumed (unless we have a kwargs arg) - if (kwargs && !kwargs.empty() && !func.has_kwargs) { + if (!func.has_kwargs && used_kwargs_count < used_kwargs.size()) { continue; // Unconsumed kwargs, but no py::kwargs argument to accept them } // 4a. If we have a py::args argument, create a new tuple with leftovers if (func.has_args) { - tuple extra_args; - if (args_to_copy == 0) { - // We didn't copy out any position arguments from the args_in tuple, so we - // can reuse it directly without copying: - extra_args = reinterpret_borrow(args_in); - } else if (positional_args_copied >= n_args_in) { - extra_args = tuple(0); + if (positional_args_copied >= n_args_in) { + call.args_ref = tuple(0); } else { size_t args_size = n_args_in - positional_args_copied; - extra_args = tuple(args_size); + tuple extra_args(args_size); for (size_t i = 0; i < args_size; ++i) { - extra_args[i] = PyTuple_GET_ITEM(args_in, positional_args_copied + i); + extra_args[i] = args_in_arr[positional_args_copied + i]; } + call.args_ref = std::move(extra_args); } if (call.args.size() <= func.nargs_pos) { - call.args.push_back(extra_args); + call.args.push_back(call.args_ref); } else { - call.args[func.nargs_pos] = extra_args; + call.args[func.nargs_pos] = call.args_ref; } call.args_convert.push_back(false); - call.args_ref = std::move(extra_args); } // 4b. If we have a py::kwargs, pass on any remaining kwargs if (func.has_kwargs) { - if (!kwargs.ptr()) { - kwargs = dict(); // If we didn't get one, send an empty one + dict kwargs; + for (size_t i = 0; i < used_kwargs.size(); ++i) { + if (!used_kwargs[i]) { + // Cast values into handles before indexing into kwargs to ensure + // well-defined evaluation order (MSVC C4866). + handle arg_in_arr = args_in_arr[n_args_in + i], + kwname = PyTuple_GET_ITEM(kwnames_in, i); + kwargs[kwname] = arg_in_arr; + } } call.args.push_back(kwargs); call.args_convert.push_back(false); call.kwargs_ref = std::move(kwargs); } -// 5. Put everything in a vector. Not technically step 5, we've been building it -// in `call.args` all along. + // 5. Put everything in a vector. Not technically step 5, we've been building it + // in `call.args` all along. + #if defined(PYBIND11_DETAILED_ERROR_MESSAGES) if (call.args.size() != func.nargs || call.args_convert.size() != func.nargs) { pybind11_fail("Internal error: function call dispatcher inserted wrong number " @@ -1048,13 +1193,14 @@ class cpp_function : public function { } #endif - std::vector second_pass_convert; + args_convert_vector second_pass_convert; if (overloaded) { // We're in the first no-convert pass, so swap out the conversion flags for a // set of all-false flags. If the call fails, we'll swap the flags back in for // the conversion-allowed call below. - second_pass_convert.resize(func.nargs, false); - call.args_convert.swap(second_pass_convert); + second_pass_convert = std::move(call.args_convert); + call.args_convert + = args_convert_vector(func.nargs, false); } // 6. Call the function. @@ -1170,40 +1316,37 @@ class cpp_function : public function { msg += '\n'; } msg += "\nInvoked with: "; - auto args_ = reinterpret_borrow(args_in); bool some_args = false; - for (size_t ti = overloads->is_constructor ? 1 : 0; ti < args_.size(); ++ti) { + for (size_t ti = overloads->is_constructor ? 1 : 0; ti < n_args_in; ++ti) { if (!some_args) { some_args = true; } else { msg += ", "; } try { - msg += pybind11::repr(args_[ti]); + msg += pybind11::repr(args_in_arr[ti]); } catch (const error_already_set &) { msg += ""; } } - if (kwargs_in) { - auto kwargs = reinterpret_borrow(kwargs_in); - if (!kwargs.empty()) { - if (some_args) { - msg += "; "; + if (kwnames_in && PyTuple_GET_SIZE(kwnames_in) > 0) { + if (some_args) { + msg += "; "; + } + msg += "kwargs: "; + bool first = true; + for (size_t i = 0; i < static_cast(PyTuple_GET_SIZE(kwnames_in)); ++i) { + if (first) { + first = false; + } else { + msg += ", "; } - msg += "kwargs: "; - bool first = true; - for (const auto &kwarg : kwargs) { - if (first) { - first = false; - } else { - msg += ", "; - } - msg += pybind11::str("{}=").format(kwarg.first); - try { - msg += pybind11::repr(kwarg.second); - } catch (const error_already_set &) { - msg += ""; - } + msg += reinterpret_borrow(PyTuple_GET_ITEM(kwnames_in, i)); + msg += '='; + try { + msg += pybind11::repr(args_in_arr[n_args_in + i]); + } catch (const error_already_set &) { + msg += ""; } } } @@ -1238,6 +1381,28 @@ class cpp_function : public function { } return result.ptr(); } + + static ssize_t keyword_index(PyObject *haystack, char const *needle) { + /* kwargs is usually very small (<= 5 entries). The arg strings are typically interned. + * CPython itself implements the search this way, first comparing all pointers ... which is + * cheap and will work if the strings are interned. If it fails, then it falls back to a + * second lexicographic check. This is wildly expensive for huge argument lists, but those + * are incredibly rare so we optimize for the vastly common case of just a couple of args. + */ + auto n = PyTuple_GET_SIZE(haystack); + auto s = reinterpret_steal(PyUnicode_InternFromString(needle)); + for (ssize_t i = 0; i < n; ++i) { + if (PyTuple_GET_ITEM(haystack, i) == s.ptr()) { + return i; + } + } + for (ssize_t i = 0; i < n; ++i) { + if (PyUnicode_Compare(PyTuple_GET_ITEM(haystack, i), s.ptr()) == 0) { + return i; + } + } + return -1; + } }; PYBIND11_NAMESPACE_BEGIN(detail) @@ -1246,9 +1411,15 @@ PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) // This implementation needs the definition of `class cpp_function`. inline void tp_dealloc_impl(PyObject *self) { - auto *py_func_rec = (function_record_PyObject *) self; + // Save type before PyObject_Free invalidates self. + auto *type = Py_TYPE(self); + auto *py_func_rec = reinterpret_cast(self); cpp_function::destruct(py_func_rec->cpp_func_rec); py_func_rec->cpp_func_rec = nullptr; + // PyObject_New increments the heap type refcount and allocates via + // PyObject_Malloc; balance both here + PyObject_Free(self); + Py_DECREF(type); } PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) @@ -1619,7 +1790,7 @@ class generic_type : public object { /* Register supplemental type information in C++ dict */ auto *tinfo = new detail::type_info(); - tinfo->type = (PyTypeObject *) m_ptr; + tinfo->type = reinterpret_cast(m_ptr); tinfo->cpptype = rec.type; tinfo->type_size = rec.type_size; tinfo->type_align = rec.type_align; @@ -1636,10 +1807,14 @@ class generic_type : public object { with_internals([&](internals &internals) { auto tindex = std::type_index(*rec.type); tinfo->direct_conversions = &internals.direct_conversions[tindex]; + auto &local_internals = get_local_internals(); if (rec.module_local) { - get_local_internals().registered_types_cpp[tindex] = tinfo; + local_internals.registered_types_cpp[rec.type] = tinfo; } else { internals.registered_types_cpp[tindex] = tinfo; +#if PYBIND11_INTERNALS_VERSION >= 12 + internals.registered_types_cpp_fast[rec.type] = tinfo; +#endif } PYBIND11_WARNING_PUSH @@ -1650,7 +1825,7 @@ class generic_type : public object { PYBIND11_WARNING_DISABLE_GCC("-Warray-bounds") PYBIND11_WARNING_DISABLE_GCC("-Wstringop-overread") #endif - internals.registered_types_py[(PyTypeObject *) m_ptr] = {tinfo}; + internals.registered_types_py[reinterpret_cast(m_ptr)] = {tinfo}; PYBIND11_WARNING_POP }); @@ -1658,7 +1833,8 @@ class generic_type : public object { mark_parents_nonsimple(tinfo->type); tinfo->simple_ancestors = false; } else if (rec.bases.size() == 1) { - auto *parent_tinfo = get_type_info((PyTypeObject *) rec.bases[0].ptr()); + auto *parent_tinfo + = get_type_info(reinterpret_cast(rec.bases[0].ptr())); assert(parent_tinfo != nullptr); bool parent_simple_ancestors = parent_tinfo->simple_ancestors; tinfo->simple_ancestors = parent_simple_ancestors; @@ -1677,17 +1853,17 @@ class generic_type : public object { void mark_parents_nonsimple(PyTypeObject *value) { auto t = reinterpret_borrow(value->tp_bases); for (handle h : t) { - auto *tinfo2 = get_type_info((PyTypeObject *) h.ptr()); + auto *tinfo2 = get_type_info(reinterpret_cast(h.ptr())); if (tinfo2) { tinfo2->simple_type = false; } - mark_parents_nonsimple((PyTypeObject *) h.ptr()); + mark_parents_nonsimple(reinterpret_cast(h.ptr())); } } void install_buffer_funcs(buffer_info *(*get_buffer)(PyObject *, void *), void *get_buffer_data) { - auto *type = (PyHeapTypeObject *) m_ptr; + auto *type = reinterpret_cast(m_ptr); auto *tinfo = detail::get_type_info(&type->ht_type); if (!type->ht_type.tp_as_buffer) { @@ -1709,8 +1885,8 @@ class generic_type : public object { const auto is_static = (rec_func != nullptr) && !(rec_func->is_method && rec_func->scope); const auto has_doc = (rec_func != nullptr) && (rec_func->doc != nullptr) && pybind11::options::show_user_defined_docstrings(); - auto property = handle( - (PyObject *) (is_static ? get_internals().static_property_type : &PyProperty_Type)); + auto property = handle(reinterpret_cast( + is_static ? get_internals().static_property_type : &PyProperty_Type)); attr(name) = property(fget.ptr() ? fget : none(), fset.ptr() ? fset : none(), /*deleter*/ none(), @@ -1779,29 +1955,89 @@ inline void add_class_method(object &cls, const char *name_, const cpp_function } } -PYBIND11_NAMESPACE_END(detail) - -/// Given a pointer to a member function, cast it to its `Derived` version. -/// Forward everything else unchanged. -template -auto method_adaptor(F &&f) -> decltype(std::forward(f)) { - return std::forward(f); -} - +/// Type trait to rebind a member function pointer's class to `Derived`, preserving all +/// cv/ref/noexcept qualifiers. The primary template has no `type` member, providing SFINAE +/// failure for unsupported member function pointer types. `source_class` holds the original +/// class for use in `is_accessible_base_of` checks. +template +struct rebind_member_ptr {}; + +// Define one specialization per supported qualifier combination via a local macro. +// The qualifiers argument appears in type position, not expression position, so +// parenthesizing it would produce invalid C++. +// The no-qualifier specialization is written out explicitly to avoid invoking the macro with an +// empty argument, which triggers MSVC warning C4003. template -auto method_adaptor(Return (Class::*pmf)(Args...)) -> Return (Derived::*)(Args...) { +struct rebind_member_ptr { + using type = Return (Derived::*)(Args...); + using source_class = Class; +}; +// NOLINTBEGIN(bugprone-macro-parentheses) +#define PYBIND11_REBIND_MEMBER_PTR(qualifiers) \ + template \ + struct rebind_member_ptr { \ + using type = Return (Derived::*)(Args...) qualifiers; \ + using source_class = Class; \ + } +PYBIND11_REBIND_MEMBER_PTR(const); +PYBIND11_REBIND_MEMBER_PTR(&); +PYBIND11_REBIND_MEMBER_PTR(const &); +PYBIND11_REBIND_MEMBER_PTR(&&); +PYBIND11_REBIND_MEMBER_PTR(const &&); +#ifdef __cpp_noexcept_function_type +PYBIND11_REBIND_MEMBER_PTR(noexcept); +PYBIND11_REBIND_MEMBER_PTR(const noexcept); +PYBIND11_REBIND_MEMBER_PTR(& noexcept); +PYBIND11_REBIND_MEMBER_PTR(const & noexcept); +PYBIND11_REBIND_MEMBER_PTR(&& noexcept); +PYBIND11_REBIND_MEMBER_PTR(const && noexcept); +#endif +#undef PYBIND11_REBIND_MEMBER_PTR +// NOLINTEND(bugprone-macro-parentheses) + +/// Shared implementation body for all method_adaptor member-function-pointer overloads. +/// Asserts Base is accessible from Derived, then casts the member pointer. +template , + typename Adapted = typename Traits::type> +constexpr PYBIND11_ALWAYS_INLINE Adapted adapt_member_ptr(T pmf) { static_assert( - detail::is_accessible_base_of::value, + detail::is_accessible_base_of::value, "Cannot bind an inaccessible base class method; use a lambda definition instead"); return pmf; } -template -auto method_adaptor(Return (Class::*pmf)(Args...) const) -> Return (Derived::*)(Args...) const { - static_assert( - detail::is_accessible_base_of::value, - "Cannot bind an inaccessible base class method; use a lambda definition instead"); - return pmf; +PYBIND11_NAMESPACE_END(detail) + +/// Given a pointer to a member function, cast it to its `Derived` version. +/// For all other callables (lambdas, function pointers, etc.), forward unchanged. +/// +/// Two overloads cover all cases without explicit per-qualifier instantiations: +/// +/// (1) Generic fallback โ€” disabled for member function pointers so that (2) wins +/// without any partial-ordering ambiguity. +/// (2) MFP overload โ€” SFINAE on rebind_member_ptr::type, which exists for every +/// supported qualifier combination (const, &, &&, noexcept, ...). A single +/// template therefore covers all combinations that rebind_member_ptr handles. +template < + typename /*Derived*/, + typename F, + detail::enable_if_t>::value, + int> = 0> +constexpr auto method_adaptor(F &&f) -> decltype(std::forward(f)) { + return std::forward(f); +} + +template ::type> +constexpr Adapted method_adaptor(T pmf) { + // Expected to be redundant (SFINAE on rebind_member_ptr) but cheap and makes the intent + // explicit. + static_assert(std::is_member_function_pointer::value, + "method_adaptor: T must be a member function pointer"); + return detail::adapt_member_ptr(pmf); } PYBIND11_NAMESPACE_BEGIN(detail) @@ -2137,10 +2373,18 @@ class class_ : public detail::generic_type { if (has_alias) { with_internals([&](internals &internals) { - auto &instances = record.module_local ? get_local_internals().registered_types_cpp - : internals.registered_types_cpp; - instances[std::type_index(typeid(type_alias))] - = instances[std::type_index(typeid(type))]; + auto &local_internals = get_local_internals(); + if (record.module_local) { + local_internals.registered_types_cpp[&typeid(type_alias)] + = local_internals.registered_types_cpp[&typeid(type)]; + } else { + type_info *const val + = internals.registered_types_cpp[std::type_index(typeid(type))]; + internals.registered_types_cpp[std::type_index(typeid(type_alias))] = val; +#if PYBIND11_INTERNALS_VERSION >= 12 + internals.registered_types_cpp_fast[&typeid(type_alias)] = val; +#endif + } }); } def("_pybind11_conduit_v1_", cpp_conduit_method); @@ -2151,6 +2395,13 @@ class class_ : public detail::generic_type { rec.add_base(typeid(Base), [](void *src) -> void * { return static_cast(reinterpret_cast(src)); }); + // Virtual inheritance means the base subobject is at a dynamic offset, + // so the reinterpret_cast shortcut in load_impl Case 2a is invalid. + // Force the MI path (implicit_casts) for correct pointer adjustment. + // Detection: static_cast(Base*) is ill-formed for virtual bases. + if PYBIND11_MAYBE_CONSTEXPR (!detail::is_static_downcastable::value) { + rec.multiple_inheritance = true; + } } template ::value, int> = 0> @@ -2252,6 +2503,40 @@ class class_ : public detail::generic_type { return def_buffer([func](const type &obj) { return (obj.*func)(); }); } + // Intentionally no &&/const&& overloads: buffer protocol callbacks are invoked on an + // existing Python object and should not move-from self. + template + class_ &def_buffer(Return (Class::*func)(Args...) &) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const &) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } + +#ifdef __cpp_noexcept_function_type + template + class_ &def_buffer(Return (Class::*func)(Args...) noexcept) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const noexcept) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) & noexcept) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const & noexcept) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } +#endif + template class_ &def_readwrite(const char *name, D C::*pm, const Extra &...extra) { static_assert(std::is_same::value || std::is_base_of::value, @@ -2368,24 +2653,52 @@ class class_ : public detail::generic_type { const Extra &...extra) { static_assert(0 == detail::constexpr_sum(std::is_base_of::value...), "Argument annotations are not allowed for properties"); + static_assert(0 == detail::constexpr_sum(detail::is_call_guard::value...), + "def_property family does not currently support call_guard. Use a " + "py::cpp_function instead."); + static_assert(0 == detail::constexpr_sum(detail::is_keep_alive::value...), + "def_property family does not currently support keep_alive. Use a " + "py::cpp_function instead."); auto rec_fget = get_function_record(fget), rec_fset = get_function_record(fset); auto *rec_active = rec_fget; if (rec_fget) { char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */ + auto args_before = rec_fget->args.size(); detail::process_attributes::init(extra..., rec_fget); if (rec_fget->doc && rec_fget->doc != doc_prev) { std::free(doc_prev); rec_fget->doc = PYBIND11_COMPAT_STRDUP(rec_fget->doc); } + // Args added by process_attributes (e.g. "self" via is_method + pos_only/kw_only) + // need their strings strdup'd: initialize_generic's strdup loop already ran during + // cpp_function construction, so it won't process these late additions. Without this, + // destruct() would call free() on string literals. See gh-5976. + for (auto i = args_before; i < rec_fget->args.size(); ++i) { + if (rec_fget->args[i].name) { + rec_fget->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].name); + } + if (rec_fget->args[i].descr) { + rec_fget->args[i].descr = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].descr); + } + } } if (rec_fset) { char *doc_prev = rec_fset->doc; + auto args_before = rec_fset->args.size(); detail::process_attributes::init(extra..., rec_fset); if (rec_fset->doc && rec_fset->doc != doc_prev) { std::free(doc_prev); rec_fset->doc = PYBIND11_COMPAT_STRDUP(rec_fset->doc); } + for (auto i = args_before; i < rec_fset->args.size(); ++i) { + if (rec_fset->args[i].name) { + rec_fset->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fset->args[i].name); + } + if (rec_fset->args[i].descr) { + rec_fset->args[i].descr = PYBIND11_COMPAT_STRDUP(rec_fset->args[i].descr); + } + } if (!rec_active) { rec_active = rec_fset; } @@ -2418,8 +2731,7 @@ class class_ : public detail::generic_type { static void init_holder_from_existing(const detail::value_and_holder &v_h, const holder_type *holder_ptr, std::true_type /*is_copy_constructible*/) { - new (std::addressof(v_h.holder())) - holder_type(*reinterpret_cast(holder_ptr)); + new (std::addressof(v_h.holder())) holder_type(*holder_ptr); } static void init_holder_from_existing(const detail::value_and_holder &v_h, @@ -2631,8 +2943,9 @@ struct enum_base { PYBIND11_NOINLINE void init(bool is_arithmetic, bool is_convertible) { m_base.attr("__entries") = dict(); - auto property = handle((PyObject *) &PyProperty_Type); - auto static_property = handle((PyObject *) get_internals().static_property_type); + auto property = handle(reinterpret_cast(&PyProperty_Type)); + auto static_property + = handle(reinterpret_cast(get_internals().static_property_type)); m_base.attr("__repr__") = cpp_function( [](const object &arg) -> str { @@ -2663,7 +2976,7 @@ struct enum_base { [](handle arg) -> std::string { std::string docstring; dict entries = arg.attr("__entries"); - if (((PyTypeObject *) arg.ptr())->tp_doc) { + if ((reinterpret_cast(arg.ptr()))->tp_doc) { docstring += std::string( reinterpret_cast(arg.ptr())->tp_doc); docstring += "\n\n"; @@ -2789,7 +3102,7 @@ struct enum_base { dict entries = m_base.attr("__entries"); str name(name_); if (entries.contains(name)) { - std::string type_name = (std::string) str(m_base.attr("__name__")); + std::string type_name = std::string(str(m_base.attr("__name__"))); throw value_error(std::move(type_name) + ": element \"" + std::string(name_) + "\" already exists!"); } @@ -2984,7 +3297,7 @@ all_type_info_get_cache(PyTypeObject *type) { if (res.second) { // New cache entry created; set up a weak reference to automatically remove it if the type // gets destroyed: - weakref((PyObject *) type, cpp_function([type](handle wr) { + weakref(reinterpret_cast(type), cpp_function([type](handle wr) { with_internals([type](internals &internals) { internals.registered_types_py.erase(type); @@ -3079,6 +3392,11 @@ iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&...extra) { using state = detail::iterator_state; // TODO: state captures only the types of Extra, not the values + // For Python < 3.14.0rc1, pycritical_section uses direct mutex locking (same as a unique + // lock), which may deadlock during type registration. See detail/internals.h for details. +#if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 + PYBIND11_LOCK_INTERNALS(get_internals()); +#endif if (!detail::get_type_info(typeid(state), false)) { class_(handle(), "iterator", pybind11::module_local()) .def( @@ -3200,13 +3518,10 @@ typing::Iterator make_value_iterator(Type &value, Extra &&...extra) { template void implicitly_convertible() { - static int tss_sentinel_pointee = 1; // arbitrary value struct set_flag { - thread_specific_storage &flag; - explicit set_flag(thread_specific_storage &flag_) : flag(flag_) { - flag = &tss_sentinel_pointee; // trick: the pointer itself is the sentinel - } - ~set_flag() { flag.reset(nullptr); } + bool &flag; + explicit set_flag(bool &flag_) : flag(flag_) { flag_ = true; } + ~set_flag() { flag = false; } // Prevent copying/moving to ensure RAII guard is used safely set_flag(const set_flag &) = delete; @@ -3215,7 +3530,7 @@ void implicitly_convertible() { set_flag &operator=(set_flag &&) = delete; }; auto implicit_caster = [](PyObject *obj, PyTypeObject *type) -> PyObject * { - static thread_specific_storage currently_used; + thread_local bool currently_used = false; if (currently_used) { // implicit conversions are non-reentrant return nullptr; } @@ -3225,7 +3540,7 @@ void implicitly_convertible() { } tuple args(1); args[0] = obj; - PyObject *result = PyObject_Call((PyObject *) type, args.ptr(), nullptr); + PyObject *result = PyObject_Call(reinterpret_cast(type), args.ptr(), nullptr); if (result == nullptr) { PyErr_Clear(); } @@ -3441,7 +3756,7 @@ get_type_override(const void *this_ptr, const type_info *this_type, const char * if (frame != nullptr) { PyCodeObject *f_code = PyFrame_GetCode(frame); // f_code is guaranteed to not be NULL - if ((std::string) str(f_code->co_name) == name && f_code->co_argcount > 0) { + if (std::string(str(f_code->co_name)) == name && f_code->co_argcount > 0) { # if PY_VERSION_HEX >= 0x030d0000 PyObject *locals = PyEval_GetFrameLocals(); # else @@ -3536,13 +3851,15 @@ function get_override(const T *this_ptr, const char *name) { auto o = override(__VA_ARGS__); \ PYBIND11_WARNING_PUSH \ PYBIND11_WARNING_DISABLE_MSVC(4127) \ - if (pybind11::detail::cast_is_temporary_value_reference::value \ + if PYBIND11_MAYBE_CONSTEXPR ( \ + pybind11::detail::cast_is_temporary_value_reference::value \ && !pybind11::detail::is_same_ignoring_cvref::value) { \ static pybind11::detail::override_caster_t caster; \ return pybind11::detail::cast_ref(std::move(o), caster); \ + } else { \ + return pybind11::detail::cast_safe(std::move(o)); \ } \ PYBIND11_WARNING_POP \ - return pybind11::detail::cast_safe(std::move(o)); \ } \ } while (false) diff --git a/pybind11/include/pybind11/pytypes.h b/pybind11/include/pybind11/pytypes.h index 9c60c94..ff4be6a 100644 --- a/pybind11/include/pybind11/pytypes.h +++ b/pybind11/include/pybind11/pytypes.h @@ -259,8 +259,7 @@ class handle : public detail::object_api { detail::enable_if_t, detail::is_pyobj_ptr_or_nullptr_t>, std::is_convertible>::value, - int> - = 0> + int> = 0> // NOLINTNEXTLINE(google-explicit-constructor) handle(T &obj) : m_ptr(obj) {} @@ -547,8 +546,13 @@ struct error_fetch_and_normalize { // The presence of __notes__ is likely due to exception normalization // errors, although that is not necessarily true, therefore insert a // hint only: - if (PyObject_HasAttrString(m_value.ptr(), "__notes__")) { + const int has_notes = PyObject_HasAttrString(m_value.ptr(), "__notes__"); + if (has_notes == 1) { m_lazy_error_string += "[WITH __notes__]"; + } else if (has_notes == -1) { + // Ignore secondary errors when probing for __notes__ to avoid leaking a + // spurious exception while still reporting the original error. + PyErr_Clear(); } #else // PyErr_NormalizeException() may change the exception type if there are cascading @@ -861,7 +865,7 @@ bool isinstance(handle obj) { } template <> -inline bool isinstance(handle) = delete; +bool isinstance(handle) = delete; template <> inline bool isinstance(handle obj) { return obj.ptr() != nullptr; @@ -992,9 +996,11 @@ inline PyObject *dict_getitem(PyObject *v, PyObject *key) { return rv; } +// PyDict_GetItemStringRef was added in Python 3.13.0a1. +// See also: https://github.com/python/pythoncapi-compat/blob/main/pythoncapi_compat.h inline PyObject *dict_getitemstringref(PyObject *v, const char *key) { -#if PY_VERSION_HEX >= 0x030D0000 - PyObject *rv; +#if PY_VERSION_HEX >= 0x030D00A1 + PyObject *rv = nullptr; if (PyDict_GetItemStringRef(v, key, &rv) < 0) { throw error_already_set(); } @@ -1009,6 +1015,46 @@ inline PyObject *dict_getitemstringref(PyObject *v, const char *key) { #endif } +inline PyObject *dict_setdefaultstring(PyObject *v, const char *key, PyObject *defaultobj) { + PyObject *kv = PyUnicode_FromString(key); + if (kv == nullptr) { + throw error_already_set(); + } + + PyObject *rv = PyDict_SetDefault(v, kv, defaultobj); + Py_DECREF(kv); + if (rv == nullptr) { + throw error_already_set(); + } + return rv; +} + +// PyDict_SetDefaultRef was added in Python 3.13.0a4. +// See also: https://github.com/python/pythoncapi-compat/blob/main/pythoncapi_compat.h +inline PyObject *dict_setdefaultstringref(PyObject *v, const char *key, PyObject *defaultobj) { +#if PY_VERSION_HEX >= 0x030D00A4 + PyObject *kv = PyUnicode_FromString(key); + if (kv == nullptr) { + throw error_already_set(); + } + + PyObject *rv = nullptr; + if (PyDict_SetDefaultRef(v, kv, defaultobj, &rv) < 0) { + Py_DECREF(kv); + throw error_already_set(); + } + Py_DECREF(kv); + return rv; +#else + PyObject *rv = dict_setdefaultstring(v, key, defaultobj); + if (rv == nullptr || PyErr_Occurred()) { + throw error_already_set(); + } + Py_XINCREF(rv); + return rv; +#endif +} + // Helper aliases/functions to support implicit casting of values given to python // accessors/methods. When given a pyobject, this simply returns the pyobject as-is; for other C++ // type, the value goes through pybind11::cast(obj) to convert it to an `object`. @@ -1039,11 +1085,11 @@ class accessor : public object_api> { void operator=(const accessor &a) & { operator=(handle(a)); } template - void operator=(T &&value) && { + enable_if_t>::value> operator=(T &&value) && { Policy::set(obj, key, object_or_cast(std::forward(value))); } template - void operator=(T &&value) & { + enable_if_t>::value> operator=(T &&value) & { get_cache() = ensure_object(object_or_cast(std::forward(value))); } @@ -1555,7 +1601,7 @@ class iterator : public object { } private: - object value = {}; + object value; }; class type : public object { @@ -1563,7 +1609,9 @@ class type : public object { PYBIND11_OBJECT(type, object, PyType_Check) /// Return a type handle from a handle or an object - static handle handle_of(handle h) { return handle((PyObject *) Py_TYPE(h.ptr())); } + static handle handle_of(handle h) { + return handle(reinterpret_cast(Py_TYPE(h.ptr()))); + } /// Return a type object from a handle or an object static type of(handle h) { return type(type::handle_of(h), borrowed_t{}); } @@ -1641,7 +1689,12 @@ class str : public object { Return a string representation of the object. This is analogous to the ``str()`` function in Python. \endrst */ - explicit str(handle h) : object(raw_str(h.ptr()), stolen_t{}) { + // Templatized to avoid ambiguity with str(const object&) for object-derived types. + template >::value + && std::is_constructible::value, + int> = 0> + explicit str(T &&h) : object(raw_str(handle(std::forward(h)).ptr()), stolen_t{}) { if (!m_ptr) { throw error_already_set(); } @@ -1661,7 +1714,7 @@ class str : public object { if (PyBytes_AsStringAndSize(temp.ptr(), &buffer, &length) != 0) { throw error_already_set(); } - return std::string(buffer, (size_t) length); + return std::string(buffer, static_cast(length)); } template @@ -1861,10 +1914,12 @@ template Unsigned as_unsigned(PyObject *o) { if (sizeof(Unsigned) <= sizeof(unsigned long)) { unsigned long v = PyLong_AsUnsignedLong(o); - return v == (unsigned long) -1 && PyErr_Occurred() ? (Unsigned) -1 : (Unsigned) v; + return v == static_cast(-1) && PyErr_Occurred() ? (Unsigned) -1 + : (Unsigned) v; } unsigned long long v = PyLong_AsUnsignedLongLong(o); - return v == (unsigned long long) -1 && PyErr_Occurred() ? (Unsigned) -1 : (Unsigned) v; + return v == static_cast(-1) && PyErr_Occurred() ? (Unsigned) -1 + : (Unsigned) v; } PYBIND11_NAMESPACE_END(detail) @@ -1908,21 +1963,21 @@ class float_ : public object { PYBIND11_OBJECT_CVT(float_, object, PyFloat_Check, PyNumber_Float) // Allow implicit conversion from float/double: // NOLINTNEXTLINE(google-explicit-constructor) - float_(float value) : object(PyFloat_FromDouble((double) value), stolen_t{}) { + float_(float value) : object(PyFloat_FromDouble(static_cast(value)), stolen_t{}) { if (!m_ptr) { pybind11_fail("Could not allocate float object!"); } } // NOLINTNEXTLINE(google-explicit-constructor) - float_(double value = .0) : object(PyFloat_FromDouble((double) value), stolen_t{}) { + float_(double value = .0) : object(PyFloat_FromDouble(value), stolen_t{}) { if (!m_ptr) { pybind11_fail("Could not allocate float object!"); } } // NOLINTNEXTLINE(google-explicit-constructor) - operator float() const { return (float) PyFloat_AsDouble(m_ptr); } + operator float() const { return static_cast(PyFloat_AsDouble(m_ptr)); } // NOLINTNEXTLINE(google-explicit-constructor) - operator double() const { return (double) PyFloat_AsDouble(m_ptr); } + operator double() const { return PyFloat_AsDouble(m_ptr); } }; class weakref : public object { @@ -2122,7 +2177,7 @@ class tuple : public object { pybind11_fail("Could not allocate tuple object!"); } } - size_t size() const { return (size_t) PyTuple_Size(m_ptr); } + size_t size() const { return static_cast(PyTuple_Size(m_ptr)); } bool empty() const { return size() == 0; } detail::tuple_accessor operator[](size_t index) const { return {*this, index}; } template ::value, int> = 0> @@ -2156,7 +2211,7 @@ class dict : public object { typename collector = detail::deferred_t, Args...>> explicit dict(Args &&...args) : dict(collector(std::forward(args)...).kwargs()) {} - size_t size() const { return (size_t) PyDict_Size(m_ptr); } + size_t size() const { return static_cast(PyDict_Size(m_ptr)); } bool empty() const { return size() == 0; } detail::dict_iterator begin() const { return {*this, 0}; } detail::dict_iterator end() const { return {}; } @@ -2176,7 +2231,8 @@ class dict : public object { if (PyDict_Check(op)) { return handle(op).inc_ref().ptr(); } - return PyObject_CallFunctionObjArgs((PyObject *) &PyDict_Type, op, nullptr); + return PyObject_CallFunctionObjArgs( + reinterpret_cast(&PyDict_Type), op, nullptr); } }; @@ -2188,7 +2244,7 @@ class sequence : public object { if (result == -1) { throw error_already_set(); } - return (size_t) result; + return static_cast(result); } bool empty() const { return size() == 0; } detail::sequence_accessor operator[](size_t index) const { return {*this, index}; } @@ -2211,7 +2267,7 @@ class list : public object { pybind11_fail("Could not allocate list object!"); } } - size_t size() const { return (size_t) PyList_Size(m_ptr); } + size_t size() const { return static_cast(PyList_Size(m_ptr)); } bool empty() const { return size() == 0; } detail::list_accessor operator[](size_t index) const { return {*this, index}; } template ::value, int> = 0> @@ -2497,7 +2553,7 @@ inline size_t len(handle h) { if (result < 0) { throw error_already_set(); } - return (size_t) result; + return static_cast(result); } /// Get the length hint of a Python object. @@ -2510,7 +2566,7 @@ inline size_t len_hint(handle h) { PyErr_Clear(); return 0; } - return (size_t) result; + return static_cast(result); } inline str repr(handle h) { diff --git a/pybind11/include/pybind11/stl/filesystem.h b/pybind11/include/pybind11/stl/filesystem.h index 1c0b18c..52d2962 100644 --- a/pybind11/include/pybind11/stl/filesystem.h +++ b/pybind11/include/pybind11/stl/filesystem.h @@ -17,7 +17,7 @@ #elif defined(PYBIND11_HAS_EXPERIMENTAL_FILESYSTEM) # include #else -# error "Neither #include nor #include nor #include is available." #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) diff --git a/pybind11/include/pybind11/stl_bind.h b/pybind11/include/pybind11/stl_bind.h index 3eb1e53..8202300 100644 --- a/pybind11/include/pybind11/stl_bind.h +++ b/pybind11/include/pybind11/stl_bind.h @@ -244,7 +244,7 @@ void vector_modifiers( } auto *seq = new Vector(); - seq->reserve((size_t) slicelength); + seq->reserve(slicelength); for (size_t i = 0; i < slicelength; ++i) { seq->push_back(v[start]); diff --git a/pybind11/include/pybind11/subinterpreter.h b/pybind11/include/pybind11/subinterpreter.h index 4b208ed..5475452 100644 --- a/pybind11/include/pybind11/subinterpreter.h +++ b/pybind11/include/pybind11/subinterpreter.h @@ -20,15 +20,6 @@ #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) -PYBIND11_NAMESPACE_BEGIN(detail) -inline PyInterpreterState *get_interpreter_state_unchecked() { - auto cur_tstate = get_thread_state_unchecked(); - if (cur_tstate) - return cur_tstate->interp; - else - return nullptr; -} -PYBIND11_NAMESPACE_END(detail) class subinterpreter; @@ -76,7 +67,7 @@ class subinterpreter { /// Create a new subinterpreter with the specified configuration /// @note This function acquires (and then releases) the main interpreter GIL, but the main /// interpreter and its GIL are not required to be held prior to calling this function. - static inline subinterpreter create(PyInterpreterConfig const &cfg) { + static subinterpreter create(PyInterpreterConfig const &cfg) { error_scope err_scope; subinterpreter result; @@ -84,7 +75,7 @@ class subinterpreter { // we must hold the main GIL in order to create a subinterpreter subinterpreter_scoped_activate main_guard(main()); - auto prev_tstate = PyThreadState_Get(); + auto *prev_tstate = PyThreadState_Get(); PyStatus status; @@ -103,13 +94,13 @@ class subinterpreter { } // this doesn't raise a normal Python exception, it provides an exit() status code. - if (PyStatus_Exception(status)) { + if (PyStatus_Exception(status) != 0) { pybind11_fail("failed to create new sub-interpreter"); } // upon success, the new interpreter is activated in this thread result.istate_ = result.creation_tstate_->interp; - detail::get_num_interpreters_seen() += 1; // there are now many interpreters + detail::has_seen_non_main_interpreter() = true; detail::get_internals(); // initialize internals.tstate, amongst other things... // In 3.13+ this state should be deleted right away, and the memory will be reused for @@ -128,7 +119,7 @@ class subinterpreter { /// Calls create() with a default configuration of an isolated interpreter that disallows fork, /// exec, and Python threads. - static inline subinterpreter create() { + static subinterpreter create() { // same as the default config in the python docs PyInterpreterConfig cfg; std::memset(&cfg, 0, sizeof(cfg)); @@ -144,8 +135,8 @@ class subinterpreter { return; } - PyThreadState *destroy_tstate; - PyThreadState *old_tstate; + PyThreadState *destroy_tstate = nullptr; + PyThreadState *old_tstate = nullptr; // Python 3.12 requires us to keep the original PyThreadState alive until we are ready to // destroy the interpreter. We prefer to use that to destroy the interpreter. @@ -173,7 +164,7 @@ class subinterpreter { old_tstate = PyThreadState_Swap(destroy_tstate); #endif - bool switch_back = old_tstate && old_tstate->interp != istate_; + bool switch_back = (old_tstate != nullptr) && old_tstate->interp != istate_; // Internals always exists in the subinterpreter, this class enforces it when it creates // the subinterpreter. Even if it didn't, this only creates the pointer-to-pointer, not the @@ -190,8 +181,9 @@ class subinterpreter { detail::get_local_internals_pp_manager().destroy(); // switch back to the old tstate and old GIL (if there was one) - if (switch_back) + if (switch_back) { PyThreadState_Swap(old_tstate); + } } /// Get a handle to the main interpreter that can be used with subinterpreter_scoped_activate @@ -214,11 +206,11 @@ class subinterpreter { /// Get the numerical identifier for the sub-interpreter int64_t id() const { - if (istate_ != nullptr) + if (istate_ != nullptr) { return PyInterpreterState_GetID(istate_); - else - return -1; // CPython uses one-up numbers from 0, so negative should be safe to return - // here. + } + return -1; // CPython uses one-up numbers from 0, so negative should be safe to return + // here. } /// Get the interpreter's state dict. This interpreter's GIL must be held before calling! @@ -271,7 +263,7 @@ inline subinterpreter_scoped_activate::subinterpreter_scoped_activate(subinterpr // make the interpreter active and acquire the GIL old_tstate_ = PyThreadState_Swap(tstate_); - // save this in internals for scoped_gil calls + // save this in internals for scoped_gil calls (see also: PR #5870) detail::get_internals().tstate = tstate_; } diff --git a/pybind11/include/pybind11/typing.h b/pybind11/include/pybind11/typing.h index 1715026..43e2187 100644 --- a/pybind11/include/pybind11/typing.h +++ b/pybind11/include/pybind11/typing.h @@ -34,10 +34,7 @@ PYBIND11_NAMESPACE_BEGIN(typing) There is no additional enforcement of types at runtime. */ -template -class Tuple : public tuple { - using tuple::tuple; -}; +// Tuple type hint defined in cast.h for use in py::make_tuple to avoid circular includes template class Dict : public dict { diff --git a/pybind11/pyproject.toml b/pybind11/pyproject.toml index a4b43d3..03214c6 100644 --- a/pybind11/pyproject.toml +++ b/pybind11/pyproject.toml @@ -38,7 +38,7 @@ requires-python = ">=3.8" [project.urls] Homepage = "https://github.com/pybind/pybind11" Documentation = "https://pybind11.readthedocs.io/" -"Bug Tracker" = "https://github.com/pybind/pybind11/issues" +"Issue Tracker" = "https://github.com/pybind/pybind11/issues" Discussions = "https://github.com/pybind/pybind11/discussions" Changelog = "https://pybind11.readthedocs.io/en/latest/changelog.html" Chat = "https://gitter.im/pybind/Lobby" @@ -57,9 +57,11 @@ pybind11 = "pybind11.share.pkgconfig" test = [ "pytest", "build", +] +dev = [ "tomlkit", + { include-group = "test" } ] -dev = [{ include-group = "test" }] [tool.scikit-build] @@ -152,6 +154,8 @@ extend-select = [ "C4", # flake8-comprehensions "EM", # flake8-errmsg "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "PERF", # Perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint @@ -182,3 +186,30 @@ isort.required-imports = ["from __future__ import annotations"] [tool.repo-review] ignore = ["PP"] + +[tool.typos] +files.extend-exclude = ["/cpython"] + +[tool.typos.default.extend-identifiers] +ser_no = "ser_no" +SerNo = "SerNo" +StrLits = "StrLits" + +[tool.typos.default.extend-words] +nd = "nd" +valu = "valu" +fo = "fo" +quater = "quater" +optin = "optin" +othr = "othr" +# NumPy uses "writeable" in public API names and flags. +writeable = "writeable" +Writeable = "Writeable" +WRITEABLE = "WRITEABLE" + +#[tool.typos.type.cpp.extend-words] +setp = "setp" +ot = "ot" + +[tool.typos.type.json.extend-words] +ba = "ba" diff --git a/pybind11/tests/CMakeLists.txt b/pybind11/tests/CMakeLists.txt index 34b22a5..fc08a90 100644 --- a/pybind11/tests/CMakeLists.txt +++ b/pybind11/tests/CMakeLists.txt @@ -5,7 +5,7 @@ # All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) # Filter out items; print an optional message if any items filtered. This ignores extensions. # @@ -117,6 +117,7 @@ set(PYBIND11_TEST_FILES test_callbacks test_chrono test_class + test_class_cross_module_use_after_one_module_dealloc test_class_release_gil_before_calling_cpp_dtor test_class_sh_basic test_class_sh_disowning @@ -166,11 +167,13 @@ set(PYBIND11_TEST_FILES test_operator_overloading test_pickling test_potentially_slicing_weak_ptr + test_pytorch_shared_ptr_cast_regression test_python_multiple_inheritance test_pytypes test_scoped_critical_section test_sequences_and_iterators test_smart_ptr + test_standalone_enum_module.py test_stl test_stl_binders test_tagbased_polymorphic @@ -239,14 +242,16 @@ list(SORT PYBIND11_PYTEST_FILES) # Contains the set of test files that require pybind11_cross_module_tests to be # built; if none of these are built (i.e. because TEST_OVERRIDE is used and # doesn't include them) the second module doesn't get built. -tests_extra_targets("test_exceptions.py;test_local_bindings.py;test_stl.py;test_stl_binders.py" - "pybind11_cross_module_tests") +tests_extra_targets( + "test_class_cross_module_use_after_one_module_dealloc.py;test_exceptions.py;test_local_bindings.py;test_stl.py;test_stl_binders.py" + "pybind11_cross_module_tests") # And add additional targets for other tests. tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already_set") tests_extra_targets("test_gil_scoped.py" "cross_module_gil_utils") tests_extra_targets("test_cpp_conduit.py" "exo_planet_pybind11;exo_planet_c_api;home_planet_very_lonely_traveler") +tests_extra_targets("test_standalone_enum_module.py" "standalone_enum_module") set(PYBIND11_EIGEN_REPO "https://gitlab.com/libeigen/eigen.git" @@ -295,10 +300,17 @@ if(PYBIND11_TEST_FILES_EIGEN_I GREATER -1) else() find_package(Eigen3 3.2.7 QUIET CONFIG) + if(NOT Eigen3_FOUND) + find_package(Eigen3 5 QUIET CONFIG) + endif() + set(EIGEN3_FOUND ${Eigen3_FOUND}) + set(EIGEN3_VERSION ${Eigen3_VERSION}) if(NOT EIGEN3_FOUND) # Couldn't load via target, so fall back to allowing module mode finding, which will pick up # tools/FindEigen3.cmake + # This MODULE-mode fallback is for older Eigen 3 setups; Eigen 5 is expected to be found + # via the CONFIG-mode probes above. find_package(Eigen3 3.2.7 QUIET) endif() endif() @@ -543,7 +555,9 @@ source_group( FILES ${PYBIND11_HEADERS}) # Make sure pytest is found or produce a warning -pybind11_find_import(pytest VERSION 3.1) +if(NOT CMAKE_CROSSCOMPILING) + pybind11_find_import(pytest VERSION 3.1) +endif() if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) # This is not used later in the build, so it's okay to regenerate each time. @@ -576,24 +590,46 @@ add_custom_target( USES_TERMINAL) if(NOT PYBIND11_CUDA_TESTS) - # This module doesn't get mixed with other test modules because those aren't subinterpreter safe. - pybind11_add_module(mod_per_interpreter_gil THIN_LTO mod_per_interpreter_gil.cpp) - pybind11_add_module(mod_shared_interpreter_gil THIN_LTO mod_shared_interpreter_gil.cpp) - set_target_properties(mod_per_interpreter_gil PROPERTIES LIBRARY_OUTPUT_DIRECTORY - "$<1:${CMAKE_CURRENT_BINARY_DIR}>") - set_target_properties(mod_shared_interpreter_gil PROPERTIES LIBRARY_OUTPUT_DIRECTORY - "$<1:${CMAKE_CURRENT_BINARY_DIR}>") + set(PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES + mod_per_interpreter_gil mod_shared_interpreter_gil mod_per_interpreter_gil_with_singleton) + + # These modules don't get mixed with other test modules because those aren't subinterpreter safe. + foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES) + pybind11_add_module("${mod}" THIN_LTO "${mod}.cpp") + pybind11_enable_warnings("${mod}") + endforeach() + + # Put the built modules next to `pybind11_tests.so` so that the test scripts can find them. + get_target_property(pybind11_tests_output_directory pybind11_tests LIBRARY_OUTPUT_DIRECTORY) + foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES) + set_target_properties("${mod}" PROPERTIES LIBRARY_OUTPUT_DIRECTORY + "${pybind11_tests_output_directory}") + # Also set config-specific output directories for multi-configuration generators (MSVC) + if(DEFINED CMAKE_CONFIGURATION_TYPES) + foreach(config ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${config} config) + set_target_properties("${mod}" PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${config} + "${pybind11_tests_output_directory}") + endforeach() + endif() + endforeach() + + if(SKBUILD) + foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES) + install(TARGETS "${mod}" LIBRARY DESTINATION .) + endforeach() + endif() + if(PYBIND11_TEST_SMART_HOLDER) - target_compile_definitions( - mod_per_interpreter_gil - PUBLIC -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE - ) - target_compile_definitions( - mod_shared_interpreter_gil - PUBLIC -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE - ) + foreach(mod IN LISTS PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES) + target_compile_definitions( + "${mod}" + PUBLIC + -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE) + endforeach() endif() - add_dependencies(pytest mod_per_interpreter_gil mod_shared_interpreter_gil) + + add_dependencies(pytest ${PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES}) endif() if(PYBIND11_TEST_OVERRIDE) @@ -647,8 +683,8 @@ if(NOT PYBIND11_CUDA_TESTS) # Test pure C++ code (not depending on Python). Provides the `test_pure_cpp` target. add_subdirectory(pure_cpp) - # Test embedding the interpreter. Provides the `cpptest` target. - add_subdirectory(test_embed) + # Test C++ code that depends on Python, such as embedding the interpreter. Provides the `cpptest` target. + add_subdirectory(test_with_catch) # Test CMake build using functions and targets from subdirectory or installed location add_subdirectory(test_cmake_build) diff --git a/pybind11/tests/conftest.py b/pybind11/tests/conftest.py index 8528cc9..ca86d8a 100644 --- a/pybind11/tests/conftest.py +++ b/pybind11/tests/conftest.py @@ -16,6 +16,7 @@ import sysconfig import textwrap import traceback +import weakref from typing import Callable import pytest @@ -208,19 +209,54 @@ def pytest_assertrepr_compare(op, left, right): # noqa: ARG001 return None +# Number of times we think repeatedly collecting garbage might do anything. +# The only reason to do more than once is because finalizers executed during +# one GC pass could create garbage that can't be collected until a future one. +# This quickly produces diminishing returns, and GC passes can be slow, so this +# value is a tradeoff between non-flakiness and fast tests. (It errs on the +# side of non-flakiness; many uses of this idiom only do 3 passes.) +num_gc_collect = 5 + + def gc_collect(): - """Run the garbage collector three times (needed when running + """Run the garbage collector several times (needed when running reference counting tests with PyPy)""" - gc.collect() - gc.collect() - gc.collect() - gc.collect() - gc.collect() + for _ in range(num_gc_collect): + gc.collect() + + +def delattr_and_ensure_destroyed(*specs): + """For each of the given *specs* (a tuple of the form ``(scope, name)``), + perform ``delattr(scope, name)``, then do enough GC collections that the + deleted reference has actually caused the target to be destroyed. This is + typically used to test what happens when a type object is destroyed; if you + use it for that, you should be aware that extension types, or all types, + are immortal on some Python versions. See ``env.TYPES_ARE_IMMORTAL``. + """ + wrs = [] + for mod, name in specs: + wrs.append(weakref.ref(getattr(mod, name))) + delattr(mod, name) + + for _ in range(num_gc_collect): + gc.collect() + if all(wr() is None for wr in wrs): + break + else: + # If this fires, most likely something is still holding a reference + # to the object you tried to destroy - for example, it's a type that + # still has some instances alive. Try setting a breakpoint here and + # examining `gc.get_referrers(wrs[0]())`. It's vaguely possible that + # num_gc_collect needs to be increased also. + pytest.fail( + f"Could not delete bindings such as {next(wr for wr in wrs if wr() is not None)!r}" + ) def pytest_configure(): pytest.suppress = contextlib.suppress pytest.gc_collect = gc_collect + pytest.delattr_and_ensure_destroyed = delattr_and_ensure_destroyed def pytest_report_header(): @@ -268,10 +304,10 @@ def backport_typehints() -> Callable[[SanitizedString], SanitizedString]: if sys.version_info < (3, 10): d["typing_extensions.TypeGuard"] = "typing.TypeGuard" - def backport(sanatized_string: SanitizedString) -> SanitizedString: + def backport(sanitized_string: SanitizedString) -> SanitizedString: for old, new in d.items(): - sanatized_string.string = sanatized_string.string.replace(old, new) + sanitized_string.string = sanitized_string.string.replace(old, new) - return sanatized_string + return sanitized_string return backport diff --git a/pybind11/tests/env.py b/pybind11/tests/env.py index 95cc1ac..12acde7 100644 --- a/pybind11/tests/env.py +++ b/pybind11/tests/env.py @@ -7,9 +7,23 @@ import pytest ANDROID = sys.platform.startswith("android") +IOS = sys.platform.startswith("ios") LINUX = sys.platform.startswith("linux") MACOS = sys.platform.startswith("darwin") WIN = sys.platform.startswith("win32") or sys.platform.startswith("cygwin") +FREEBSD = sys.platform.startswith("freebsd") + +MUSLLINUX = False +MANYLINUX = False +if LINUX: + + def _is_musl() -> bool: + libc, _ = platform.libc_ver() + return libc == "musl" or (libc != "glibc" and libc != "") + + MUSLLINUX = _is_musl() + MANYLINUX = not MUSLLINUX + del _is_musl CPYTHON = platform.python_implementation() == "CPython" PYPY = platform.python_implementation() == "PyPy" @@ -18,20 +32,45 @@ sys.modules["__graalpython__"].get_graalvm_version() if GRAALPY else "0.0.0" ) GRAALPY_VERSION = tuple(int(t) for t in _graalpy_version.split("-")[0].split(".")[:3]) + +# Compile-time config (what the binary was built for) PY_GIL_DISABLED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) +# Runtime state (what's actually happening now) +sys_is_gil_enabled = getattr(sys, "_is_gil_enabled", lambda: True) + +TYPES_ARE_IMMORTAL = ( + PYPY + or GRAALPY + or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) +) + + +def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None: + """Runs the given code in a subprocess.""" + import os + import subprocess + import sys + import textwrap + if ANDROID or IOS or sys.platform.startswith("emscripten"): + pytest.skip("Requires subprocess support") -def deprecated_call(): - """ - pytest.deprecated_call() seems broken in pytest<3.9.x; concretely, it - doesn't work on CPython 3.8.0 with pytest==3.3.2 on Ubuntu 18.04 (#2922). - - This is a narrowed reimplementation of the following PR :( - https://github.com/pytest-dev/pytest/pull/4104 - """ - # TODO: Remove this when testing requires pytest>=3.9. - pieces = pytest.__version__.split(".") - pytest_major_minor = (int(pieces[0]), int(pieces[1])) - if pytest_major_minor < (3, 9): - return pytest.warns((DeprecationWarning, PendingDeprecationWarning)) - return pytest.deprecated_call() + code = textwrap.dedent(code).strip() + try: + for _ in range(rerun): # run flakily failing test multiple times + subprocess.check_output( + [sys.executable, "-c", code], + cwd=os.getcwd(), + stderr=subprocess.STDOUT, + text=True, + ) + except subprocess.CalledProcessError as ex: + raise RuntimeError( + f"Subprocess failed with exit code {ex.returncode}.\n\n" + f"Code:\n" + f"```python\n" + f"{code}\n" + f"```\n\n" + f"Output:\n" + f"{ex.output}" + ) from None diff --git a/pybind11/tests/extra_python_package/test_files.py b/pybind11/tests/extra_python_package/test_files.py index 81df847..cbbc59c 100644 --- a/pybind11/tests/extra_python_package/test_files.py +++ b/pybind11/tests/extra_python_package/test_files.py @@ -76,12 +76,14 @@ } detail_headers = { + "include/pybind11/detail/argument_vector.h", "include/pybind11/detail/class.h", "include/pybind11/detail/common.h", "include/pybind11/detail/cpp_conduit.h", "include/pybind11/detail/descr.h", "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h", "include/pybind11/detail/function_record_pyobject.h", + "include/pybind11/detail/holder_caster_foreign_helpers.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", "include/pybind11/detail/native_enum_data.h", diff --git a/pybind11/tests/local_bindings.h b/pybind11/tests/local_bindings.h index dea1813..23d46eb 100644 --- a/pybind11/tests/local_bindings.h +++ b/pybind11/tests/local_bindings.h @@ -25,8 +25,9 @@ using MixedLocalGlobal = LocalBase<4>; using MixedGlobalLocal = LocalBase<5>; /// Registered with py::module_local only in the secondary module: -using ExternalType1 = LocalBase<6>; -using ExternalType2 = LocalBase<7>; +using ExternalType1 = LocalBase<6>; // default holder +using ExternalType2 = LocalBase<7>; // held by std::shared_ptr +using ExternalType3 = LocalBase<8>; // held by smart_holder using LocalVec = std::vector; using LocalVec2 = std::vector; @@ -65,11 +66,11 @@ PYBIND11_MAKE_OPAQUE(NonLocalMap) PYBIND11_MAKE_OPAQUE(NonLocalMap2) // Simple bindings (used with the above): -template -py::class_ bind_local(Args &&...args) { - return py::class_(std::forward(args)...).def(py::init()).def("get", [](T &i) { - return i.i + Adjust; - }); +template , typename... Args> +py::class_ bind_local(Args &&...args) { + return py::class_(std::forward(args)...) + .def(py::init()) + .def("get", [](T &i) { return i.i + Adjust; }); } // Simulate a foreign library base class (to match the example in the docs): diff --git a/pybind11/tests/mod_per_interpreter_gil_with_singleton.cpp b/pybind11/tests/mod_per_interpreter_gil_with_singleton.cpp new file mode 100644 index 0000000..90e2ec8 --- /dev/null +++ b/pybind11/tests/mod_per_interpreter_gil_with_singleton.cpp @@ -0,0 +1,147 @@ +#include +#include + +#include + +namespace py = pybind11; + +#ifdef PYBIND11_HAS_NATIVE_ENUM +# include +#endif + +namespace pybind11_tests { +namespace mod_per_interpreter_gil_with_singleton { +// A singleton class that holds references to certain Python objects +// This singleton is per-interpreter using gil_safe_call_once_and_store +class MySingleton { +public: + MySingleton() = default; + ~MySingleton() = default; + MySingleton(const MySingleton &) = delete; + MySingleton &operator=(const MySingleton &) = delete; + MySingleton(MySingleton &&) = default; + MySingleton &operator=(MySingleton &&) = default; + + static MySingleton &get_instance() { + PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store storage; + return storage + .call_once_and_store_result([]() -> MySingleton { + MySingleton instance{}; + + auto emplace = [&instance](const py::handle &obj) -> void { + obj.inc_ref(); // Ensure the object is not GC'd while interpreter is alive + instance.objects.emplace_back(obj); + }; + + // Example objects to store in the singleton + emplace(py::type::handle_of(py::none())); // static type + emplace(py::type::handle_of(py::tuple())); // static type + emplace(py::type::handle_of(py::list())); // static type + emplace(py::type::handle_of(py::dict())); // static type + emplace(py::module_::import("collections").attr("OrderedDict")); // static type + emplace(py::module_::import("collections").attr("defaultdict")); // heap type + emplace(py::module_::import("collections").attr("deque")); // heap type + + assert(instance.objects.size() == 7); + return instance; + }) + .get_stored(); + } + + std::vector &get_objects() { return objects; } + + static void init() { + // Ensure the singleton is created + auto &instance = get_instance(); + (void) instance; // suppress unused variable warning + assert(instance.objects.size() == 7); + // Register cleanup at interpreter exit + py::module_::import("atexit").attr("register")(py::cpp_function(&MySingleton::clear)); + } + + static void clear() { + auto &instance = get_instance(); + (void) instance; // suppress unused variable warning + assert(instance.objects.size() == 7); + for (const auto &obj : instance.objects) { + obj.dec_ref(); + } + instance.objects.clear(); + } + +private: + std::vector objects; +}; + +class MyClass { +public: + explicit MyClass(py::ssize_t v) : value(v) {} + py::ssize_t get_value() const { return value; } + +private: + py::ssize_t value; +}; + +class MyGlobalError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class MyLocalError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +enum class MyEnum : int { + ONE = 1, + TWO = 2, + THREE = 3, +}; +} // namespace mod_per_interpreter_gil_with_singleton +} // namespace pybind11_tests + +PYBIND11_MODULE(mod_per_interpreter_gil_with_singleton, + m, + py::mod_gil_not_used(), + py::multiple_interpreters::per_interpreter_gil()) { + using namespace pybind11_tests::mod_per_interpreter_gil_with_singleton; + +#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT + m.attr("defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT") = true; +#else + m.attr("defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT") = false; +#endif + + MySingleton::init(); + + // Ensure py::multiple_interpreters::per_interpreter_gil() works with singletons using + // py::gil_safe_call_once_and_store + m.def( + "get_objects_in_singleton", + []() -> std::vector { return MySingleton::get_instance().get_objects(); }, + "Get the list of objects stored in the singleton"); + + // Ensure py::multiple_interpreters::per_interpreter_gil() works with class bindings + py::class_(m, "MyClass") + .def(py::init()) + .def("get_value", &MyClass::get_value); + + // Ensure py::multiple_interpreters::per_interpreter_gil() works with global exceptions + py::register_exception(m, "MyGlobalError"); + // Ensure py::multiple_interpreters::per_interpreter_gil() works with local exceptions + py::register_local_exception(m, "MyLocalError"); + +#ifdef PYBIND11_HAS_NATIVE_ENUM + // Ensure py::multiple_interpreters::per_interpreter_gil() works with native_enum + py::native_enum(m, "MyEnum", "enum.IntEnum") + .value("ONE", MyEnum::ONE) + .value("TWO", MyEnum::TWO) + .value("THREE", MyEnum::THREE) + .finalize(); +#else + py::enum_(m, "MyEnum") + .value("ONE", MyEnum::ONE) + .value("TWO", MyEnum::TWO) + .value("THREE", MyEnum::THREE); +#endif +} diff --git a/pybind11/tests/pure_cpp/CMakeLists.txt b/pybind11/tests/pure_cpp/CMakeLists.txt index 1150cb4..d2757db 100644 --- a/pybind11/tests/pure_cpp/CMakeLists.txt +++ b/pybind11/tests/pure_cpp/CMakeLists.txt @@ -15,6 +15,8 @@ target_link_libraries(smart_holder_poc_test PRIVATE pybind11::headers Catch2::Ca add_custom_target( test_pure_cpp COMMAND "$" - WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + USES_TERMINAL # Ensures output is shown immediately (not buffered and possibly lost on crash) +) add_dependencies(check test_pure_cpp) diff --git a/pybind11/tests/pybind11_cross_module_tests.cpp b/pybind11/tests/pybind11_cross_module_tests.cpp index 76f40bf..9a00c00 100644 --- a/pybind11/tests/pybind11_cross_module_tests.cpp +++ b/pybind11/tests/pybind11_cross_module_tests.cpp @@ -16,6 +16,15 @@ #include #include +class CrossDSOClass { +public: + CrossDSOClass() = default; + virtual ~CrossDSOClass(); + CrossDSOClass(const CrossDSOClass &) = default; +}; + +CrossDSOClass::~CrossDSOClass() = default; + PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) { m.doc() = "pybind11 cross-module test module"; @@ -26,7 +35,9 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) { // test_load_external bind_local(m, "ExternalType1", py::module_local()); - bind_local(m, "ExternalType2", py::module_local()); + bind_local>( + m, "ExternalType2", py::module_local()); + bind_local(m, "ExternalType3", py::module_local()); // test_exceptions.py py::register_local_exception(m, "LocalSimpleException"); @@ -146,4 +157,7 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) { // which appears when this header is missing. m.def("missing_header_arg", [](const std::vector &) {}); m.def("missing_header_return", []() { return std::vector(); }); + + // test_class_cross_module_use_after_one_module_dealloc + m.def("consume_cross_dso_class", [](const CrossDSOClass &) {}); } diff --git a/pybind11/tests/pybind11_tests.cpp b/pybind11/tests/pybind11_tests.cpp index 4317925..5dacd7a 100644 --- a/pybind11/tests/pybind11_tests.cpp +++ b/pybind11/tests/pybind11_tests.cpp @@ -96,6 +96,12 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) { #else false; #endif + m.attr("PYBIND11_TEST_SMART_HOLDER") = +#if defined(PYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE) + true; +#else + false; +#endif bind_ConstructorStats(m); @@ -105,6 +111,12 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) { m.attr("detailed_error_messages_enabled") = false; #endif +#if defined(__cpp_noexcept_function_type) + m.attr("defined___cpp_noexcept_function_type") = true; +#else + m.attr("defined___cpp_noexcept_function_type") = false; +#endif + py::class_(m, "UserType", "A `py::class_` type for testing") .def(py::init<>()) .def(py::init()) diff --git a/pybind11/tests/pyproject.toml b/pybind11/tests/pyproject.toml index fa47812..8821ea3 100644 --- a/pybind11/tests/pyproject.toml +++ b/pybind11/tests/pyproject.toml @@ -27,7 +27,9 @@ PYBIND11_FINDPYTHON = true [tool.cibuildwheel] test-sources = ["tests", "pyproject.toml"] -test-command = "python -m pytest -o timeout=0 -p no:cacheprovider tests" +test-command = "python -m pytest -v -o timeout=120 -p no:cacheprovider tests" +# Pyodide doesn't have signal.setitimer, so pytest-timeout can't work with timeout > 0 +pyodide.test-command = "python -m pytest -v -o timeout=0 -p no:cacheprovider tests" environment.PIP_ONLY_BINARY = "numpy" environment.PIP_PREFER_BINARY = "1" diff --git a/pybind11/tests/pytest.ini b/pybind11/tests/pytest.ini index 6ca7a91..6147c6b 100644 --- a/pybind11/tests/pytest.ini +++ b/pybind11/tests/pytest.ini @@ -9,7 +9,7 @@ addopts = --capture=sys # Show local info when a failure occurs --showlocals -log_cli_level = info +log_level = INFO filterwarnings = # make warnings into errors but ignore certain third-party extension issues error diff --git a/pybind11/tests/requirements.txt b/pybind11/tests/requirements.txt index 6e3a260..39bf867 100644 --- a/pybind11/tests/requirements.txt +++ b/pybind11/tests/requirements.txt @@ -7,8 +7,10 @@ numpy~=2.2.0; python_version=="3.10" and platform_python_implementation=="PyPy" numpy~=1.26.0; platform_python_implementation=="GraalVM" and sys_platform=="linux" numpy~=1.21.5; platform_python_implementation=="CPython" and python_version>="3.8" and python_version<"3.10" numpy~=1.22.2; platform_python_implementation=="CPython" and python_version=="3.10" -numpy~=1.26.0; platform_python_implementation=="CPython" and python_version>="3.11" and python_version<"3.13" -numpy~=2.2.0; platform_python_implementation=="CPython" and python_version=="3.13" +numpy~=1.26.0; platform_python_implementation=="CPython" and python_version>="3.11" and python_version<"3.13" and platform_machine!="ARM64" +numpy>=2.3.0; platform_python_implementation=="CPython" and python_version>="3.11" and platform_machine=="ARM64" +numpy~=2.2.0; platform_python_implementation=="CPython" and python_version=="3.13" and platform_machine!="ARM64" +numpy>=2.4.0; platform_python_implementation=="CPython" and python_version>="3.14" pytest>=6 pytest-timeout scipy~=1.5.4; platform_python_implementation=="CPython" and python_version<"3.10" diff --git a/pybind11/tests/standalone_enum_module.cpp b/pybind11/tests/standalone_enum_module.cpp new file mode 100644 index 0000000..7e917b9 --- /dev/null +++ b/pybind11/tests/standalone_enum_module.cpp @@ -0,0 +1,13 @@ +// Copyright (c) 2026 The pybind Community. + +#include + +namespace standalone_enum_module_ns { +enum SomeEnum {}; +} // namespace standalone_enum_module_ns + +using namespace standalone_enum_module_ns; + +PYBIND11_MODULE(standalone_enum_module, m) { // Added in PR #6015 + pybind11::enum_ some_enum_wrapper(m, "SomeEnum"); +} diff --git a/pybind11/tests/test_buffers.cpp b/pybind11/tests/test_buffers.cpp index a090c87..5ac4553 100644 --- a/pybind11/tests/test_buffers.cpp +++ b/pybind11/tests/test_buffers.cpp @@ -227,7 +227,7 @@ TEST_SUBMODULE(buffers, m) { + std::to_string(cols) + "(*" + std::to_string(col_factor) + ") matrix"); } - + DiscontiguousMatrix(const DiscontiguousMatrix &) = delete; ~DiscontiguousMatrix() { print_destroyed(this, std::to_string(rows() / m_row_factor) + "(*" @@ -237,11 +237,11 @@ TEST_SUBMODULE(buffers, m) { } float operator()(py::ssize_t i, py::ssize_t j) const { - return Matrix::operator()(i *m_row_factor, j *m_col_factor); + return Matrix::operator()(i * m_row_factor, j * m_col_factor); } float &operator()(py::ssize_t i, py::ssize_t j) { - return Matrix::operator()(i *m_row_factor, j *m_col_factor); + return Matrix::operator()(i * m_row_factor, j * m_col_factor); } using Matrix::data; @@ -439,4 +439,133 @@ TEST_SUBMODULE(buffers, m) { PyBuffer_Release(&buffer); return result; }); + + // test_noexcept_def_buffer (issue #2234) + // def_buffer(Return (Class::*)(Args...) noexcept) and + // def_buffer(Return (Class::*)(Args...) const noexcept) must compile and work correctly. + struct OneDBuffer { + // Declare m_data before m_n to match initialiser list order below. + float *m_data; + py::ssize_t m_n; + explicit OneDBuffer(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBuffer() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) noexcept) + py::buffer_info get_buffer() noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + + // non-const noexcept member function form + py::class_(m, "OneDBuffer", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBuffer::get_buffer); + + // const noexcept member function form (separate class to avoid ambiguity) + struct OneDBufferConst { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConst(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConst() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const noexcept) + py::buffer_info get_buffer() const noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_(m, "OneDBufferConst", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConst::get_buffer); + + // test_ref_qualified_def_buffer + struct OneDBufferLRef { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferLRef(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferLRef() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) &) + py::buffer_info get_buffer() & { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + py::class_(m, "OneDBufferLRef", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferLRef::get_buffer); + + struct OneDBufferConstLRef { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConstLRef(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConstLRef() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const &) + py::buffer_info get_buffer() const & { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_(m, "OneDBufferConstLRef", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConstLRef::get_buffer); + +#ifdef __cpp_noexcept_function_type + struct OneDBufferLRefNoexcept { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferLRefNoexcept(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferLRefNoexcept() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) & noexcept) + py::buffer_info get_buffer() & noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + py::class_(m, "OneDBufferLRefNoexcept", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferLRefNoexcept::get_buffer); + + struct OneDBufferConstLRefNoexcept { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConstLRefNoexcept(py::ssize_t n) + : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConstLRefNoexcept() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const & noexcept) + py::buffer_info get_buffer() const & noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_( + m, "OneDBufferConstLRefNoexcept", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConstLRefNoexcept::get_buffer); +#endif } diff --git a/pybind11/tests/test_buffers.py b/pybind11/tests/test_buffers.py index f666df5..c1d5770 100644 --- a/pybind11/tests/test_buffers.py +++ b/pybind11/tests/test_buffers.py @@ -7,7 +7,7 @@ import pytest import env -from pybind11_tests import ConstructorStats +from pybind11_tests import ConstructorStats, defined___cpp_noexcept_function_type from pybind11_tests import buffers as m np = pytest.importorskip("numpy") @@ -399,3 +399,73 @@ def check_strides(mat): m.get_py_buffer(dmat, m.PyBUF_ANY_CONTIGUOUS) with pytest.raises(expected_exception): m.get_py_buffer(dmat, m.PyBUF_F_CONTIGUOUS) + + +def test_noexcept_def_buffer(): + """Test issue #2234: def_buffer with noexcept member function pointers. + + Covers both new def_buffer specialisations: + - def_buffer(Return (Class::*)(Args...) noexcept) + - def_buffer(Return (Class::*)(Args...) const noexcept) + """ + # non-const noexcept member function form + buf = m.OneDBuffer(5) + arr = np.frombuffer(buf, dtype=np.float32) + assert arr.shape == (5,) + arr[2] = 3.14 + arr2 = np.frombuffer(buf, dtype=np.float32) + assert arr2[2] == pytest.approx(3.14) + + # const noexcept member function form + cbuf = m.OneDBufferConst(4) + carr = np.frombuffer(cbuf, dtype=np.float32) + assert carr.shape == (4,) + assert carr.flags["WRITEABLE"] is False + + +def test_ref_qualified_def_buffer(): + """Test issue #2234 follow-up: def_buffer with ref-qualified member function pointers. + + Covers: + - def_buffer(Return (Class::*)(Args...) &) + - def_buffer(Return (Class::*)(Args...) const &) + - def_buffer(Return (Class::*)(Args...) & noexcept) + - def_buffer(Return (Class::*)(Args...) const & noexcept) + """ + # non-const lvalue ref-qualified member function form + buf = m.OneDBufferLRef(5) + arr = np.frombuffer(buf, dtype=np.float32) + assert arr.shape == (5,) + arr[1] = 2.5 + arr2 = np.frombuffer(buf, dtype=np.float32) + assert arr2[1] == pytest.approx(2.5) + + # const lvalue ref-qualified member function form + cbuf = m.OneDBufferConstLRef(4) + carr = np.frombuffer(cbuf, dtype=np.float32) + assert carr.shape == (4,) + assert carr.flags["WRITEABLE"] is False + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_ref_qualified_noexcept_def_buffer(): + """Test issue #2234 follow-up: def_buffer with noexcept ref-qualified member pointers. + + Covers: + - def_buffer(Return (Class::*)(Args...) & noexcept) + - def_buffer(Return (Class::*)(Args...) const & noexcept) + """ + nbuf = m.OneDBufferLRefNoexcept(3) + narr = np.frombuffer(nbuf, dtype=np.float32) + assert narr.shape == (3,) + narr[2] = 7.0 + narr2 = np.frombuffer(nbuf, dtype=np.float32) + assert narr2[2] == pytest.approx(7.0) + + ncbuf = m.OneDBufferConstLRefNoexcept(2) + ncarr = np.frombuffer(ncbuf, dtype=np.float32) + assert ncarr.shape == (2,) + assert ncarr.flags["WRITEABLE"] is False diff --git a/pybind11/tests/test_builtin_casters.cpp b/pybind11/tests/test_builtin_casters.cpp index c516f8d..6cde727 100644 --- a/pybind11/tests/test_builtin_casters.cpp +++ b/pybind11/tests/test_builtin_casters.cpp @@ -363,9 +363,11 @@ TEST_SUBMODULE(builtin_casters, m) { m.def("complex_cast", [](float x) { return "{}"_s.format(x); }); m.def("complex_cast", [](std::complex x) { return "({}, {})"_s.format(x.real(), x.imag()); }); + m.def("complex_convert", [](std::complex x) { return x; }); + m.def("complex_noconvert", [](std::complex x) { return x; }, py::arg{}.noconvert()); // test int vs. long (Python 2) - m.def("int_cast", []() { return (int) 42; }); + m.def("int_cast", []() { return 42; }); m.def("long_cast", []() { return (long) 42; }); m.def("longlong_cast", []() { return ULLONG_MAX; }); diff --git a/pybind11/tests/test_builtin_casters.py b/pybind11/tests/test_builtin_casters.py index 7a2c6a4..23c191c 100644 --- a/pybind11/tests/test_builtin_casters.py +++ b/pybind11/tests/test_builtin_casters.py @@ -286,7 +286,10 @@ def __int__(self): convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert - assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt) -> int" + assert ( + doc(convert) + == "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + ) assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int" def requires_conversion(v): @@ -301,7 +304,7 @@ def cant_convert(v): # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) if sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): assert convert(Int()) == 42 else: assert convert(Int()) == 42 @@ -322,19 +325,39 @@ def cant_convert(v): def test_float_convert(doc): + class Int: + def __int__(self): + return -5 + + class Index: + def __index__(self) -> int: + return -7 + class Float: def __float__(self): return 41.45 convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert - assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat) -> float" + assert ( + doc(convert) + == "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float" + ) assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float" def requires_conversion(v): pytest.raises(TypeError, noconvert, v) + def cant_convert(v): + pytest.raises(TypeError, convert, v) + requires_conversion(Float()) + requires_conversion(Index()) assert pytest.approx(convert(Float())) == 41.45 + assert pytest.approx(convert(Index())) == -7.0 + assert isinstance(convert(Float()), float) + assert pytest.approx(convert(3)) == 3.0 + requires_conversion(3) + cant_convert(Int()) def test_numpy_int_convert(): @@ -354,7 +377,7 @@ def require_implicit(v): # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) # https://github.com/pybind/pybind11/issues/3408 if (3, 8) <= sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): assert convert(np.float32(3.14159)) == 3 else: assert convert(np.float32(3.14159)) == 3 @@ -381,7 +404,7 @@ def test_tuple(doc): assert ( doc(m.tuple_passthrough) == """ - tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt]) -> tuple[int, str, bool] + tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt | typing.SupportsIndex]) -> tuple[int, str, bool] Return a triple in reversed order """ @@ -458,11 +481,61 @@ def test_reference_wrapper(): assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10] -def test_complex_cast(): +def test_complex_cast(doc): """std::complex casts""" + + class Complex: + def __complex__(self) -> complex: + return complex(5, 4) + + class Float: + def __float__(self) -> float: + return 5.0 + + class Int: + def __int__(self) -> int: + return 3 + + class Index: + def __index__(self) -> int: + return 1 + assert m.complex_cast(1) == "1.0" + assert m.complex_cast(1.0) == "1.0" + assert m.complex_cast(Complex()) == "(5.0, 4.0)" assert m.complex_cast(2j) == "(0.0, 2.0)" + convert, noconvert = m.complex_convert, m.complex_noconvert + + def requires_conversion(v): + pytest.raises(TypeError, noconvert, v) + + def cant_convert(v): + pytest.raises(TypeError, convert, v) + + assert ( + doc(convert) + == "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex" + ) + assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex" + + assert convert(1) == 1.0 + assert convert(2.0) == 2.0 + assert convert(1 + 5j) == 1.0 + 5.0j + assert convert(Complex()) == 5.0 + 4j + assert convert(Float()) == 5.0 + assert isinstance(convert(Float()), complex) + cant_convert(Int()) + assert convert(Index()) == 1 + assert isinstance(convert(Index()), complex) + + requires_conversion(1) + requires_conversion(2.0) + assert noconvert(1 + 5j) == 1.0 + 5.0j + requires_conversion(Complex()) + requires_conversion(Float()) + requires_conversion(Index()) + def test_bool_caster(): """Test bool caster implicit conversions.""" diff --git a/pybind11/tests/test_callbacks.py b/pybind11/tests/test_callbacks.py index 09dadd9..327e41e 100644 --- a/pybind11/tests/test_callbacks.py +++ b/pybind11/tests/test_callbacks.py @@ -140,11 +140,11 @@ def test_cpp_function_roundtrip(): def test_function_signatures(doc): assert ( doc(m.test_callback3) - == "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt], int]) -> str" + == "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]) -> str" ) assert ( doc(m.test_callback4) - == "test_callback4() -> collections.abc.Callable[[typing.SupportsInt], int]" + == "test_callback4() -> collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]" ) @@ -179,10 +179,11 @@ def gen_f(): # do some work async work = [1, 2, 3, 4] m.test_async_callback(gen_f(), work) - # wait until work is done - from time import sleep - - sleep(0.5) + # Wait for all detached worker threads to finish. + deadline = time.monotonic() + 5.0 + while len(res) < len(work) and time.monotonic() < deadline: + time.sleep(0.01) + assert len(res) == len(work), f"Timed out waiting for callbacks: res={res!r}" assert sum(res) == sum(x + 3 for x in work) diff --git a/pybind11/tests/test_class.cpp b/pybind11/tests/test_class.cpp index 3d567fc..84efb80 100644 --- a/pybind11/tests/test_class.cpp +++ b/pybind11/tests/test_class.cpp @@ -58,6 +58,13 @@ class ForwardClass; class Args : public py::args {}; } // namespace pr5396_forward_declared_class +struct ConvertibleFromAnything { + ConvertibleFromAnything() = default; + template + // NOLINTNEXTLINE(bugprone-forwarding-reference-overload,google-explicit-constructor) + ConvertibleFromAnything(T &&) {} +}; + } // namespace test_class static_assert(py::detail::is_same_or_base_of::value, ""); @@ -97,6 +104,10 @@ TEST_SUBMODULE(class_, m) { ~NoConstructorNew() { print_destroyed(this); } }; + struct DynamicAttr { + DynamicAttr() = default; + }; + py::class_(m, "NoConstructor") .def_static("new_instance", &NoConstructor::new_instance, "Return an instance"); @@ -105,6 +116,8 @@ TEST_SUBMODULE(class_, m) { .def_static("__new__", [](const py::object &) { return NoConstructorNew::new_instance(); }); + py::class_(m, "DynamicAttr", py::dynamic_attr()).def(py::init<>()); + // test_pass_unique_ptr struct ToBeHeldByUniquePtr {}; py::class_>(m, "ToBeHeldByUniquePtr") @@ -521,6 +534,7 @@ TEST_SUBMODULE(class_, m) { // test_exception_rvalue_abort struct PyPrintDestructor { PyPrintDestructor() = default; + PyPrintDestructor(const PyPrintDestructor &) = default; ~PyPrintDestructor() { py::print("Print from destructor"); } void throw_something() { throw std::runtime_error("error"); } }; @@ -577,6 +591,11 @@ TEST_SUBMODULE(class_, m) { }); test_class::pr4220_tripped_over_this::bind_empty0(m); + + // Regression test for compiler error that showed up in #5866 + m.def("return_universal_recipient", []() -> test_class::ConvertibleFromAnything { + return test_class::ConvertibleFromAnything{}; + }); } template diff --git a/pybind11/tests/test_class.py b/pybind11/tests/test_class.py index 1e82930..201c7e3 100644 --- a/pybind11/tests/test_class.py +++ b/pybind11/tests/test_class.py @@ -1,5 +1,6 @@ from __future__ import annotations +import gc import sys from unittest import mock @@ -18,6 +19,13 @@ def refcount_immortal(ob: object) -> int: return sys.getrefcount(ob) +MANAGED_DICT_GET_REFERRERS_SUPPORTED = ( + env.CPYTHON + and sys.version_info >= (3, 13, 13) + and (sys.version_info < (3, 14) or sys.version_info >= (3, 14, 4)) +) + + def test_obj_class_name(): expected_name = "UserType" if env.PYPY else "pybind11_tests.UserType" assert m.obj_class_name(UserType(1)) == expected_name @@ -45,6 +53,16 @@ def test_instance(msg): assert cstats.alive() == 0 +@pytest.mark.skipif( + not MANAGED_DICT_GET_REFERRERS_SUPPORTED, + reason="Requires CPython 3.13.13+ or 3.14.4+ managed dict traversal support", +) +def test_get_referrers(): + instance = m.DynamicAttr() + instance.a = "test" + assert instance in gc.get_referrers(instance.__dict__) + + def test_instance_new(): instance = m.NoConstructorNew() # .__new__(m.NoConstructor.__class__) @@ -163,13 +181,13 @@ def test_qualname(doc): assert ( doc(m.NestBase.Nested.fn) == """ - fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None + fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None """ ) assert ( doc(m.NestBase.Nested.fa) == """ - fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None + fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt | typing.SupportsIndex, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None """ ) assert m.NestBase.__module__ == "pybind11_tests.class_" diff --git a/pybind11/tests/test_class_cross_module_use_after_one_module_dealloc.cpp b/pybind11/tests/test_class_cross_module_use_after_one_module_dealloc.cpp new file mode 100644 index 0000000..6f6e62d --- /dev/null +++ b/pybind11/tests/test_class_cross_module_use_after_one_module_dealloc.cpp @@ -0,0 +1,23 @@ +#include "pybind11_tests.h" + +#include + +class CrossDSOClass { +public: + CrossDSOClass() = default; + virtual ~CrossDSOClass(); + CrossDSOClass(const CrossDSOClass &) = default; +}; + +CrossDSOClass::~CrossDSOClass() = default; + +struct UnrelatedClass {}; + +TEST_SUBMODULE(class_cross_module_use_after_one_module_dealloc, m) { + m.def("register_and_instantiate_cross_dso_class", [](const py::module_ &m) { + py::class_(m, "CrossDSOClass").def(py::init<>()); + return CrossDSOClass(); + }); + m.def("register_unrelated_class", + [](const py::module_ &m) { py::class_(m, "UnrelatedClass"); }); +} diff --git a/pybind11/tests/test_class_cross_module_use_after_one_module_dealloc.py b/pybind11/tests/test_class_cross_module_use_after_one_module_dealloc.py new file mode 100644 index 0000000..58c8f72 --- /dev/null +++ b/pybind11/tests/test_class_cross_module_use_after_one_module_dealloc.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import types + +import pytest + +import env +from pybind11_tests import class_cross_module_use_after_one_module_dealloc as m + + +@pytest.mark.skipif( + env.TYPES_ARE_IMMORTAL, reason="can't GC type objects on this platform" +) +def test_cross_module_use_after_one_module_dealloc(): + # This is a regression test for a bug that occurred during development of + # internals::registered_types_cpp_fast (see #5842). registered_types_cpp_fast maps + # &typeid(T) to a raw non-owning pointer to a Python type object. If two DSOs both + # look up the same global type, they will create two separate entries in + # registered_types_cpp_fast, which will look like: + # +=========================================+ + # |&typeid(T) from DSO 1|type object pointer| + # |&typeid(T) from DSO 2|type object pointer| + # +=========================================+ + # + # Then, if the type object is destroyed and we don't take extra steps to clean up + # the table thoroughly, the first row of the table will be cleaned up but the second + # one will contain a dangling pointer to the old type object. Further lookups from + # DSO 2 will then return that dangling pointer, which will cause use-after-frees. + + import pybind11_cross_module_tests as cm + + module_scope = types.ModuleType("module_scope") + instance = m.register_and_instantiate_cross_dso_class(module_scope) + cm.consume_cross_dso_class(instance) + + del instance + pytest.delattr_and_ensure_destroyed((module_scope, "CrossDSOClass")) + + # Make sure that CrossDSOClass gets allocated at a different address. + m.register_unrelated_class(module_scope) + + instance = m.register_and_instantiate_cross_dso_class(module_scope) + cm.consume_cross_dso_class(instance) diff --git a/pybind11/tests/test_class_release_gil_before_calling_cpp_dtor.cpp b/pybind11/tests/test_class_release_gil_before_calling_cpp_dtor.cpp index a4869a8..7349c41 100644 --- a/pybind11/tests/test_class_release_gil_before_calling_cpp_dtor.cpp +++ b/pybind11/tests/test_class_release_gil_before_calling_cpp_dtor.cpp @@ -22,6 +22,7 @@ struct ProbeType { public: explicit ProbeType(const std::string &unique_key) : unique_key{unique_key} {} + ProbeType(const ProbeType &) = default; ~ProbeType() { RegistryType ® = PyGILState_Check_Results(); diff --git a/pybind11/tests/test_class_sh_basic.py b/pybind11/tests/test_class_sh_basic.py index 18f0b06..aaea87b 100644 --- a/pybind11/tests/test_class_sh_basic.py +++ b/pybind11/tests/test_class_sh_basic.py @@ -112,8 +112,8 @@ def test_pass_unique_ptr_disowns(pass_f, rtrn_f, expected): pass_f(obj) assert str(exc_info.value) == ( "Missing value for wrapped C++ type" - + " `pybind11_tests::class_sh_basic::atyp`:" - + " Python instance was disowned." + " `pybind11_tests::class_sh_basic::atyp`:" + " Python instance was disowned." ) diff --git a/pybind11/tests/test_class_sh_mi_thunks.cpp b/pybind11/tests/test_class_sh_mi_thunks.cpp index d8548ec..0d1672c 100644 --- a/pybind11/tests/test_class_sh_mi_thunks.cpp +++ b/pybind11/tests/test_class_sh_mi_thunks.cpp @@ -10,6 +10,8 @@ namespace test_class_sh_mi_thunks { // C++ vtables - Part 2 - Multiple Inheritance // ... the compiler creates a 'thunk' method that corrects `this` ... +// This test was added under PR #4380 + struct Base0 { virtual ~Base0() = default; Base0() = default; @@ -30,6 +32,110 @@ struct Derived : Base1, Base0 { Derived(const Derived &) = delete; }; +// ChatGPT-generated Diamond added under PR #5836 + +struct VBase { + VBase() = default; + VBase(const VBase &) = default; // silence -Wdeprecated-copy-with-dtor + VBase &operator=(const VBase &) = default; + VBase(VBase &&) = default; + VBase &operator=(VBase &&) = default; + virtual ~VBase() = default; + virtual int ping() const { return 1; } + int vbase_tag = 42; // ensure it's not empty +}; + +// Make the virtual bases non-empty and (likely) differently sized. +// The test does *not* require different sizes; we only want to avoid "all at offset 0". +// If a compiler/ABI still places the virtual base at offset 0, our test logs that via +// test_virtual_base_at_offset_0() and continues. +struct Left : virtual VBase { + char pad_l[4]; // small, typically 4 + padding + ~Left() override = default; +}; +struct Right : virtual VBase { + char pad_r[24]; // larger, to differ from Left + ~Right() override = default; +}; + +struct Diamond : Left, Right { + Diamond() = default; + Diamond(const Diamond &) = default; + ~Diamond() override = default; + int ping() const override { return 7; } + int self_tag = 99; +}; + +VBase *make_diamond_as_vbase_raw_ptr() { + auto *ptr = new Diamond; + return ptr; // upcast +} + +std::shared_ptr make_diamond_as_vbase_shared_ptr() { + auto shptr = std::make_shared(); + return shptr; // upcast +} + +std::unique_ptr make_diamond_as_vbase_unique_ptr() { + auto uqptr = std::unique_ptr(new Diamond); + return uqptr; // upcast +} + +// For diagnostics +struct DiamondAddrs { + uintptr_t as_self; + uintptr_t as_vbase; + uintptr_t as_left; + uintptr_t as_right; +}; + +DiamondAddrs diamond_addrs() { + auto sp = std::make_shared(); + return DiamondAddrs{reinterpret_cast(sp.get()), + reinterpret_cast(static_cast(sp.get())), + reinterpret_cast(static_cast(sp.get())), + reinterpret_cast(static_cast(sp.get()))}; +} + +// Animal-Cat-Tiger reproducer copied from PR #5796 +// clone_raw_ptr, clone_unique_ptr added under PR #5836 + +class Animal { +public: + Animal() = default; + Animal(const Animal &) = default; + Animal &operator=(const Animal &) = default; + virtual Animal *clone_raw_ptr() const = 0; + virtual std::shared_ptr clone_shared_ptr() const = 0; + virtual std::unique_ptr clone_unique_ptr() const = 0; + virtual ~Animal() = default; +}; + +class Cat : virtual public Animal { +public: + Cat() = default; + Cat(const Cat &) = default; + Cat &operator=(const Cat &) = default; + ~Cat() override = default; +}; + +class Tiger : virtual public Cat { +public: + Tiger() = default; + Tiger(const Tiger &) = default; + Tiger &operator=(const Tiger &) = default; + ~Tiger() override = default; + Animal *clone_raw_ptr() const override { + return new Tiger(*this); // upcast + } + std::shared_ptr clone_shared_ptr() const override { + return std::make_shared(*this); // upcast + } + std::unique_ptr clone_unique_ptr() const override { + return std::unique_ptr(new Tiger(*this)); // upcast + } +}; + } // namespace test_class_sh_mi_thunks TEST_SUBMODULE(class_sh_mi_thunks, m) { @@ -90,4 +196,35 @@ TEST_SUBMODULE(class_sh_mi_thunks, m) { } return obj_der->vec.size(); }); + + py::class_(m, "VBase").def("ping", &VBase::ping); + + py::class_(m, "Left"); + py::class_(m, "Right"); + + py::class_(m, "Diamond", py::multiple_inheritance()) + .def(py::init<>()) + .def("ping", &Diamond::ping); + + m.def("make_diamond_as_vbase_raw_ptr", + &make_diamond_as_vbase_raw_ptr, + py::return_value_policy::take_ownership); + m.def("make_diamond_as_vbase_shared_ptr", &make_diamond_as_vbase_shared_ptr); + m.def("make_diamond_as_vbase_unique_ptr", &make_diamond_as_vbase_unique_ptr); + + py::class_(m, "DiamondAddrs") + .def_readonly("as_self", &DiamondAddrs::as_self) + .def_readonly("as_vbase", &DiamondAddrs::as_vbase) + .def_readonly("as_left", &DiamondAddrs::as_left) + .def_readonly("as_right", &DiamondAddrs::as_right); + + m.def("diamond_addrs", &diamond_addrs); + + py::classh(m, "Animal"); + py::classh(m, "Cat"); + py::classh(m, "Tiger", py::multiple_inheritance()) + .def(py::init<>()) + .def("clone_raw_ptr", &Tiger::clone_raw_ptr) + .def("clone_shared_ptr", &Tiger::clone_shared_ptr) + .def("clone_unique_ptr", &Tiger::clone_unique_ptr); } diff --git a/pybind11/tests/test_class_sh_mi_thunks.py b/pybind11/tests/test_class_sh_mi_thunks.py index 32bf475..7fa164d 100644 --- a/pybind11/tests/test_class_sh_mi_thunks.py +++ b/pybind11/tests/test_class_sh_mi_thunks.py @@ -51,3 +51,54 @@ def test_get_shared_vec_size_unique(): assert ( str(exc_info.value) == "Cannot disown external shared_ptr (load_as_unique_ptr)." ) + + +def test_virtual_base_not_at_offset_0(): + # This test ensures that the Diamond fixture actually exercises a non-zero + # virtual-base subobject offset on our supported platforms/ABIs. + # + # If this assert ever fails on some platform/toolchain, please adjust the + # C++ fixture so the virtual base is *not* at offset 0: + # - Keep VBase non-empty. + # - Make Left and Right non-empty and asymmetrically sized and, if + # needed, nudge with a modest alignment. + # - The goal is to achieve a non-zero address delta between `Diamond*` + # and `static_cast(Diamond*)`. + # + # Rationale: certain smart_holder features are exercised only when the + # registered subobject address differs from the most-derived object start, + # so this check guards test efficacy across compilers. + addrs = m.diamond_addrs() + assert addrs.as_vbase - addrs.as_self != 0, ( + "Diamond VBase at offset 0 on this platform; to ensure test efficacy, " + "tweak fixtures (VBase/Left/Right) to ensure non-zero subobject offset." + ) + + +@pytest.mark.parametrize( + "make_fn", + [ + m.make_diamond_as_vbase_raw_ptr, # exercises smart_holder::from_raw_ptr_take_ownership + m.make_diamond_as_vbase_shared_ptr, # exercises smart_holder_from_shared_ptr + m.make_diamond_as_vbase_unique_ptr, # exercises smart_holder_from_unique_ptr + ], +) +def test_make_diamond_as_vbase(make_fn): + # Added under PR #5836 + vb = make_fn() + assert vb.ping() == 7 + + +@pytest.mark.parametrize( + "clone_fn", + [ + m.Tiger.clone_raw_ptr, + m.Tiger.clone_shared_ptr, + m.Tiger.clone_unique_ptr, + ], +) +def test_animal_cat_tiger(clone_fn): + # Based on Animal-Cat-Tiger reproducer under PR #5796 + tiger = m.Tiger() + cloned = clone_fn(tiger) + assert isinstance(cloned, m.Tiger) diff --git a/pybind11/tests/test_class_sh_property.cpp b/pybind11/tests/test_class_sh_property.cpp index 8863ad7..8777bdc 100644 --- a/pybind11/tests/test_class_sh_property.cpp +++ b/pybind11/tests/test_class_sh_property.cpp @@ -43,6 +43,24 @@ struct WithConstCharPtrMember { const char *const_char_ptr_member = "ConstChar*"; }; +// See PR #6008 +enum class EnumAB { + A = 0, + B = 1, +}; + +struct ShWithEnumABMember { + EnumAB level = EnumAB::A; +}; + +struct SimpleStruct { + int value = 7; +}; + +struct ShWithSimpleStructMember { + SimpleStruct legacy; +}; + } // namespace test_class_sh_property TEST_SUBMODULE(class_sh_property, m) { @@ -91,4 +109,21 @@ TEST_SUBMODULE(class_sh_property, m) { py::classh(m, "WithConstCharPtrMember") .def(py::init<>()) .def_readonly("const_char_ptr_member", &WithConstCharPtrMember::const_char_ptr_member); + + // See PR #6008 + py::enum_(m, "EnumAB").value("A", EnumAB::A).value("B", EnumAB::B); + + py::classh(m, "ShWithEnumABMember") + .def(py::init<>()) + .def_readwrite("level", &ShWithEnumABMember::level); + + py::class_(m, "SimpleStruct") + .def(py::init<>()) + .def_readwrite("value", &SimpleStruct::value); + + py::classh(m, "ShWithSimpleStructMember") + .def(py::init<>()) + .def_readwrite("legacy", &ShWithSimpleStructMember::legacy); + + m.def("getSimpleStructAsShared", []() { return std::make_shared(); }); } diff --git a/pybind11/tests/test_class_sh_property.py b/pybind11/tests/test_class_sh_property.py index 0250a7f..b8a5933 100644 --- a/pybind11/tests/test_class_sh_property.py +++ b/pybind11/tests/test_class_sh_property.py @@ -5,6 +5,7 @@ import pytest import env # noqa: F401 +import pybind11_tests from pybind11_tests import class_sh_property as m @@ -164,3 +165,54 @@ def test_readonly_char6_member(): def test_readonly_const_char_ptr_member(): obj = m.WithConstCharPtrMember() assert obj.const_char_ptr_member == "ConstChar*" + + +# See PR #6008 +def test_enum_member_with_smart_holder_def_readwrite(): + obj = m.ShWithEnumABMember() + assert obj.level == m.EnumAB.A + for _ in range(100): + v = obj.level + assert v == m.EnumAB.A + del v + + +# See PR #6008 +def test_non_smart_holder_member_type_with_smart_holder_owner(): + obj = m.ShWithSimpleStructMember() + for _ in range(1000): + v = obj.legacy + assert v.value == 7 + del v + + +# See PR #6008, previously this was UB +@pytest.mark.skipif( + pybind11_tests.PYBIND11_TEST_SMART_HOLDER, + reason="PYBIND11_TEST_SMART_HOLDER changes the default holder", +) +def test_shared_ptr_return_for_unique_ptr_holder(): + with pytest.raises( + RuntimeError, + match="Unable to convert std::shared_ptr to Python when the bound type does not use std::shared_ptr or py::smart_holder as its holder type", + ): + m.getSimpleStructAsShared() + + +def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member(): + obj = m.ShWithSimpleStructMember() + legacy = obj.legacy + legacy.value = 13 + assert obj.legacy.value == 13 + + +def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member_multiple_reads(): + obj = m.ShWithSimpleStructMember() + + a = obj.legacy + b = obj.legacy + + a.value = 13 + + assert b.value == 13 + assert obj.legacy.value == 13 diff --git a/pybind11/tests/test_class_sh_property_non_owning.cpp b/pybind11/tests/test_class_sh_property_non_owning.cpp index 45fe7c7..5a5877e 100644 --- a/pybind11/tests/test_class_sh_property_non_owning.cpp +++ b/pybind11/tests/test_class_sh_property_non_owning.cpp @@ -33,6 +33,8 @@ struct DataFieldsHolder { } } + DataFieldsHolder(DataFieldsHolder &&) noexcept = default; + DataField *vec_at(std::size_t index) { if (index >= vec.size()) { return nullptr; diff --git a/pybind11/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp b/pybind11/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp index 5580848..6936379 100644 --- a/pybind11/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp +++ b/pybind11/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp @@ -36,9 +36,9 @@ struct PySpBase : SpBase, py::trampoline_self_life_support { struct SpBaseTester { std::shared_ptr get_object() const { return m_obj; } void set_object(std::shared_ptr obj) { m_obj = std::move(obj); } - bool is_base_used() { return m_obj->is_base_used(); } - bool has_instance() { return (bool) m_obj; } - bool has_python_instance() { return m_obj && m_obj->has_python_instance(); } + bool is_base_used() const { return m_obj->is_base_used(); } + bool has_instance() const { return (bool) m_obj; } + bool has_python_instance() const { return m_obj && m_obj->has_python_instance(); } void set_nonpython_instance() { m_obj = std::make_shared(); } std::shared_ptr m_obj; }; diff --git a/pybind11/tests/test_cmake_build/installed_embed/CMakeLists.txt b/pybind11/tests/test_cmake_build/installed_embed/CMakeLists.txt index 8561ef4..a364966 100644 --- a/pybind11/tests/test_cmake_build/installed_embed/CMakeLists.txt +++ b/pybind11/tests/test_cmake_build/installed_embed/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) project(test_installed_embed CXX) diff --git a/pybind11/tests/test_cmake_build/installed_function/CMakeLists.txt b/pybind11/tests/test_cmake_build/installed_function/CMakeLists.txt index 8e75f86..d277411 100644 --- a/pybind11/tests/test_cmake_build/installed_function/CMakeLists.txt +++ b/pybind11/tests/test_cmake_build/installed_function/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) project(test_installed_function CXX) diff --git a/pybind11/tests/test_cmake_build/installed_target/CMakeLists.txt b/pybind11/tests/test_cmake_build/installed_target/CMakeLists.txt index d8af439..6ee0169 100644 --- a/pybind11/tests/test_cmake_build/installed_target/CMakeLists.txt +++ b/pybind11/tests/test_cmake_build/installed_target/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) project(test_installed_target CXX) diff --git a/pybind11/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt b/pybind11/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt index d84916e..81b4458 100644 --- a/pybind11/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt +++ b/pybind11/tests/test_cmake_build/subdirectory_embed/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) project(test_subdirectory_embed CXX) @@ -26,11 +26,11 @@ add_custom_target( DEPENDS test_subdirectory_embed) # Test custom export group -- PYBIND11_EXPORT_NAME -add_library(test_embed_lib ../embed.cpp) -target_link_libraries(test_embed_lib PRIVATE pybind11::embed) +add_library(test_with_catch_lib ../embed.cpp) +target_link_libraries(test_with_catch_lib PRIVATE pybind11::embed) install( - TARGETS test_embed_lib + TARGETS test_with_catch_lib EXPORT test_export ARCHIVE DESTINATION bin LIBRARY DESTINATION lib diff --git a/pybind11/tests/test_cmake_build/subdirectory_function/CMakeLists.txt b/pybind11/tests/test_cmake_build/subdirectory_function/CMakeLists.txt index f355492..10b283d 100644 --- a/pybind11/tests/test_cmake_build/subdirectory_function/CMakeLists.txt +++ b/pybind11/tests/test_cmake_build/subdirectory_function/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) project(test_subdirectory_function CXX) diff --git a/pybind11/tests/test_cmake_build/subdirectory_target/CMakeLists.txt b/pybind11/tests/test_cmake_build/subdirectory_target/CMakeLists.txt index aadc8c0..88d73f6 100644 --- a/pybind11/tests/test_cmake_build/subdirectory_target/CMakeLists.txt +++ b/pybind11/tests/test_cmake_build/subdirectory_target/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) project(test_subdirectory_target CXX) diff --git a/pybind11/tests/test_cpp_conduit.py b/pybind11/tests/test_cpp_conduit.py index 5650a65..652b9b9 100644 --- a/pybind11/tests/test_cpp_conduit.py +++ b/pybind11/tests/test_cpp_conduit.py @@ -12,7 +12,7 @@ def import_warns_freethreaded(name): - if name not in sys.modules and not getattr(sys, "_is_gil_enabled", lambda: True)(): + if name not in sys.modules and not env.sys_is_gil_enabled(): with pytest.warns( RuntimeWarning, match=f"has been enabled to load module '{name}'" ): diff --git a/pybind11/tests/test_cross_module_rtti/CMakeLists.txt b/pybind11/tests/test_cross_module_rtti/CMakeLists.txt index 97d2c78..c9b95bf 100644 --- a/pybind11/tests/test_cross_module_rtti/CMakeLists.txt +++ b/pybind11/tests/test_cross_module_rtti/CMakeLists.txt @@ -60,7 +60,9 @@ add_custom_target( test_cross_module_rtti COMMAND "$" DEPENDS test_cross_module_rtti_main - WORKING_DIRECTORY "$") + WORKING_DIRECTORY "$" + USES_TERMINAL # Ensures output is shown immediately (not buffered and possibly lost on crash) +) set_target_properties(test_cross_module_rtti_bindings PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/pybind11/tests/test_cross_module_rtti/lib.h b/pybind11/tests/test_cross_module_rtti/lib.h index 0925b08..2f76043 100644 --- a/pybind11/tests/test_cross_module_rtti/lib.h +++ b/pybind11/tests/test_cross_module_rtti/lib.h @@ -12,6 +12,7 @@ __pragma(warning(disable : 4251)) class TEST_CROSS_MODULE_RTTI_LIB_EXPORT Base : public std::enable_shared_from_this { public: Base(int a, int b); + Base(const Base &) = default; virtual ~Base() = default; virtual int get() const; diff --git a/pybind11/tests/test_custom_type_casters.py b/pybind11/tests/test_custom_type_casters.py index bf31d3f..6ed1c56 100644 --- a/pybind11/tests/test_custom_type_casters.py +++ b/pybind11/tests/test_custom_type_casters.py @@ -75,7 +75,7 @@ def test_noconvert_args(msg): msg(excinfo.value) == """ ints_preferred(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt) -> int + 1. (i: typing.SupportsInt | typing.SupportsIndex) -> int Invoked with: 4.0 """ diff --git a/pybind11/tests/test_custom_type_setup.cpp b/pybind11/tests/test_custom_type_setup.cpp index 35d30ab..15516b2 100644 --- a/pybind11/tests/test_custom_type_setup.cpp +++ b/pybind11/tests/test_custom_type_setup.cpp @@ -7,22 +7,67 @@ BSD-style license that can be found in the LICENSE file. */ +#include #include #include "pybind11_tests.h" +#include + namespace py = pybind11; namespace { +struct ContainerOwnsPythonObjects { + std::vector list; -struct OwnsPythonObjects { - py::object value = py::none(); + void append(const py::object &obj) { list.emplace_back(obj); } + py::object at(py::ssize_t index) const { + if (index >= size() || index < 0) { + throw py::index_error("Index out of range"); + } + return list.at(py::size_t(index)); + } + py::ssize_t size() const { return py::ssize_t_cast(list.size()); } + void clear() { list.clear(); } }; + +void add_gc_checkers_with_weakrefs(const py::object &obj) { + py::handle global_capsule = py::detail::get_internals_capsule(); + if (!global_capsule) { + throw std::runtime_error("No global internals capsule found"); + } + (void) py::weakref(obj, py::cpp_function([global_capsule](py::handle weakref) -> void { + py::handle current_global_capsule = py::detail::get_internals_capsule(); + if (!current_global_capsule.is(global_capsule)) { + throw std::runtime_error( + "Global internals capsule was destroyed prematurely"); + } + weakref.dec_ref(); + })) + .release(); + + py::handle local_capsule = py::detail::get_local_internals_capsule(); + if (!local_capsule) { + throw std::runtime_error("No local internals capsule found"); + } + (void) py::weakref( + obj, py::cpp_function([local_capsule](py::handle weakref) -> void { + py::handle current_local_capsule = py::detail::get_local_internals_capsule(); + if (!current_local_capsule.is(local_capsule)) { + throw std::runtime_error("Local internals capsule was destroyed prematurely"); + } + weakref.dec_ref(); + })) + .release(); +} } // namespace TEST_SUBMODULE(custom_type_setup, m) { - py::class_ cls( - m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) { + py::class_ cls( + m, + "ContainerOwnsPythonObjects", + // Please review/update docs/advanced/classes.rst after making changes here. + py::custom_type_setup([](PyHeapTypeObject *heap_type) { auto *type = &heap_type->ht_type; type->tp_flags |= Py_TPFLAGS_HAVE_GC; type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) { @@ -31,19 +76,29 @@ TEST_SUBMODULE(custom_type_setup, m) { Py_VISIT(Py_TYPE(self_base)); #endif if (py::detail::is_holder_constructed(self_base)) { - auto &self = py::cast(py::handle(self_base)); - Py_VISIT(self.value.ptr()); + auto &self = py::cast(py::handle(self_base)); + for (auto &item : self.list) { + Py_VISIT(item.ptr()); + } } return 0; }; type->tp_clear = [](PyObject *self_base) { if (py::detail::is_holder_constructed(self_base)) { - auto &self = py::cast(py::handle(self_base)); - self.value = py::none(); + auto &self = py::cast(py::handle(self_base)); + for (auto &item : self.list) { + Py_CLEAR(item.ptr()); + } + self.list.clear(); } return 0; }; })); cls.def(py::init<>()); - cls.def_readwrite("value", &OwnsPythonObjects::value); + cls.def("append", &ContainerOwnsPythonObjects::append); + cls.def("at", &ContainerOwnsPythonObjects::at); + cls.def("size", &ContainerOwnsPythonObjects::size); + cls.def("clear", &ContainerOwnsPythonObjects::clear); + + m.def("add_gc_checkers_with_weakrefs", &add_gc_checkers_with_weakrefs); } diff --git a/pybind11/tests/test_custom_type_setup.py b/pybind11/tests/test_custom_type_setup.py index bb2865c..a200b02 100644 --- a/pybind11/tests/test_custom_type_setup.py +++ b/pybind11/tests/test_custom_type_setup.py @@ -1,11 +1,13 @@ from __future__ import annotations import gc +import os import weakref import pytest -import env # noqa: F401 +import env +import pybind11_tests from pybind11_tests import custom_type_setup as m @@ -36,15 +38,42 @@ def add_ref(obj): # PyPy does not seem to reliably garbage collect. @pytest.mark.skipif("env.PYPY or env.GRAALPY") def test_self_cycle(gc_tester): - obj = m.OwnsPythonObjects() - obj.value = obj + obj = m.ContainerOwnsPythonObjects() + obj.append(obj) gc_tester(obj) # PyPy does not seem to reliably garbage collect. @pytest.mark.skipif("env.PYPY or env.GRAALPY") def test_indirect_cycle(gc_tester): - obj = m.OwnsPythonObjects() - obj_list = [obj] - obj.value = obj_list + obj = m.ContainerOwnsPythonObjects() + obj.append([obj]) gc_tester(obj) + + +@pytest.mark.skipif("env.PYPY or env.GRAALPY") +def test_py_cast_useable_on_shutdown(): + """Test that py::cast works during interpreter shutdown. + + See PR #5972 and https://github.com/pybind/pybind11/pull/5958#discussion_r2717645230. + """ + env.check_script_success_in_subprocess( + f""" + import sys + + sys.path.insert(0, {os.path.dirname(env.__file__)!r}) + sys.path.insert(0, {os.path.dirname(pybind11_tests.__file__)!r}) + + from pybind11_tests import custom_type_setup as m + + # Create a self-referential cycle that will be collected during shutdown. + # The tp_traverse and tp_clear callbacks call py::cast, which requires + # internals to still be valid. + obj = m.ContainerOwnsPythonObjects() + obj.append(obj) + + # Add weakref callbacks that verify the capsule is still alive when the + # pybind11 object is garbage collected during shutdown. + m.add_gc_checkers_with_weakrefs(obj) + """ + ) diff --git a/pybind11/tests/test_docstring_options.py b/pybind11/tests/test_docstring_options.py index f2a1048..ffd1a73 100644 --- a/pybind11/tests/test_docstring_options.py +++ b/pybind11/tests/test_docstring_options.py @@ -20,11 +20,11 @@ def test_docstring_options(): # options.enable_function_signatures() assert m.test_function3.__doc__.startswith( - "test_function3(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function3(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function4.__doc__.startswith( - "test_function4(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function4(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function4.__doc__.endswith("A custom docstring\n") @@ -37,7 +37,7 @@ def test_docstring_options(): # RAII destructor assert m.test_function7.__doc__.startswith( - "test_function7(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function7(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function7.__doc__.endswith("A custom docstring\n") @@ -48,7 +48,7 @@ def test_docstring_options(): assert not m.DocstringTestFoo.__doc__ assert not m.DocstringTestFoo.value_prop.__doc__ - # Check existig behaviour of enum docstings + # Check existing behaviour of enum docstings assert ( m.DocstringTestEnum1.__doc__ == "Enum docstring\n\nMembers:\n\n Member1\n\n Member2" diff --git a/pybind11/tests/test_eigen_matrix.cpp b/pybind11/tests/test_eigen_matrix.cpp index f141c18..1f65a90 100644 --- a/pybind11/tests/test_eigen_matrix.cpp +++ b/pybind11/tests/test_eigen_matrix.cpp @@ -259,6 +259,7 @@ TEST_SUBMODULE(eigen_matrix, m) { public: ReturnTester() { print_created(this); } + ReturnTester(const ReturnTester &) = default; ~ReturnTester() { print_destroyed(this); } static Eigen::MatrixXd create() { return Eigen::MatrixXd::Ones(10, 10); } // NOLINTNEXTLINE(readability-const-return-type) diff --git a/pybind11/tests/test_embed/catch.cpp b/pybind11/tests/test_embed/catch.cpp deleted file mode 100644 index 558a7a3..0000000 --- a/pybind11/tests/test_embed/catch.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// The Catch implementation is compiled here. This is a standalone -// translation unit to avoid recompiling it for every test change. - -#include - -// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to -// catch 2.0.1; this should be fixed in the next catch release after 2.0.1). -PYBIND11_WARNING_DISABLE_MSVC(4996) - -// Catch uses _ internally, which breaks gettext style defines -#ifdef _ -# undef _ -#endif - -#define CATCH_CONFIG_RUNNER -#include - -namespace py = pybind11; - -int main(int argc, char *argv[]) { - // Setup for TEST_CASE in test_interpreter.cpp, tagging on a large random number: - std::string updated_pythonpath("pybind11_test_embed_PYTHONPATH_2099743835476552"); - const char *preexisting_pythonpath = getenv("PYTHONPATH"); - if (preexisting_pythonpath != nullptr) { -#if defined(_WIN32) - updated_pythonpath += ';'; -#else - updated_pythonpath += ':'; -#endif - updated_pythonpath += preexisting_pythonpath; - } -#if defined(_WIN32) - _putenv_s("PYTHONPATH", updated_pythonpath.c_str()); -#else - setenv("PYTHONPATH", updated_pythonpath.c_str(), /*replace=*/1); -#endif - - py::scoped_interpreter guard{}; - - auto result = Catch::Session().run(argc, argv); - - return result < 0xff ? result : 0xff; -} diff --git a/pybind11/tests/test_enum.py b/pybind11/tests/test_enum.py index 99d4a88..f295b01 100644 --- a/pybind11/tests/test_enum.py +++ b/pybind11/tests/test_enum.py @@ -328,7 +328,7 @@ def test_generated_dunder_methods_pos_only(): ) assert ( re.match( - r"^__setstate__\(self: [\w\.]+, state: [\w\.]+, /\)", + r"^__setstate__\(self: [\w\.]+, state: [\w\. \|]+, /\)", enum_type.__setstate__.__doc__, ) is not None diff --git a/pybind11/tests/test_exceptions.py b/pybind11/tests/test_exceptions.py index 79b3879..59845b4 100644 --- a/pybind11/tests/test_exceptions.py +++ b/pybind11/tests/test_exceptions.py @@ -76,9 +76,9 @@ def test_cross_module_exceptions(msg): # TODO: FIXME @pytest.mark.xfail( - "(env.MACOS and env.PYPY) or env.ANDROID", + "(env.MACOS and env.PYPY) or env.ANDROID or env.FREEBSD", raises=RuntimeError, - reason="See Issue #2847, PR #2999, PR #4324", + reason="See Issue #2847, PR #2999, PR #4324, PR #5925", strict=not env.PYPY, # PR 5569 ) def test_cross_module_exception_translator(): diff --git a/pybind11/tests/test_factory_constructors.cpp b/pybind11/tests/test_factory_constructors.cpp index a387cd2..c96a3a3 100644 --- a/pybind11/tests/test_factory_constructors.cpp +++ b/pybind11/tests/test_factory_constructors.cpp @@ -73,7 +73,7 @@ class TestFactory3 { // Inheritance test class TestFactory4 : public TestFactory3 { public: - TestFactory4() : TestFactory3() { print_default_created(this); } + TestFactory4() { print_default_created(this); } explicit TestFactory4(int v) : TestFactory3(v) { print_created(this, v); } ~TestFactory4() override { print_destroyed(this); } }; @@ -376,10 +376,14 @@ TEST_SUBMODULE(factory_constructors, m) { py::print("noisy placement new"); return p; } - static void operator delete(void *p, size_t) { + static void operator delete(void *p) noexcept { py::print("noisy delete"); ::operator delete(p); } + static void operator delete(void *p, size_t) { + py::print("noisy delete size"); + ::operator delete(p); + } static void operator delete(void *, void *) { py::print("noisy placement delete"); } }; diff --git a/pybind11/tests/test_factory_constructors.py b/pybind11/tests/test_factory_constructors.py index 67f859b..c6ae98c 100644 --- a/pybind11/tests/test_factory_constructors.py +++ b/pybind11/tests/test_factory_constructors.py @@ -78,10 +78,10 @@ def test_init_factory_signature(msg): msg(excinfo.value) == """ __init__(): incompatible constructor arguments. The following argument types are supported: - 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt) + 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt | typing.SupportsIndex) 2. m.factory_constructors.TestFactory1(arg0: str) 3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag) - 4. m.factory_constructors.TestFactory1(arg0: object, arg1: typing.SupportsInt, arg2: object) + 4. m.factory_constructors.TestFactory1(arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: object) Invoked with: 'invalid', 'constructor', 'arguments' """ @@ -93,13 +93,13 @@ def test_init_factory_signature(msg): __init__(*args, **kwargs) Overloaded function. - 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt) -> None + 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt | typing.SupportsIndex) -> None 2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None 3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None - 4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: typing.SupportsInt, arg2: object) -> None + 4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: object) -> None """ ) diff --git a/pybind11/tests/test_gil_scoped.py b/pybind11/tests/test_gil_scoped.py index 04f7b09..fc998b0 100644 --- a/pybind11/tests/test_gil_scoped.py +++ b/pybind11/tests/test_gil_scoped.py @@ -168,6 +168,9 @@ def _intentional_deadlock(): def _run_in_process(target, *args, **kwargs): + if env.ANDROID or env.IOS or sys.platform.startswith("emscripten"): + pytest.skip("Requires subprocess support") + test_fn = target if len(args) == 0 else args[0] # Do not need to wait much, 10s should be more than enough. timeout = 0.1 if test_fn is _intentional_deadlock else 10 @@ -199,8 +202,14 @@ def _run_in_process(target, *args, **kwargs): if process.exitcode is None: assert t_delta > 0.9 * timeout msg = "DEADLOCK, most likely, exactly what this test is meant to detect." - if env.PYPY and env.WIN: - pytest.skip(msg) + soabi = sysconfig.get_config_var("SOABI") + if env.WIN and env.PYPY: + pytest.xfail(f"[TEST-GIL-SCOPED] {soabi} PyPy: " + msg) + if env.MACOS: + if not env.sys_is_gil_enabled(): + pytest.xfail(f"[TEST-GIL-SCOPED] {soabi} with GIL disabled: " + msg) + if env.PY_GIL_DISABLED: + pytest.xfail(f"[TEST-GIL-SCOPED] {soabi}: " + msg) raise RuntimeError(msg) return process.exitcode finally: diff --git a/pybind11/tests/test_iostream.cpp b/pybind11/tests/test_iostream.cpp index 421eaa2..7484e73 100644 --- a/pybind11/tests/test_iostream.cpp +++ b/pybind11/tests/test_iostream.cpp @@ -123,4 +123,40 @@ TEST_SUBMODULE(iostream, m) { .def("stop", &TestThread::stop) .def("join", &TestThread::join) .def("sleep", &TestThread::sleep); + + m.def("move_redirect_output", [](const std::string &msg_before, const std::string &msg_after) { + py::scoped_ostream_redirect redir1(std::cout, py::module_::import("sys").attr("stdout")); + std::cout << msg_before << std::flush; + py::scoped_ostream_redirect redir2(std::move(redir1)); + std::cout << msg_after << std::flush; + }); + + m.def("move_redirect_output_unflushed", + [](const std::string &msg_before, const std::string &msg_after) { + py::scoped_ostream_redirect redir1(std::cout, + py::module_::import("sys").attr("stdout")); + std::cout << msg_before; + py::scoped_ostream_redirect redir2(std::move(redir1)); + std::cout << msg_after << std::flush; + }); + + // Redirect a stream whose original rdbuf is nullptr, then move the redirect. + // Verifies that nullptr is correctly restored (not confused with a moved-from sentinel). + m.def("move_redirect_null_rdbuf", [](const std::string &msg) { + std::ostream os(nullptr); + py::scoped_ostream_redirect redir1(os, py::module_::import("sys").attr("stdout")); + os << msg << std::flush; + py::scoped_ostream_redirect redir2(std::move(redir1)); + os << msg << std::flush; + // After redir2 goes out of scope, os.rdbuf() should be restored to nullptr. + }); + + m.def("get_null_rdbuf_restored", [](const std::string &msg) -> bool { + std::ostream os(nullptr); + { + py::scoped_ostream_redirect redir(os, py::module_::import("sys").attr("stdout")); + os << msg << std::flush; + } + return os.rdbuf() == nullptr; + }); } diff --git a/pybind11/tests/test_iostream.py b/pybind11/tests/test_iostream.py index c3d9877..857e0b5 100644 --- a/pybind11/tests/test_iostream.py +++ b/pybind11/tests/test_iostream.py @@ -6,8 +6,19 @@ import pytest +import env from pybind11_tests import iostream as m +if env.WIN: + wv_build = sys.getwindowsversion().build + skip_if_ge = 26100 + if wv_build >= skip_if_ge: + pytest.skip( + f"Windows build {wv_build} >= {skip_if_ge}:" + " Skipping iostream capture (redirection regression needs investigation)", + allow_module_level=True, + ) + def test_captured(capsys): msg = "I've been redirected to Python, I hope!" @@ -273,15 +284,36 @@ def test_redirect_both(capfd): assert stream2.getvalue() == msg2 +def test_move_redirect(capsys): + m.move_redirect_output("before_move", "after_move") + stdout, stderr = capsys.readouterr() + assert stdout == "before_moveafter_move" + assert not stderr + + +def test_move_redirect_unflushed(capsys): + m.move_redirect_output_unflushed("before_move", "after_move") + stdout, stderr = capsys.readouterr() + assert stdout == "before_moveafter_move" + assert not stderr + + +def test_move_redirect_null_rdbuf(capsys): + m.move_redirect_null_rdbuf("hello") + stdout, stderr = capsys.readouterr() + assert stdout == "hellohello" + assert not stderr + + +def test_null_rdbuf_restored(): + assert m.get_null_rdbuf_restored("test") + + @pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_threading(): with m.ostream_redirect(stdout=True, stderr=False): # start some threads - threads = [] - - # start some threads - for _j in range(20): - threads.append(m.TestThread()) + threads = [m.TestThread() for _j in range(20)] # give the threads some time to fail threads[0].sleep() diff --git a/pybind11/tests/test_kwargs_and_defaults.py b/pybind11/tests/test_kwargs_and_defaults.py index b62e4b7..a7745d1 100644 --- a/pybind11/tests/test_kwargs_and_defaults.py +++ b/pybind11/tests/test_kwargs_and_defaults.py @@ -9,44 +9,45 @@ def test_function_signatures(doc): assert ( doc(m.kw_func0) - == "kw_func0(arg0: typing.SupportsInt, arg1: typing.SupportsInt) -> str" + == "kw_func0(arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> str" ) assert ( doc(m.kw_func1) - == "kw_func1(x: typing.SupportsInt, y: typing.SupportsInt) -> str" + == "kw_func1(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex) -> str" ) assert ( doc(m.kw_func2) - == "kw_func2(x: typing.SupportsInt = 100, y: typing.SupportsInt = 200) -> str" + == "kw_func2(x: typing.SupportsInt | typing.SupportsIndex = 100, y: typing.SupportsInt | typing.SupportsIndex = 200) -> str" ) assert doc(m.kw_func3) == "kw_func3(data: str = 'Hello world!') -> None" assert ( doc(m.kw_func4) - == "kw_func4(myList: collections.abc.Sequence[typing.SupportsInt] = [13, 17]) -> str" + == "kw_func4(myList: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = [13, 17]) -> str" ) assert ( doc(m.kw_func_udl) - == "kw_func_udl(x: typing.SupportsInt, y: typing.SupportsInt = 300) -> str" + == "kw_func_udl(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex = 300) -> str" ) assert ( doc(m.kw_func_udl_z) - == "kw_func_udl_z(x: typing.SupportsInt, y: typing.SupportsInt = 0) -> str" + == "kw_func_udl_z(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex = 0) -> str" ) assert doc(m.args_function) == "args_function(*args) -> tuple" assert ( - doc(m.args_kwargs_function) == "args_kwargs_function(*args, **kwargs) -> tuple" + doc(m.args_kwargs_function) + == "args_kwargs_function(*args, **kwargs) -> tuple[tuple, dict[str, typing.Any]]" ) assert ( doc(m.args_kwargs_subclass_function) - == "args_kwargs_subclass_function(*args: str, **kwargs: str) -> tuple" + == "args_kwargs_subclass_function(*args: str, **kwargs: str) -> tuple[tuple[str, ...], dict[str, str]]" ) assert ( doc(m.KWClass.foo0) - == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: typing.SupportsInt, arg1: typing.SupportsFloat) -> None" + == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex) -> None" ) assert ( doc(m.KWClass.foo1) - == "foo1(self: m.kwargs_and_defaults.KWClass, x: typing.SupportsInt, y: typing.SupportsFloat) -> None" + == "foo1(self: m.kwargs_and_defaults.KWClass, x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsFloat | typing.SupportsIndex) -> None" ) assert ( doc(m.kw_lb_func0) @@ -138,7 +139,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt, arg1: typing.SupportsFloat, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple[int, float, tuple] Invoked with: 1 """ @@ -149,7 +150,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt, arg1: typing.SupportsFloat, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple[int, float, tuple] Invoked with: """ @@ -183,7 +184,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple[int, float, tuple, dict[str, typing.Any]] Invoked with: 1; kwargs: i=1 """ @@ -194,7 +195,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple[int, float, tuple, dict[str, typing.Any]] Invoked with: 1, 2; kwargs: j=1 """ @@ -211,7 +212,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ args_kwonly(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt, j: typing.SupportsFloat, *args, z: typing.SupportsInt) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, float, tuple, int] Invoked with: 2, 2.5, 22 """ @@ -233,12 +234,12 @@ def test_mixed_args_and_kwargs(msg): ) assert ( m.args_kwonly_kwargs.__doc__ - == "args_kwonly_kwargs(i: typing.SupportsInt, j: typing.SupportsFloat, *args, z: typing.SupportsInt, **kwargs) -> tuple\n" + == "args_kwonly_kwargs(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex, **kwargs) -> tuple[int, float, tuple, int, dict[str, typing.Any]]\n" ) assert ( m.args_kwonly_kwargs_defaults.__doc__ - == "args_kwonly_kwargs_defaults(i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, z: typing.SupportsInt = 42, **kwargs) -> tuple\n" + == "args_kwonly_kwargs_defaults(i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple[int, float, tuple, int, dict[str, typing.Any]]\n" ) assert m.args_kwonly_kwargs_defaults() == (1, 3.14159, (), 42, {}) assert m.args_kwonly_kwargs_defaults(2) == (2, 3.14159, (), 42, {}) @@ -294,11 +295,11 @@ def test_keyword_only_args(msg): x.method(i=1, j=2) assert ( m.first_arg_kw_only.__init__.__doc__ - == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt = 0) -> None\n" + == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt | typing.SupportsIndex = 0) -> None\n" ) assert ( m.first_arg_kw_only.method.__doc__ - == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt = 1, j: typing.SupportsInt = 2) -> None\n" + == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsInt | typing.SupportsIndex = 2) -> None\n" ) @@ -344,7 +345,7 @@ def test_positional_only_args(): # Mix it with args and kwargs: assert ( m.args_kwonly_full_monty.__doc__ - == "args_kwonly_full_monty(arg0: typing.SupportsInt = 1, arg1: typing.SupportsInt = 2, /, j: typing.SupportsFloat = 3.14159, *args, z: typing.SupportsInt = 42, **kwargs) -> tuple\n" + == "args_kwonly_full_monty(arg0: typing.SupportsInt | typing.SupportsIndex = 1, arg1: typing.SupportsInt | typing.SupportsIndex = 2, /, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple[int, int, float, tuple, int, dict[str, typing.Any]]\n" ) assert m.args_kwonly_full_monty() == (1, 2, 3.14159, (), 42, {}) assert m.args_kwonly_full_monty(8) == (8, 2, 3.14159, (), 42, {}) @@ -387,30 +388,30 @@ def test_positional_only_args(): # https://github.com/pybind/pybind11/pull/3402#issuecomment-963341987 assert ( m.first_arg_kw_only.pos_only.__doc__ - == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: typing.SupportsInt, j: typing.SupportsInt) -> None\n" + == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> None\n" ) def test_signatures(): assert ( m.kw_only_all.__doc__ - == "kw_only_all(*, i: typing.SupportsInt, j: typing.SupportsInt) -> tuple\n" + == "kw_only_all(*, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int]\n" ) assert ( m.kw_only_mixed.__doc__ - == "kw_only_mixed(i: typing.SupportsInt, *, j: typing.SupportsInt) -> tuple\n" + == "kw_only_mixed(i: typing.SupportsInt | typing.SupportsIndex, *, j: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int]\n" ) assert ( m.pos_only_all.__doc__ - == "pos_only_all(i: typing.SupportsInt, j: typing.SupportsInt, /) -> tuple\n" + == "pos_only_all(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex, /) -> tuple[int, int]\n" ) assert ( m.pos_only_mix.__doc__ - == "pos_only_mix(i: typing.SupportsInt, /, j: typing.SupportsInt) -> tuple\n" + == "pos_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int]\n" ) assert ( m.pos_kw_only_mix.__doc__ - == "pos_kw_only_mix(i: typing.SupportsInt, /, j: typing.SupportsInt, *, k: typing.SupportsInt) -> tuple\n" + == "pos_kw_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex, *, k: typing.SupportsInt | typing.SupportsIndex) -> tuple[int, int, int]\n" ) @@ -457,7 +458,8 @@ def test_args_refcount(): assert refcount(myval) == expected exp3 = refcount(myval, myval, myval) - assert m.args_refcount(myval, myval, myval) == (exp3, exp3, exp3) + # if we have to create a new tuple internally, then it will hold an extra reference for each item in it. + assert m.args_refcount(myval, myval, myval) == (exp3 + 3, exp3 + 3, exp3 + 3) assert refcount(myval) == expected # This function takes the first arg as a `py::object` and the rest as a `py::args`. Unlike the diff --git a/pybind11/tests/test_local_bindings.cpp b/pybind11/tests/test_local_bindings.cpp index 1373677..5118b25 100644 --- a/pybind11/tests/test_local_bindings.cpp +++ b/pybind11/tests/test_local_bindings.cpp @@ -21,6 +21,31 @@ TEST_SUBMODULE(local_bindings, m) { // test_load_external m.def("load_external1", [](ExternalType1 &e) { return e.i; }); m.def("load_external2", [](ExternalType2 &e) { return e.i; }); + m.def("load_external3", [](ExternalType3 &e) { return e.i; }); + + struct SharedKeepAlive { + std::shared_ptr contents; + int value() const { return contents ? *contents : -20251012; } + long use_count() const { return contents.use_count(); } + }; + py::class_(m, "SharedKeepAlive") + .def_property_readonly("value", &SharedKeepAlive::value) + .def_property_readonly("use_count", &SharedKeepAlive::use_count); + m.def("load_external2_shared", [](const std::shared_ptr &p) { + return SharedKeepAlive{std::shared_ptr(p, &p->i)}; + }); + m.def("load_external3_shared", [](const std::shared_ptr &p) { + return SharedKeepAlive{std::shared_ptr(p, &p->i)}; + }); + m.def("load_external1_unique", [](std::unique_ptr p) { return p->i; }); + m.def("load_external3_unique", [](std::unique_ptr p) { return p->i; }); + + // Aspects of set_foreign_holder that are not covered: + // - loading a foreign instance into a custom holder should fail + // - we're only covering the case where the local module doesn't know + // about the type; the paths where it does (e.g., if both global and + // foreign-module-local bindings exist for the same type) should work + // the same way (they use the same code so they very likely do) // test_local_bindings // Register a class with py::module_local: diff --git a/pybind11/tests/test_local_bindings.py b/pybind11/tests/test_local_bindings.py index 57552f7..cac89d0 100644 --- a/pybind11/tests/test_local_bindings.py +++ b/pybind11/tests/test_local_bindings.py @@ -1,5 +1,8 @@ from __future__ import annotations +import sys +from contextlib import suppress + import pytest from pybind11_tests import local_bindings as m @@ -11,6 +14,7 @@ def test_load_external(): assert m.load_external1(cm.ExternalType1(11)) == 11 assert m.load_external2(cm.ExternalType2(22)) == 22 + assert m.load_external3(cm.ExternalType3(33)) == 33 with pytest.raises(TypeError) as excinfo: assert m.load_external2(cm.ExternalType1(21)) == 21 @@ -20,6 +24,36 @@ def test_load_external(): assert m.load_external1(cm.ExternalType2(12)) == 12 assert "incompatible function arguments" in str(excinfo.value) + def test_shared(val, ctor, loader): + obj = ctor(val) + with suppress(AttributeError): # non-cpython VMs don't have getrefcount + rc_before = sys.getrefcount(obj) + wrapper = loader(obj) + # wrapper holds a shared_ptr that keeps obj alive + assert wrapper.use_count == 1 + assert wrapper.value == val + with suppress(AttributeError): + rc_after = sys.getrefcount(obj) + assert rc_after > rc_before + + test_shared(220, cm.ExternalType2, m.load_external2_shared) + test_shared(330, cm.ExternalType3, m.load_external3_shared) + + with pytest.raises(TypeError, match="incompatible function arguments"): + test_shared(320, cm.ExternalType2, m.load_external3_shared) + with pytest.raises(TypeError, match="incompatible function arguments"): + test_shared(230, cm.ExternalType3, m.load_external2_shared) + + with pytest.raises( + RuntimeError, match="Foreign instance cannot be converted to std::unique_ptr" + ): + m.load_external1_unique(cm.ExternalType1(2200)) + + with pytest.raises( + RuntimeError, match="Foreign instance cannot be converted to std::unique_ptr" + ): + m.load_external3_unique(cm.ExternalType3(3300)) + def test_local_bindings(): """Tests that duplicate `py::module_local` class bindings work across modules""" diff --git a/pybind11/tests/test_methods_and_attributes.cpp b/pybind11/tests/test_methods_and_attributes.cpp index e324c8b..86b7493 100644 --- a/pybind11/tests/test_methods_and_attributes.cpp +++ b/pybind11/tests/test_methods_and_attributes.cpp @@ -11,6 +11,12 @@ #include "constructor_stats.h" #include "pybind11_tests.h" +#if !defined(PYPY_VERSION) +// Flag set by the capsule destructor in test_dynamic_attr_dealloc_frees_dict_contents. +// File scope so the captureless capsule destructor (void(*)(void*)) can access it. +static bool s_dynamic_attr_capsule_freed = false; +#endif + #if !defined(PYBIND11_OVERLOAD_CAST) template using overload_cast_ = pybind11::detail::overload_cast_impl; @@ -161,6 +167,123 @@ class RegisteredDerived : public UnregisteredBase { double sum() const { return rw_value + ro_value; } }; +// Issue #2234: noexcept methods in an unregistered base should be bindable on the derived class. +// In C++17, noexcept is part of the function type, so &Derived::method resolves to +// a Base member function pointer with noexcept, requiring explicit template specializations. +class NoexceptUnregisteredBase { +public: + // Exercises cpp_function(Return (Class::*)(Args...) const noexcept, ...) + int value() const noexcept { return m_value; } + // Exercises cpp_function(Return (Class::*)(Args...) noexcept, ...) + void set_value(int v) noexcept { m_value = v; } + // Exercises cpp_function(Return (Class::*)(Args...) & noexcept, ...) + void increment() & noexcept { ++m_value; } + // Exercises cpp_function(Return (Class::*)(Args...) const & noexcept, ...) + int capped_value() const & noexcept { return m_value < 100 ? m_value : 100; } + +private: + int m_value = 99; +}; +class NoexceptDerived : public NoexceptUnregisteredBase { +public: + using NoexceptUnregisteredBase::NoexceptUnregisteredBase; +}; + +// Exercises cpp_function(Return (Class::*)(Args...) &&, ...) and +// cpp_function(Return (Class::*)(Args...) const &&, ...) via an unregistered base. +class RValueRefUnregisteredBase { +public: + // Exercises cpp_function(Return (Class::*)(Args...) &&, ...). + // Moves m_payload to verify that std::move(*c).*f is used in the lambda body. + std::string take() && { // NOLINT(readability-make-member-function-const) + return std::move(m_payload); + } + // Exercises cpp_function(Return (Class::*)(Args...) const &&, ...) + int peek() const && { return m_value; } +#ifdef __cpp_noexcept_function_type + // Exercises cpp_function(Return (Class::*)(Args...) && noexcept, ...) + std::string take_noexcept() && noexcept { // NOLINT(readability-make-member-function-const) + return std::move(m_payload); + } + // Exercises cpp_function(Return (Class::*)(Args...) const && noexcept, ...) + int peek_noexcept() const && noexcept { return m_value; } +#endif + +private: + int m_value = 77; + std::string m_payload{"rref_payload"}; +}; +class RValueRefDerived : public RValueRefUnregisteredBase { +public: + using RValueRefUnregisteredBase::RValueRefUnregisteredBase; +}; + +// Exercises overload_cast with noexcept member function pointers (issue #2234). +// In C++17, overload_cast must have noexcept variants to resolve noexcept overloads. +struct NoexceptOverloaded { + py::str method(int) noexcept { return "(int)"; } + py::str method(int) const noexcept { return "(int) const"; } + py::str method(float) noexcept { return "(float)"; } +}; +// Exercises overload_cast with noexcept free function pointers. +int noexcept_free_func(int x) noexcept { return x + 1; } +int noexcept_free_func(float x) noexcept { return static_cast(x) + 2; } + +// Exercises overload_cast with ref-qualified member function pointers. +struct RefQualifiedOverloaded { + py::str method(int) & { return "(int) &"; } + py::str method(int) const & { return "(int) const &"; } + py::str method(float) && { return "(float) &&"; } + py::str method(float) const && { return "(float) const &&"; } +#ifdef __cpp_noexcept_function_type + py::str method(long) & noexcept { return "(long) & noexcept"; } + py::str method(long) const & noexcept { return "(long) const & noexcept"; } + py::str method(double) && noexcept { return "(double) && noexcept"; } + py::str method(double) const && noexcept { return "(double) const && noexcept"; } +#endif +}; + +// Compile-only guard: catch overload_cast resolution regressions/ambiguities early. +using RefQualifiedOverloadedIntCast = py::detail::overload_cast_impl; +using RefQualifiedOverloadedFloatCast = py::detail::overload_cast_impl; +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); + +#ifdef __cpp_noexcept_function_type +using RefQualifiedOverloadedLongCast = py::detail::overload_cast_impl; +using RefQualifiedOverloadedDoubleCast = py::detail::overload_cast_impl; +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); +#endif + // Test explicit lvalue ref-qualification struct RefQualified { int value = 0; @@ -388,6 +511,24 @@ TEST_SUBMODULE(methods_and_attributes, m) { class CppDerivedDynamicClass : public DynamicClass {}; py::class_(m, "CppDerivedDynamicClass").def(py::init()); + + // test_dynamic_attr_dealloc_frees_dict_contents + // Regression test: pybind11_object_dealloc() must call PyObject_ClearManagedDict() + // before tp_free() so that objects stored in a py::dynamic_attr() instance __dict__ + // have their refcounts decremented when the pybind11 object is freed. On Python 3.14+ + // tp_free no longer implicitly clears the managed dict, causing permanent leaks. + m.def("make_dynamic_attr_with_capsule", []() -> py::object { + s_dynamic_attr_capsule_freed = false; + auto *dummy = new int(0); + py::capsule cap(dummy, [](void *ptr) { + delete static_cast(ptr); + s_dynamic_attr_capsule_freed = true; + }); + py::object obj = py::cast(new DynamicClass(), py::return_value_policy::take_ownership); + obj.attr("data") = cap; + return obj; + }); + m.def("is_dynamic_attr_capsule_freed", []() { return s_dynamic_attr_capsule_freed; }); #endif // test_bad_arg_default @@ -474,6 +615,161 @@ TEST_SUBMODULE(methods_and_attributes, m) { = decltype(py::method_adaptor(&RegisteredDerived::do_nothing)); static_assert(std::is_same::value, ""); + // test_noexcept_base (issue #2234) + // In C++17, noexcept is part of the function type. Binding a noexcept method from an + // unregistered base class must resolve `self` to the derived type, not the base type. + py::class_(m, "NoexceptDerived") + .def(py::init<>()) + // cpp_function(Return (Class::*)(Args...) const noexcept, ...) + .def("value", &NoexceptDerived::value) + // cpp_function(Return (Class::*)(Args...) noexcept, ...) + .def("set_value", &NoexceptDerived::set_value) + // cpp_function(Return (Class::*)(Args...) & noexcept, ...) + .def("increment", &NoexceptDerived::increment) + // cpp_function(Return (Class::*)(Args...) const & noexcept, ...) + .def("capped_value", &NoexceptDerived::capped_value); + + // test_rvalue_ref_qualified_methods: rvalue-ref-qualified methods from an unregistered base. + // method_adaptor must rebind &&/const&& member pointers to the derived type. + py::class_(m, "RValueRefDerived") + .def(py::init<>()) + // cpp_function(Return (Class::*)(Args...) &&, ...) + .def("take", &RValueRefDerived::take) + // cpp_function(Return (Class::*)(Args...) const &&, ...) + .def("peek", &RValueRefDerived::peek) +#ifdef __cpp_noexcept_function_type + // cpp_function(Return (Class::*)(Args...) && noexcept, ...) + .def("take_noexcept", &RValueRefDerived::take_noexcept) + // cpp_function(Return (Class::*)(Args...) const && noexcept, ...) + .def("peek_noexcept", &RValueRefDerived::peek_noexcept) +#endif + ; + + // Verify that &&-qualified methods cannot be called on lvalues, only on rvalues. + // This confirms that the cpp_function lambda must use std::move(*c).*f, not c->*f. +#if __cplusplus >= 201703L + static_assert(!std::is_invocable::value, + "&&-qualified method must not be callable on lvalue"); + static_assert(std::is_invocable::value, + "&&-qualified method must be callable on rvalue"); +#endif + + // Verify method_adaptor preserves &&/const&& qualifiers when rebinding. + using AdaptedRRef = decltype(py::method_adaptor(&RValueRefDerived::take)); + static_assert(std::is_same::value, ""); + using AdaptedConstRRef + = decltype(py::method_adaptor(&RValueRefDerived::peek)); + static_assert(std::is_same::value, ""); + +#ifdef __cpp_noexcept_function_type + // method_adaptor must also handle noexcept member function pointers (issue #2234). + // Verify the noexcept specifier is preserved in the resulting Derived pointer type. + using AdaptedConstNoexcept + = decltype(py::method_adaptor(&NoexceptDerived::value)); + static_assert( + std::is_same::value, ""); + using AdaptedNoexcept + = decltype(py::method_adaptor(&NoexceptDerived::set_value)); + static_assert(std::is_same::value, + ""); + using AdaptedRRefNoexcept + = decltype(py::method_adaptor(&RValueRefDerived::take_noexcept)); + static_assert(std::is_same < AdaptedRRefNoexcept, + std::string (RValueRefDerived::*)() && noexcept > ::value, + ""); + using AdaptedConstRRefNoexcept + = decltype(py::method_adaptor(&RValueRefDerived::peek_noexcept)); + static_assert(std::is_same < AdaptedConstRRefNoexcept, + int (RValueRefDerived::*)() const && noexcept > ::value, + ""); +#endif + + // test_noexcept_overload_cast (issue #2234) + // overload_cast must have noexcept operator() overloads so it can resolve noexcept methods. +#ifdef PYBIND11_OVERLOAD_CAST + py::class_(m, "NoexceptOverloaded") + .def(py::init<>()) + // overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) + .def("method", py::overload_cast(&NoexceptOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type) + .def("method_const", py::overload_cast(&NoexceptOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float + .def("method_float", py::overload_cast(&NoexceptOverloaded::method)); + // overload_cast_impl::operator()(Return (*)(Args...) noexcept) + m.def("noexcept_free_func", py::overload_cast(noexcept_free_func)); + m.def("noexcept_free_func_float", py::overload_cast(noexcept_free_func)); + + py::class_(m, "RefQualifiedOverloaded") + .def(py::init<>()) + // overload_cast_impl::operator()(Return (Class::*)(Args...) &, false_type) + .def("method_lref", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const &, true_type) + .def("method_const_lref", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) &&, false_type) + .def("method_rref", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const &&, true_type) + .def("method_const_rref", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) +# ifdef __cpp_noexcept_function_type + // overload_cast_impl::operator()(Return (Class::*)(Args...) & noexcept, false_type) + .def("method_lref_noexcept", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const & noexcept, true_type) + .def("method_const_lref_noexcept", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) && noexcept, false_type) + .def("method_rref_noexcept", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const && noexcept, true_type) + .def("method_const_rref_noexcept", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) +# endif + ; +#else + // Fallback using explicit static_cast for C++11/14 + py::class_(m, "NoexceptOverloaded") + .def(py::init<>()) + .def("method", + static_cast(&NoexceptOverloaded::method)) + .def("method_const", + static_cast(&NoexceptOverloaded::method)) + .def("method_float", + static_cast(&NoexceptOverloaded::method)); + m.def("noexcept_free_func", static_cast(noexcept_free_func)); + m.def("noexcept_free_func_float", static_cast(noexcept_free_func)); + + py::class_(m, "RefQualifiedOverloaded") + .def(py::init<>()) + .def("method_lref", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_const_lref", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_rref", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_const_rref", + static_cast( + &RefQualifiedOverloaded::method)) +# ifdef __cpp_noexcept_function_type + .def("method_lref_noexcept", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_const_lref_noexcept", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_rref_noexcept", + static_cast < py::str (RefQualifiedOverloaded::*)(double) + && noexcept > (&RefQualifiedOverloaded::method)) + .def("method_const_rref_noexcept", + static_cast < py::str (RefQualifiedOverloaded::*)(double) const && noexcept + > (&RefQualifiedOverloaded::method)) +# endif + ; +#endif + // test_methods_and_attributes py::class_(m, "RefQualified") .def(py::init<>()) diff --git a/pybind11/tests/test_methods_and_attributes.py b/pybind11/tests/test_methods_and_attributes.py index 6a8d993..c094374 100644 --- a/pybind11/tests/test_methods_and_attributes.py +++ b/pybind11/tests/test_methods_and_attributes.py @@ -5,7 +5,7 @@ import pytest import env -from pybind11_tests import ConstructorStats +from pybind11_tests import ConstructorStats, defined___cpp_noexcept_function_type from pybind11_tests import methods_and_attributes as m NO_GETTER_MSG = ( @@ -251,7 +251,7 @@ def test_no_mixed_overloads(): "#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for more details" if not detailed_error_messages_enabled else "error while attempting to bind static method ExampleMandA.overload_mixed1" - "(arg0: typing.SupportsFloat) -> str" + "(arg0: typing.SupportsFloat | typing.SupportsIndex) -> str" ) ) @@ -264,7 +264,7 @@ def test_no_mixed_overloads(): "#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for more details" if not detailed_error_messages_enabled else "error while attempting to bind instance method ExampleMandA.overload_mixed2" - "(self: pybind11_tests.methods_and_attributes.ExampleMandA, arg0: typing.SupportsInt, arg1: typing.SupportsInt)" + "(self: pybind11_tests.methods_and_attributes.ExampleMandA, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex)" " -> str" ) ) @@ -383,6 +383,23 @@ def test_cyclic_gc(): assert cstats.alive() == 0 +@pytest.mark.xfail("env.PYPY", strict=False) +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_dynamic_attr_dealloc_frees_dict_contents(): + """Regression: py::dynamic_attr() objects must free __dict__ contents on dealloc. + + pybind11_object_dealloc() did not call PyObject_ClearManagedDict() before tp_free(), + causing objects stored in __dict__ to have their refcounts permanently abandoned on + Python 3.14+ (where tp_free no longer implicitly clears the managed dict). + This caused capsule destructors to never run, leaking the underlying C++ data. + """ + instance = m.make_dynamic_attr_with_capsule() + assert not m.is_dynamic_attr_capsule_freed() + del instance + pytest.gc_collect() + assert m.is_dynamic_attr_capsule_freed() + + def test_bad_arg_default(msg): from pybind11_tests import detailed_error_messages_enabled @@ -491,7 +508,7 @@ def test_str_issue(msg): msg(excinfo.value) == """ __init__(): incompatible constructor arguments. The following argument types are supported: - 1. m.methods_and_attributes.StrIssue(arg0: typing.SupportsInt) + 1. m.methods_and_attributes.StrIssue(arg0: typing.SupportsInt | typing.SupportsIndex) 2. m.methods_and_attributes.StrIssue() Invoked with: 'no', 'such', 'constructor' @@ -517,6 +534,130 @@ def test_unregistered_base_implementations(): assert a.ro_value_prop == 1.75 +def test_noexcept_base(): + """Test issue #2234: binding noexcept methods inherited from an unregistered base class. + + In C++17 noexcept is part of the function type, so &Derived::noexcept_method resolves + to a Base member-function pointer with noexcept specifier. pybind11 must use the Derived + type as `self`, not the Base type, otherwise the call raises TypeError at runtime. + + Covers all four new cpp_function constructor specialisations: + - Return (Class::*)(Args...) noexcept (set_value) + - Return (Class::*)(Args...) const noexcept (value) + - Return (Class::*)(Args...) & noexcept (increment) + - Return (Class::*)(Args...) const & noexcept (capped_value) + """ + obj = m.NoexceptDerived() + # const noexcept + assert obj.value() == 99 + # noexcept (non-const) + obj.set_value(7) + assert obj.value() == 7 + # & noexcept (non-const lvalue ref-qualified) + obj.increment() + assert obj.value() == 8 + # const & noexcept (const lvalue ref-qualified) + assert obj.capped_value() == 8 + obj.set_value(200) + assert obj.capped_value() == 100 # capped at 100 + + +def test_rvalue_ref_qualified_methods(): + """Test that rvalue-ref-qualified (&&/const&&) methods from an unregistered base bind + correctly with `self` resolved to the derived type. + + take() moves m_payload out on each call, so the second call returns "". + This confirms that the cpp_function lambda uses std::move(*c).*f rather than c->*f. + + Covers: + - Return (Class::*)(Args...) && (take) + - Return (Class::*)(Args...) const && (peek) + """ + obj = m.RValueRefDerived() + # && moves m_payload: first call gets the value, second gets empty string + assert obj.take() == "rref_payload" + assert obj.take() == "" + # const && doesn't move: peek() is stable across calls + assert obj.peek() == 77 + assert obj.peek() == 77 + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_noexcept_rvalue_ref_qualified_methods(): + """Test noexcept rvalue-ref-qualified methods from an unregistered base. + + Covers: + - Return (Class::*)(Args...) && noexcept (take_noexcept) + - Return (Class::*)(Args...) const && noexcept (peek_noexcept) + """ + obj = m.RValueRefDerived() + assert obj.take_noexcept() == "rref_payload" + assert obj.take_noexcept() == "" + assert obj.peek_noexcept() == 77 + assert obj.peek_noexcept() == 77 + + +def test_noexcept_overload_cast(): + """Test issue #2234: overload_cast must handle noexcept member and free function pointers. + + In C++17 noexcept is part of the function type, so overload_cast_impl needs dedicated + operator() overloads for noexcept free functions and non-const/const member functions. + """ + obj = m.NoexceptOverloaded() + # overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) + assert obj.method(1) == "(int)" + # overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type) + assert obj.method_const(2) == "(int) const" + # overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float + assert obj.method_float(3.0) == "(float)" + # overload_cast_impl::operator()(Return (*)(Args...) noexcept) + assert m.noexcept_free_func(10) == 11 + assert m.noexcept_free_func_float(10.0) == 12 + + +def test_ref_qualified_overload_cast(): + """Test issue #2234 follow-up: overload_cast with ref-qualified member pointers. + + Covers: + - overload_cast_impl::operator()(Return (Class::*)(Args...) &, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const &, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) &&, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const &&, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) & noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const & noexcept, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) && noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const && noexcept, true_type) + """ + obj = m.RefQualifiedOverloaded() + assert obj.method_lref(1) == "(int) &" + assert obj.method_const_lref(1) == "(int) const &" + assert obj.method_rref(1.0) == "(float) &&" + assert obj.method_const_rref(1.0) == "(float) const &&" + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_noexcept_ref_qualified_overload_cast(): + """Test issue #2234 follow-up: overload_cast with noexcept ref-qualified member pointers. + + Covers: + - overload_cast_impl::operator()(Return (Class::*)(Args...) & noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const & noexcept, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) && noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const && noexcept, true_type) + """ + obj = m.RefQualifiedOverloaded() + assert obj.method_lref_noexcept(1) == "(long) & noexcept" + assert obj.method_const_lref_noexcept(1) == "(long) const & noexcept" + assert obj.method_rref_noexcept(1.0) == "(double) && noexcept" + assert obj.method_const_rref_noexcept(1.0) == "(double) const && noexcept" + + def test_ref_qualified(): """Tests that explicit lvalue ref-qualified methods can be called just like their non ref-qualified counterparts.""" @@ -534,21 +675,27 @@ def test_overload_ordering(): assert m.overload_order(0) == 4 assert ( - "1. overload_order(arg0: typing.SupportsInt) -> int" in m.overload_order.__doc__ + "1. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + in m.overload_order.__doc__ ) assert "2. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert "3. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert ( - "4. overload_order(arg0: typing.SupportsInt) -> int" in m.overload_order.__doc__ + "4. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + in m.overload_order.__doc__ ) with pytest.raises(TypeError) as err: m.overload_order(1.1) - assert "1. (arg0: typing.SupportsInt) -> int" in str(err.value) + assert "1. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( + err.value + ) assert "2. (arg0: str) -> int" in str(err.value) assert "3. (arg0: str) -> int" in str(err.value) - assert "4. (arg0: typing.SupportsInt) -> int" in str(err.value) + assert "4. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( + err.value + ) def test_rvalue_ref_param(): diff --git a/pybind11/tests/test_multiple_interpreters.py b/pybind11/tests/test_multiple_interpreters.py index 627ccc5..65b2733 100644 --- a/pybind11/tests/test_multiple_interpreters.py +++ b/pybind11/tests/test_multiple_interpreters.py @@ -4,9 +4,16 @@ import os import pickle import sys +import textwrap import pytest +import env +import pybind11_tests + +if env.IOS: + pytest.skip("Subinterpreters not supported on iOS", allow_module_level=True) + # 3.14.0b3+, though sys.implementation.supports_isolated_interpreters is being added in b4 # Can be simplified when we drop support for the first three betas CONCURRENT_INTERPRETERS_SUPPORT = ( @@ -82,21 +89,23 @@ def run_string( def test_independent_subinterpreters(): """Makes sure the internals object differs across independent subinterpreters""" - sys.path.append(".") + sys.path.insert(0, os.path.dirname(pybind11_tests.__file__)) run_string, create = get_interpreters(modern=True) - m = pytest.importorskip("mod_per_interpreter_gil") + import mod_per_interpreter_gil as m if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT: pytest.skip("Does not have subinterpreter support compiled in") - code = """ -import mod_per_interpreter_gil as m -import pickle -with open(pipeo, 'wb') as f: - pickle.dump(m.internals_at(), f) -""" + code = textwrap.dedent( + """ + import mod_per_interpreter_gil as m + import pickle + with open(pipeo, 'wb') as f: + pickle.dump(m.internals_at(), f) + """ + ).strip() with create() as interp1, create() as interp2: try: @@ -131,20 +140,22 @@ def test_independent_subinterpreters(): def test_independent_subinterpreters_modern(): """Makes sure the internals object differs across independent subinterpreters. Modern (3.14+) syntax.""" - sys.path.append(".") + sys.path.insert(0, os.path.dirname(pybind11_tests.__file__)) - m = pytest.importorskip("mod_per_interpreter_gil") + import mod_per_interpreter_gil as m if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT: pytest.skip("Does not have subinterpreter support compiled in") from concurrent import interpreters - code = """ -import mod_per_interpreter_gil as m + code = textwrap.dedent( + """ + import mod_per_interpreter_gil as m -values.put_nowait(m.internals_at()) -""" + values.put_nowait(m.internals_at()) + """ + ).strip() with contextlib.closing(interpreters.create()) as interp1, contextlib.closing( interpreters.create() @@ -175,21 +186,23 @@ def test_independent_subinterpreters_modern(): def test_dependent_subinterpreters(): """Makes sure the internals object differs across subinterpreters""" - sys.path.append(".") + sys.path.insert(0, os.path.dirname(pybind11_tests.__file__)) run_string, create = get_interpreters(modern=False) - m = pytest.importorskip("mod_shared_interpreter_gil") + import mod_shared_interpreter_gil as m if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT: pytest.skip("Does not have subinterpreter support compiled in") - code = """ -import mod_shared_interpreter_gil as m -import pickle -with open(pipeo, 'wb') as f: - pickle.dump(m.internals_at(), f) -""" + code = textwrap.dedent( + """ + import mod_shared_interpreter_gil as m + import pickle + with open(pipeo, 'wb') as f: + pickle.dump(m.internals_at(), f) + """ + ).strip() with create("legacy") as interp1: pipei, pipeo = os.pipe() @@ -198,3 +211,227 @@ def test_dependent_subinterpreters(): res1 = pickle.load(f) assert res1 != m.internals_at(), "internals should differ from main interpreter" + + +PREAMBLE_CODE = textwrap.dedent( + f""" + def test(): + import sys + + sys.path.insert(0, {os.path.dirname(env.__file__)!r}) + sys.path.insert(0, {os.path.dirname(pybind11_tests.__file__)!r}) + + import collections + import mod_per_interpreter_gil_with_singleton as m + + objects = m.get_objects_in_singleton() + expected = [ + type(None), # static type: shared between interpreters + tuple, # static type: shared between interpreters + list, # static type: shared between interpreters + dict, # static type: shared between interpreters + collections.OrderedDict, # static type: shared between interpreters + collections.defaultdict, # heap type: dynamically created per interpreter + collections.deque, # heap type: dynamically created per interpreter + ] + # Check that we have the expected objects. Avoid IndexError by checking lengths first. + assert len(objects) == len(expected), ( + f"Expected {{expected!r}} ({{len(expected)}}), got {{objects!r}} ({{len(objects)}})." + ) + # The first ones are static types shared between interpreters. + assert objects[:-2] == expected[:-2], ( + f"Expected static objects {{expected[:-2]!r}}, got {{objects[:-2]!r}}." + ) + # The last two are heap types created per-interpreter. + # The expected objects are dynamically imported from `collections`. + assert objects[-2:] == expected[-2:], ( + f"Expected heap objects {{expected[-2:]!r}}, got {{objects[-2:]!r}}." + ) + + assert hasattr(m, 'MyClass'), "Module missing MyClass" + assert hasattr(m, 'MyGlobalError'), "Module missing MyGlobalError" + assert hasattr(m, 'MyLocalError'), "Module missing MyLocalError" + assert hasattr(m, 'MyEnum'), "Module missing MyEnum" + """ +).lstrip() + + +@pytest.mark.skipif( + sys.platform.startswith("emscripten"), reason="Requires loadable modules" +) +@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+") +def test_import_module_with_singleton_per_interpreter(): + """Tests that a singleton storing Python objects works correctly per-interpreter""" + from concurrent import interpreters + + code = f"{PREAMBLE_CODE.strip()}\n\ntest()\n" + with contextlib.closing(interpreters.create()) as interp: + interp.exec(code) + + +@pytest.mark.skipif( + sys.platform.startswith("emscripten"), reason="Requires loadable modules" +) +@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+") +def test_import_in_subinterpreter_after_main(): + """Tests that importing a module in a subinterpreter after the main interpreter works correctly""" + env.check_script_success_in_subprocess( + PREAMBLE_CODE + + textwrap.dedent( + """ + import contextlib + import gc + from concurrent import interpreters + + test() + + interp = None + with contextlib.closing(interpreters.create()) as interp: + interp.call(test) + + del interp + for _ in range(5): + gc.collect() + """ + ) + ) + + env.check_script_success_in_subprocess( + PREAMBLE_CODE + + textwrap.dedent( + """ + import contextlib + import gc + import random + from concurrent import interpreters + + test() + + interps = interp = None + with contextlib.ExitStack() as stack: + interps = [ + stack.enter_context(contextlib.closing(interpreters.create())) + for _ in range(8) + ] + random.shuffle(interps) + for interp in interps: + interp.call(test) + + del interps, interp, stack + for _ in range(5): + gc.collect() + """ + ) + ) + + +@pytest.mark.skipif( + sys.platform.startswith("emscripten"), reason="Requires loadable modules" +) +@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+") +def test_import_in_subinterpreter_before_main(): + """Tests that importing a module in a subinterpreter before the main interpreter works correctly""" + env.check_script_success_in_subprocess( + PREAMBLE_CODE + + textwrap.dedent( + """ + import contextlib + import gc + from concurrent import interpreters + + interp = None + with contextlib.closing(interpreters.create()) as interp: + interp.call(test) + + test() + + del interp + for _ in range(5): + gc.collect() + """ + ) + ) + + env.check_script_success_in_subprocess( + PREAMBLE_CODE + + textwrap.dedent( + """ + import contextlib + import gc + from concurrent import interpreters + + interps = interp = None + with contextlib.ExitStack() as stack: + interps = [ + stack.enter_context(contextlib.closing(interpreters.create())) + for _ in range(8) + ] + for interp in interps: + interp.call(test) + + test() + + del interps, interp, stack + for _ in range(5): + gc.collect() + """ + ) + ) + + env.check_script_success_in_subprocess( + PREAMBLE_CODE + + textwrap.dedent( + """ + import contextlib + import gc + from concurrent import interpreters + + interps = interp = None + with contextlib.ExitStack() as stack: + interps = [ + stack.enter_context(contextlib.closing(interpreters.create())) + for _ in range(8) + ] + for interp in interps: + interp.call(test) + + test() + + del interps, interp, stack + for _ in range(5): + gc.collect() + """ + ) + ) + + +@pytest.mark.skipif( + sys.platform.startswith("emscripten"), reason="Requires loadable modules" +) +@pytest.mark.xfail( + env.MUSLLINUX, + reason="Flaky on musllinux, see also: https://github.com/pybind/pybind11/pull/5972#discussion_r2755283335", + strict=False, +) +@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+") +def test_import_in_subinterpreter_concurrently(): + """Tests that importing a module in multiple subinterpreters concurrently works correctly""" + env.check_script_success_in_subprocess( + PREAMBLE_CODE + + textwrap.dedent( + """ + import gc + from concurrent.futures import InterpreterPoolExecutor, as_completed + + futures = future = None + with InterpreterPoolExecutor(max_workers=16) as executor: + futures = [executor.submit(test) for _ in range(32)] + for future in as_completed(futures): + future.result() + del futures, future, executor + + for _ in range(5): + gc.collect() + """ + ) + ) diff --git a/pybind11/tests/test_native_enum.cpp b/pybind11/tests/test_native_enum.cpp index 0ec6d85..816ace0 100644 --- a/pybind11/tests/test_native_enum.cpp +++ b/pybind11/tests/test_native_enum.cpp @@ -93,10 +93,14 @@ TEST_SUBMODULE(native_enum, m) { .value("blue", color::blue) .finalize(); - py::native_enum(m, "altitude", "enum.Enum") - .value("high", altitude::high) - .value("low", altitude::low) - .finalize(); + m.def("bind_altitude", [](const py::module_ &mod) { + py::native_enum(mod, "altitude", "enum.Enum") + .value("high", altitude::high) + .value("low", altitude::low) + .finalize(); + }); + m.def("is_high_altitude", [](altitude alt) { return alt == altitude::high; }); + m.def("get_altitude", []() -> altitude { return altitude::high; }); py::native_enum(m, "flags_uchar", "enum.Flag") .value("bit0", flags_uchar::bit0) diff --git a/pybind11/tests/test_native_enum.py b/pybind11/tests/test_native_enum.py index b039747..4262200 100644 --- a/pybind11/tests/test_native_enum.py +++ b/pybind11/tests/test_native_enum.py @@ -59,7 +59,6 @@ ENUM_TYPES_AND_MEMBERS = ( (m.smallenum, SMALLENUM_MEMBERS), (m.color, COLOR_MEMBERS), - (m.altitude, ALTITUDE_MEMBERS), (m.flags_uchar, FLAGS_UCHAR_MEMBERS), (m.flags_uint, FLAGS_UINT_MEMBERS), (m.export_values, EXPORT_VALUES_MEMBERS), @@ -320,3 +319,59 @@ def test_native_enum_missing_finalize_failure(): if not isinstance(m.native_enum_missing_finalize_failure, str): m.native_enum_missing_finalize_failure() pytest.fail("Process termination expected.") + + +def test_unregister_native_enum_when_destroyed(): + # For stability when running tests in parallel, this test should be the + # only one that touches `m.altitude` or calls `m.bind_altitude`. + + def test_altitude_enum(): + # Logic copied from test_enum_type / test_enum_members. + # We don't test altitude there to avoid possible clashes if + # parallelizing against other tests in this file, and we also + # don't want to hold any references to the enumerators that + # would prevent GCing the enum type below. + assert isinstance(m.altitude, enum.EnumMeta) + assert m.altitude.__module__ == m.__name__ + for name, value in ALTITUDE_MEMBERS: + assert m.altitude[name].value == value + + def test_altitude_binding(): + assert m.is_high_altitude(m.altitude.high) + assert not m.is_high_altitude(m.altitude.low) + assert m.get_altitude() is m.altitude.high + with pytest.raises(TypeError, match="incompatible function arguments"): + m.is_high_altitude("oops") + + m.bind_altitude(m) + test_altitude_enum() + test_altitude_binding() + + if env.TYPES_ARE_IMMORTAL: + pytest.skip("can't GC type objects on this platform") + + # Delete the enum type. Returning an instance from Python should fail + # rather than accessing a deleted object. + pytest.delattr_and_ensure_destroyed((m, "altitude")) + with pytest.raises(TypeError, match="Unable to convert function return"): + m.get_altitude() + with pytest.raises(TypeError, match="incompatible function arguments"): + m.is_high_altitude("oops") + + # Recreate the enum type; should not have any duplicate-binding error + m.bind_altitude(m) + test_altitude_enum() + test_altitude_binding() + + # Remove the pybind11 capsule without removing the type; enum is still + # usable but can't be passed to/from bound functions + del m.altitude.__pybind11_native_enum__ + pytest.gc_collect() + test_altitude_enum() # enum itself still works + + with pytest.raises(TypeError, match="Unable to convert function return"): + m.get_altitude() + with pytest.raises(TypeError, match="incompatible function arguments"): + m.is_high_altitude(m.altitude.high) + + del m.altitude diff --git a/pybind11/tests/test_numpy_array.cpp b/pybind11/tests/test_numpy_array.cpp index 1bfca33..3bce988 100644 --- a/pybind11/tests/test_numpy_array.cpp +++ b/pybind11/tests/test_numpy_array.cpp @@ -14,11 +14,12 @@ #include #include +#include // Size / dtype checks. struct DtypeCheck { - py::dtype numpy{}; - py::dtype pybind11{}; + py::dtype numpy; + py::dtype pybind11; }; template @@ -43,11 +44,11 @@ std::vector get_concrete_dtype_checks() { } struct DtypeSizeCheck { - std::string name{}; + std::string name; int size_cpp{}; int size_numpy{}; // For debugging. - py::dtype dtype{}; + py::dtype dtype; }; template @@ -246,6 +247,22 @@ TEST_SUBMODULE(numpy_array, sm) { sm.def("nbytes", [](const arr &a) { return a.nbytes(); }); sm.def("owndata", [](const arr &a) { return a.owndata(); }); +#ifdef PYBIND11_HAS_SPAN + // test_shape_strides_span + sm.def("shape_span", [](const arr &a) { + auto span = a.shape_span(); + return std::vector(span.begin(), span.end()); + }); + sm.def("strides_span", [](const arr &a) { + auto span = a.strides_span(); + return std::vector(span.begin(), span.end()); + }); + // Test that spans can be used to construct new arrays + sm.def("array_from_spans", [](const arr &a) { + return py::array(a.dtype(), a.shape_span(), a.strides_span(), a.data(), a); + }); +#endif + // test_index_offset def_index_fn(index_at, const arr &); def_index_fn(index_at_t, const arr_t &); @@ -282,6 +299,7 @@ TEST_SUBMODULE(numpy_array, sm) { struct ArrayClass { int data[2] = {1, 2}; ArrayClass() { py::print("ArrayClass()"); } + ArrayClass(const ArrayClass &) = default; ~ArrayClass() { py::print("~ArrayClass()"); } }; py::class_(sm, "ArrayClass") diff --git a/pybind11/tests/test_numpy_array.py b/pybind11/tests/test_numpy_array.py index 19c07ca..93477aa 100644 --- a/pybind11/tests/test_numpy_array.py +++ b/pybind11/tests/test_numpy_array.py @@ -68,6 +68,45 @@ def test_array_attributes(): assert not m.owndata(a) +@pytest.mark.skipif(not hasattr(m, "shape_span"), reason="std::span not available") +def test_shape_strides_span(): + # Test 0-dimensional array (scalar) + a = np.array(42, "f8") + assert m.ndim(a) == 0 + assert m.shape_span(a) == [] + assert m.strides_span(a) == [] + + # Test 1-dimensional array + a = np.array([1, 2, 3, 4], "u2") + assert m.ndim(a) == 1 + assert m.shape_span(a) == [4] + assert m.strides_span(a) == [2] + + # Test 2-dimensional array + a = np.array([[1, 2, 3], [4, 5, 6]], "u2").view() + a.flags.writeable = False + assert m.ndim(a) == 2 + assert m.shape_span(a) == [2, 3] + assert m.strides_span(a) == [6, 2] + + # Test 3-dimensional array + a = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], "i4") + assert m.ndim(a) == 3 + assert m.shape_span(a) == [2, 2, 2] + # Verify spans match regular shape/strides + assert list(m.shape_span(a)) == list(m.shape(a)) + assert list(m.strides_span(a)) == list(m.strides(a)) + + # Test that spans can be used to construct new arrays + original = np.array([[1, 2, 3], [4, 5, 6]], "f4") + new_array = m.array_from_spans(original) + assert new_array.shape == original.shape + assert new_array.strides == original.strides + assert new_array.dtype == original.dtype + # Verify data is shared (since we pass the same data pointer) + np.testing.assert_array_equal(new_array, original) + + @pytest.mark.parametrize( ("args", "ret"), [([], 0), ([0], 0), ([1], 3), ([0, 1], 1), ([1, 2], 5)] ) diff --git a/pybind11/tests/test_numpy_dtypes.py b/pybind11/tests/test_numpy_dtypes.py index 9f85742..22814ab 100644 --- a/pybind11/tests/test_numpy_dtypes.py +++ b/pybind11/tests/test_numpy_dtypes.py @@ -367,7 +367,7 @@ def test_complex_array(): def test_signature(doc): assert ( doc(m.create_rec_nested) - == "create_rec_nested(arg0: typing.SupportsInt) -> numpy.typing.NDArray[NestedStruct]" + == "create_rec_nested(arg0: typing.SupportsInt | typing.SupportsIndex) -> numpy.typing.NDArray[NestedStruct]" ) diff --git a/pybind11/tests/test_numpy_vectorize.cpp b/pybind11/tests/test_numpy_vectorize.cpp index dcc4c6a..2334521 100644 --- a/pybind11/tests/test_numpy_vectorize.cpp +++ b/pybind11/tests/test_numpy_vectorize.cpp @@ -78,6 +78,27 @@ TEST_SUBMODULE(numpy_vectorize, m) { struct VectorizeTestClass { explicit VectorizeTestClass(int v) : value{v} {}; float method(int x, float y) const { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) &) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_lref(int x, float y) & { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const &) + float method_const_lref(int x, float y) const & { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) noexcept) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_noexcept(int x, float y) noexcept { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const noexcept) + float method_const_noexcept(int x, float y) const noexcept { + return y + (float) (x + value); + } +#ifdef __cpp_noexcept_function_type + // Exercises vectorize(Return (Class::*)(Args...) & noexcept) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_lref_noexcept(int x, float y) & noexcept { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const & noexcept) + float method_const_lref_noexcept(int x, float y) const & noexcept { + return y + (float) (x + value); + } +#endif int value = 0; }; py::class_ vtc(m, "VectorizeTestClass"); @@ -85,6 +106,15 @@ TEST_SUBMODULE(numpy_vectorize, m) { // Automatic vectorizing of methods vtc.def("method", py::vectorize(&VectorizeTestClass::method)); + vtc.def("method_lref", py::vectorize(&VectorizeTestClass::method_lref)); + vtc.def("method_const_lref", py::vectorize(&VectorizeTestClass::method_const_lref)); + vtc.def("method_noexcept", py::vectorize(&VectorizeTestClass::method_noexcept)); + vtc.def("method_const_noexcept", py::vectorize(&VectorizeTestClass::method_const_noexcept)); +#ifdef __cpp_noexcept_function_type + vtc.def("method_lref_noexcept", py::vectorize(&VectorizeTestClass::method_lref_noexcept)); + vtc.def("method_const_lref_noexcept", + py::vectorize(&VectorizeTestClass::method_const_lref_noexcept)); +#endif // test_trivial_broadcasting // Internal optimization test for whether the input is trivially broadcastable: diff --git a/pybind11/tests/test_numpy_vectorize.py b/pybind11/tests/test_numpy_vectorize.py index d405e68..030abec 100644 --- a/pybind11/tests/test_numpy_vectorize.py +++ b/pybind11/tests/test_numpy_vectorize.py @@ -2,6 +2,7 @@ import pytest +from pybind11_tests import defined___cpp_noexcept_function_type from pybind11_tests import numpy_vectorize as m np = pytest.importorskip("numpy") @@ -211,11 +212,11 @@ def test_passthrough_arguments(doc): "vec_passthrough(" + ", ".join( [ - "arg0: typing.SupportsFloat", + "arg0: typing.SupportsFloat | typing.SupportsIndex", "arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", "arg2: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", "arg3: typing.Annotated[numpy.typing.ArrayLike, numpy.int32]", - "arg4: typing.SupportsInt", + "arg4: typing.SupportsInt | typing.SupportsIndex", "arg5: m.numpy_vectorize.NonPODClass", "arg6: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", ] @@ -246,6 +247,56 @@ def test_method_vectorization(): assert np.all(o.method(x, y) == [[14, 15], [24, 25]]) +def test_ref_qualified_method_vectorization(): + """Test issue #2234 follow-up: vectorize with lvalue-ref-qualified member pointers. + + Covers: + - vectorize(Return (Class::*)(Args...) &) + - vectorize(Return (Class::*)(Args...) const &) + - vectorize(Return (Class::*)(Args...) & noexcept) + - vectorize(Return (Class::*)(Args...) const & noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + assert np.all(o.method_lref(x, y) == [[14, 15], [24, 25]]) + assert np.all(o.method_const_lref(x, y) == [[14, 15], [24, 25]]) + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_noexcept_ref_qualified_method_vectorization(): + """Test issue #2234 follow-up: vectorize with noexcept lvalue-ref-qualified member pointers. + + Covers: + - vectorize(Return (Class::*)(Args...) & noexcept) + - vectorize(Return (Class::*)(Args...) const & noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + assert np.all(o.method_lref_noexcept(x, y) == [[14, 15], [24, 25]]) + assert np.all(o.method_const_lref_noexcept(x, y) == [[14, 15], [24, 25]]) + + +def test_noexcept_method_vectorization(): + """Test issue #2234: vectorize must handle noexcept member function pointers. + + Covers both new vectorize specialisations: + - vectorize(Return (Class::*)(Args...) noexcept) + - vectorize(Return (Class::*)(Args...) const noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + # vectorize(Return (Class::*)(Args...) noexcept) + assert np.all(o.method_noexcept(x, y) == [[14, 15], [24, 25]]) + # vectorize(Return (Class::*)(Args...) const noexcept) + assert np.all(o.method_const_noexcept(x, y) == [[14, 15], [24, 25]]) + + def test_array_collapse(): assert not isinstance(m.vectorized_func(1, 2, 3), np.ndarray) assert not isinstance(m.vectorized_func(np.array(1), 2, 3), np.ndarray) diff --git a/pybind11/tests/test_potentially_slicing_weak_ptr.cpp b/pybind11/tests/test_potentially_slicing_weak_ptr.cpp index 01b147f..c1bf36f 100644 --- a/pybind11/tests/test_potentially_slicing_weak_ptr.cpp +++ b/pybind11/tests/test_potentially_slicing_weak_ptr.cpp @@ -11,7 +11,9 @@ namespace potentially_slicing_weak_ptr { template // Using int as a trick to easily generate multiple types. struct VirtBase { + VirtBase() = default; virtual ~VirtBase() = default; + VirtBase(const VirtBase &) = delete; virtual int get_code() { return 100; } }; diff --git a/pybind11/tests/test_pytorch_shared_ptr_cast_regression.cpp b/pybind11/tests/test_pytorch_shared_ptr_cast_regression.cpp new file mode 100644 index 0000000..1425962 --- /dev/null +++ b/pybind11/tests/test_pytorch_shared_ptr_cast_regression.cpp @@ -0,0 +1,62 @@ +#include "pybind11_tests.h" + +#include +#include + +#if defined(__clang__) +# if __has_warning("-Wdeprecated-copy-with-user-provided-dtor") +# pragma clang diagnostic error "-Wdeprecated-copy-with-user-provided-dtor" +# endif +# if __has_warning("-Wdeprecated-copy-with-dtor") +# pragma clang diagnostic error "-Wdeprecated-copy-with-dtor" +# endif +#endif + +namespace test_pytorch_regressions { + +// Directly extracted from PyTorch patterns that regressed in CI. +struct TracingState : std::enable_shared_from_this { + TracingState() = default; + ~TracingState() = default; + int value = 0; +}; + +const std::shared_ptr &get_tracing_state() { + static std::shared_ptr state = std::make_shared(); + return state; +} + +struct InterfaceType { + ~InterfaceType() = default; + int value = 0; +}; +using InterfaceTypePtr = std::shared_ptr; + +struct CompilationUnit { + InterfaceTypePtr iface = std::make_shared(); + + InterfaceTypePtr get_interface(const std::string &) const { return iface; } +}; + +} // namespace test_pytorch_regressions + +TEST_SUBMODULE(pybind11_pytorch_regressions, m) { + using namespace test_pytorch_regressions; + + py::class_>(m, "TracingState") + .def(py::init<>()) + .def_readwrite("value", &TracingState::value); + + m.def("_get_tracing_state", []() { return get_tracing_state(); }); + + py::class_(m, "InterfaceType") + .def(py::init<>()) + .def_readwrite("value", &InterfaceType::value); + + py::class_>(m, "CompilationUnit") + .def(py::init<>()) + .def("get_interface", + [](const std::shared_ptr &self, const std::string &name) { + return self->get_interface(name); + }); +} diff --git a/pybind11/tests/test_pytorch_shared_ptr_cast_regression.py b/pybind11/tests/test_pytorch_shared_ptr_cast_regression.py new file mode 100644 index 0000000..b7c393b --- /dev/null +++ b/pybind11/tests/test_pytorch_shared_ptr_cast_regression.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pybind11_tests import pybind11_pytorch_regressions as m + + +def test_pytorch_like_get_tracing_state_aliases_singleton_shared_ptr(): + a = m._get_tracing_state() + b = m._get_tracing_state() + + a.value = 17 + + assert b.value == 17 + assert m._get_tracing_state().value == 17 + + +def test_pytorch_like_compilation_unit_get_interface_aliases_member_shared_ptr(): + cu = m.CompilationUnit() + + a = cu.get_interface("iface") + b = cu.get_interface("iface") + + a.value = 23 + + assert b.value == 23 + assert cu.get_interface("iface").value == 23 diff --git a/pybind11/tests/test_pytypes.cpp b/pybind11/tests/test_pytypes.cpp index 1c136cf..ff77940 100644 --- a/pybind11/tests/test_pytypes.cpp +++ b/pybind11/tests/test_pytypes.cpp @@ -209,6 +209,7 @@ TEST_SUBMODULE(pytypes, m) { m.def("get_tuple_from_iterable", [](const py::iterable &iter) { return py::tuple(iter); }); // test_float m.def("get_float", [] { return py::float_(0.0f); }); + m.def("float_roundtrip", [](py::float_ f) { return f; }); // test_list m.def("list_no_args", []() { return py::list{}; }); m.def("list_ssize_t", []() { return py::list{(py::ssize_t) 0}; }); @@ -1085,8 +1086,7 @@ TEST_SUBMODULE(pytypes, m) { static_class.def(py::init()); static_class.attr_with_type_hint>("x") = 1.0; static_class.attr_with_type_hint>>( - "dict_str_int") - = py::dict(); + "dict_str_int") = py::dict(); struct Instance {}; auto instance = py::class_(m, "Instance", py::dynamic_attr()); @@ -1210,4 +1210,6 @@ TEST_SUBMODULE(pytypes, m) { m.def("check_type_is", [](const py::object &x) -> py::typing::TypeIs { return py::isinstance(x); }); + + m.def("const_kwargs_ref_to_str", [](const py::kwargs &kwargs) { return py::str(kwargs); }); } diff --git a/pybind11/tests/test_pytypes.py b/pybind11/tests/test_pytypes.py index a199d72..580371f 100644 --- a/pybind11/tests/test_pytypes.py +++ b/pybind11/tests/test_pytypes.py @@ -61,6 +61,13 @@ def test_iterable(doc): def test_float(doc): assert doc(m.get_float) == "get_float() -> float" + assert doc(m.float_roundtrip) == "float_roundtrip(arg0: float) -> float" + f1 = m.float_roundtrip(5.5) + assert isinstance(f1, float) + assert f1 == 5.5 + f2 = m.float_roundtrip(5) + assert isinstance(f2, float) + assert f2 == 5.0 def test_list(capture, doc): @@ -917,7 +924,7 @@ def test_inplace_rshift(a, b): def test_tuple_nonempty_annotations(doc): assert ( doc(m.annotate_tuple_float_str) - == "annotate_tuple_float_str(arg0: tuple[typing.SupportsFloat, str]) -> None" + == "annotate_tuple_float_str(arg0: tuple[float, str]) -> None" ) @@ -930,21 +937,21 @@ def test_tuple_empty_annotations(doc): def test_tuple_variable_length_annotations(doc): assert ( doc(m.annotate_tuple_variable_length) - == "annotate_tuple_variable_length(arg0: tuple[typing.SupportsFloat, ...]) -> None" + == "annotate_tuple_variable_length(arg0: tuple[float, ...]) -> None" ) def test_dict_annotations(doc): assert ( doc(m.annotate_dict_str_int) - == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt]) -> None" + == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt | typing.SupportsIndex]) -> None" ) def test_list_annotations(doc): assert ( doc(m.annotate_list_int) - == "annotate_list_int(arg0: list[typing.SupportsInt]) -> None" + == "annotate_list_int(arg0: list[typing.SupportsInt | typing.SupportsIndex]) -> None" ) @@ -962,7 +969,7 @@ def test_iterable_annotations(doc): def test_iterator_annotations(doc): assert ( doc(m.annotate_iterator_int) - == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt]) -> None" + == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt | typing.SupportsIndex]) -> None" ) @@ -982,14 +989,15 @@ def test_fn_return_only(doc): def test_type_annotation(doc): assert ( - doc(m.annotate_type) == "annotate_type(arg0: type[typing.SupportsInt]) -> type" + doc(m.annotate_type) + == "annotate_type(arg0: type[typing.SupportsInt | typing.SupportsIndex]) -> type" ) def test_union_annotations(doc): assert ( doc(m.annotate_union) - == "annotate_union(arg0: list[str | typing.SupportsInt | object], arg1: str, arg2: typing.SupportsInt, arg3: object) -> list[str | int | object]" + == "annotate_union(arg0: list[str | int | object], arg1: str, arg2: int, arg3: object) -> list[str | int | object]" ) @@ -1000,7 +1008,7 @@ def test_union_typing_only(doc): def test_union_object_annotations(doc): assert ( doc(m.annotate_union_to_object) - == "annotate_union_to_object(arg0: typing.SupportsInt | str) -> object" + == "annotate_union_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | str) -> object" ) @@ -1037,7 +1045,7 @@ def test_never_annotation(doc, backport_typehints): def test_optional_object_annotations(doc): assert ( doc(m.annotate_optional_to_object) - == "annotate_optional_to_object(arg0: typing.SupportsInt | None) -> object" + == "annotate_optional_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> object" ) @@ -1160,7 +1168,10 @@ def get_annotations_helper(o): def test_module_attribute_types() -> None: module_annotations = get_annotations_helper(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt]" + assert ( + module_annotations["list_int"] + == "list[typing.SupportsInt | typing.SupportsIndex]" + ) assert module_annotations["set_str"] == "set[str]" assert module_annotations["foo"] == "pybind11_tests.pytypes.foo" @@ -1183,7 +1194,10 @@ def test_get_annotations_compliance() -> None: module_annotations = get_annotations(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt]" + assert ( + module_annotations["list_int"] + == "list[typing.SupportsInt | typing.SupportsIndex]" + ) assert module_annotations["set_str"] == "set[str]" @@ -1197,10 +1211,13 @@ def test_class_attribute_types() -> None: instance_annotations = get_annotations_helper(m.Instance) assert empty_annotations is None - assert static_annotations["x"] == "typing.ClassVar[typing.SupportsFloat]" + assert ( + static_annotations["x"] + == "typing.ClassVar[typing.SupportsFloat | typing.SupportsIndex]" + ) assert ( static_annotations["dict_str_int"] - == "typing.ClassVar[dict[str, typing.SupportsInt]]" + == "typing.ClassVar[dict[str, typing.SupportsInt | typing.SupportsIndex]]" ) assert m.Static.x == 1.0 @@ -1212,7 +1229,7 @@ def test_class_attribute_types() -> None: static.dict_str_int["hi"] = 3 assert m.Static().dict_str_int == {"hi": 3} - assert instance_annotations["y"] == "typing.SupportsFloat" + assert instance_annotations["y"] == "typing.SupportsFloat | typing.SupportsIndex" instance1 = m.Instance() instance1.y = 4.0 @@ -1229,7 +1246,10 @@ def test_class_attribute_types() -> None: def test_redeclaration_attr_with_type_hint() -> None: obj = m.Instance() m.attr_with_type_hint_float_x(obj) - assert get_annotations_helper(obj)["x"] == "typing.SupportsFloat" + assert ( + get_annotations_helper(obj)["x"] + == "typing.SupportsFloat | typing.SupportsIndex" + ) with pytest.raises( RuntimeError, match=r'^__annotations__\["x"\] was set already\.$' ): @@ -1347,3 +1367,8 @@ def test_arg_return_type_hints(doc, backport_typehints): backport_typehints(doc(m.check_type_guard)) == "check_type_guard(arg0: list[object]) -> typing.TypeGuard[list[float]]" ) + + +def test_const_kwargs_ref_to_str(): + assert m.const_kwargs_ref_to_str() == "{}" + assert m.const_kwargs_ref_to_str(a=1) == "{'a': 1}" diff --git a/pybind11/tests/test_scoped_critical_section.cpp b/pybind11/tests/test_scoped_critical_section.cpp index dc9a69e..7401eb0 100644 --- a/pybind11/tests/test_scoped_critical_section.cpp +++ b/pybind11/tests/test_scoped_critical_section.cpp @@ -7,8 +7,7 @@ #include #include -#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include() -# define PYBIND11_HAS_BARRIER 1 +#if defined(PYBIND11_HAS_STD_BARRIER) # include #endif @@ -39,7 +38,7 @@ class BoolWrapper { std::atomic_bool value_{false}; }; -#if defined(PYBIND11_HAS_BARRIER) +#if defined(PYBIND11_HAS_STD_BARRIER) // Modifying the C/C++ members of a Python object from multiple threads requires a critical section // to ensure thread safety and data integrity. @@ -259,7 +258,7 @@ TEST_SUBMODULE(scoped_critical_section, m) { (void) BoolWrapperHandle.ptr(); // suppress unused variable warning m.attr("has_barrier") = -#ifdef PYBIND11_HAS_BARRIER +#ifdef PYBIND11_HAS_STD_BARRIER true; #else false; diff --git a/pybind11/tests/test_sequences_and_iterators.cpp b/pybind11/tests/test_sequences_and_iterators.cpp index ccb0d14..3f03daf 100644 --- a/pybind11/tests/test_sequences_and_iterators.cpp +++ b/pybind11/tests/test_sequences_and_iterators.cpp @@ -556,7 +556,7 @@ TEST_SUBMODULE(sequences_and_iterators, m) { }); m.def("count_nonzeros", [](const py::dict &d) { - return std::count_if(d.begin(), d.end(), [](std::pair p) { + return std::count_if(d.begin(), d.end(), [](const std::pair &p) { return p.second.cast() != 0; }); }); diff --git a/pybind11/tests/test_smart_ptr.cpp b/pybind11/tests/test_smart_ptr.cpp index 5fdd69d..3617fa3 100644 --- a/pybind11/tests/test_smart_ptr.cpp +++ b/pybind11/tests/test_smart_ptr.cpp @@ -161,6 +161,8 @@ class MyObject4a { print_created(this); pointer_set().insert(this); }; + MyObject4a(const MyObject4a &) = delete; + int value; static void cleanupAllInstances() { @@ -182,6 +184,7 @@ class MyObject4a { class MyObject4b : public MyObject4a { public: explicit MyObject4b(int i) : MyObject4a(i) { print_created(this); } + MyObject4b(const MyObject4b &) = delete; ~MyObject4b() override { print_destroyed(this); } }; @@ -189,6 +192,7 @@ class MyObject4b : public MyObject4a { class MyObject5 { // managed by huge_unique_ptr public: explicit MyObject5(int value) : value{value} { print_created(this); } + MyObject5(const MyObject5 &) = delete; ~MyObject5() { print_destroyed(this); } int value; }; @@ -216,7 +220,7 @@ struct SharedPtrRef { ~A() { print_destroyed(this); } }; - A value = {}; + A value; std::shared_ptr shared = std::make_shared(); }; @@ -224,13 +228,14 @@ struct SharedPtrRef { struct SharedFromThisRef { struct B : std::enable_shared_from_this { B() { print_created(this); } - // NOLINTNEXTLINE(bugprone-copy-constructor-init) + // NOLINTNEXTLINE(bugprone-copy-constructor-init, readability-redundant-member-init) B(const B &) : std::enable_shared_from_this() { print_copy_created(this); } + // NOLINTNEXTLINE(readability-redundant-member-init) B(B &&) noexcept : std::enable_shared_from_this() { print_move_created(this); } ~B() { print_destroyed(this); } }; - B value = {}; + B value; std::shared_ptr shared = std::make_shared(); }; @@ -242,9 +247,32 @@ struct SharedFromThisVBase : std::enable_shared_from_this { }; struct SharedFromThisVirt : virtual SharedFromThisVBase {}; +// Issue #5989: static_pointer_cast where dynamic_pointer_cast is needed +// (virtual inheritance with shared_ptr holder) +struct SftVirtBase : std::enable_shared_from_this { + SftVirtBase() = default; + virtual ~SftVirtBase() = default; + static std::shared_ptr create() { return std::make_shared(); } + virtual std::string name() { return "SftVirtBase"; } +}; +struct SftVirtDerived : SftVirtBase { + using SftVirtBase::SftVirtBase; + static std::shared_ptr create() { return std::make_shared(); } + std::string name() override { return "SftVirtDerived"; } +}; +struct SftVirtDerived2 : virtual SftVirtDerived { + using SftVirtDerived::SftVirtDerived; + static std::shared_ptr create() { + return std::make_shared(); + } + std::string name() override { return "SftVirtDerived2"; } + std::string call_name(const std::shared_ptr &d2) { return d2->name(); } +}; + // test_move_only_holder struct C { C() { print_created(this); } + C(const C &) = delete; ~C() { print_destroyed(this); } }; @@ -265,6 +293,7 @@ struct TypeForHolderWithAddressOf { // test_move_only_holder_with_addressof_operator struct TypeForMoveOnlyHolderWithAddressOf { explicit TypeForMoveOnlyHolderWithAddressOf(int value) : value{value} { print_created(this); } + TypeForMoveOnlyHolderWithAddressOf(const TypeForMoveOnlyHolderWithAddressOf &) = delete; ~TypeForMoveOnlyHolderWithAddressOf() { print_destroyed(this); } std::string toString() const { return "MoveOnlyHolderWithAddressOf[" + std::to_string(value) + "]"; @@ -515,6 +544,17 @@ TEST_SUBMODULE(smart_ptr, m) { py::class_>(m, "SharedFromThisVirt") .def_static("get", []() { return sft.get(); }); + // Issue #5989: static_pointer_cast where dynamic_pointer_cast is needed + py::class_>(m, "SftVirtBase") + .def(py::init<>(&SftVirtBase::create)) + .def("name", &SftVirtBase::name); + py::class_>(m, "SftVirtDerived") + .def(py::init<>(&SftVirtDerived::create)); + py::class_>( + m, "SftVirtDerived2") + .def(py::init<>(&SftVirtDerived2::create)) + .def("call_name", &SftVirtDerived2::call_name, py::arg("d2")); + // test_move_only_holder py::class_>(m, "TypeWithMoveOnlyHolder") .def_static("make", []() { return custom_unique_ptr(new C); }) diff --git a/pybind11/tests/test_smart_ptr.py b/pybind11/tests/test_smart_ptr.py index 2d48aac..76ebd8c 100644 --- a/pybind11/tests/test_smart_ptr.py +++ b/pybind11/tests/test_smart_ptr.py @@ -251,6 +251,19 @@ def test_shared_ptr_from_this_and_references(): assert y is z +def test_shared_from_this_virt_shared_ptr_arg(): + """Issue #5989: static_pointer_cast fails with virtual inheritance.""" + b = m.SftVirtBase() + assert b.name() == "SftVirtBase" + + d = m.SftVirtDerived() + assert d.name() == "SftVirtDerived" + + d2 = m.SftVirtDerived2() + assert d2.name() == "SftVirtDerived2" + assert d2.call_name(d2) == "SftVirtDerived2" + + @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") def test_move_only_holder(): a = m.TypeWithMoveOnlyHolder.make() diff --git a/pybind11/tests/test_standalone_enum_module.py b/pybind11/tests/test_standalone_enum_module.py new file mode 100644 index 0000000..3358b88 --- /dev/null +++ b/pybind11/tests/test_standalone_enum_module.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import os + +import env + + +def test_enum_import_exit_no_crash(): + # Added in PR #6015. Modeled after reproducer under issue #5976 + env.check_script_success_in_subprocess( + f""" + import sys + sys.path.insert(0, {os.path.dirname(env.__file__)!r}) + import standalone_enum_module as m + assert m.SomeEnum.__class__.__name__ == "pybind11_type" + """, + rerun=1, + ) diff --git a/pybind11/tests/test_stl.cpp b/pybind11/tests/test_stl.cpp index 5e6d6a3..6084d51 100644 --- a/pybind11/tests/test_stl.cpp +++ b/pybind11/tests/test_stl.cpp @@ -99,6 +99,7 @@ class OptionalProperties { using OptionalEnumValue = OptionalImpl; OptionalProperties() : value(EnumType::kSet) {} + OptionalProperties(const OptionalProperties &) = default; ~OptionalProperties() { // Reset value to detect use-after-destruction. // This is set to a specific value rather than nullopt to ensure that diff --git a/pybind11/tests/test_stl.py b/pybind11/tests/test_stl.py index 4a57635..b04f55c 100644 --- a/pybind11/tests/test_stl.py +++ b/pybind11/tests/test_stl.py @@ -22,7 +22,7 @@ def test_vector(doc): assert doc(m.cast_vector) == "cast_vector() -> list[int]" assert ( doc(m.load_vector) - == "load_vector(arg0: collections.abc.Sequence[typing.SupportsInt]) -> bool" + == "load_vector(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> bool" ) # Test regression caused by 936: pointers to stl containers weren't castable @@ -51,7 +51,7 @@ def test_array(doc): ) assert ( doc(m.load_array) - == 'load_array(arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsInt], "FixedSize(2)"]) -> bool' + == 'load_array(arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> bool' ) @@ -72,7 +72,7 @@ def test_valarray(doc): assert doc(m.cast_valarray) == "cast_valarray() -> list[int]" assert ( doc(m.load_valarray) - == "load_valarray(arg0: collections.abc.Sequence[typing.SupportsInt]) -> bool" + == "load_valarray(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> bool" ) @@ -234,7 +234,7 @@ def test_reference_sensitive_optional(doc): assert ( doc(m.double_or_zero_refsensitive) - == "double_or_zero_refsensitive(arg0: typing.SupportsInt | None) -> int" + == "double_or_zero_refsensitive(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> int" ) assert m.half_or_none_refsensitive(0) is None @@ -352,7 +352,7 @@ def test_variant(doc): assert ( doc(m.load_variant) - == "load_variant(arg0: typing.SupportsInt | str | typing.SupportsFloat | None) -> str" + == "load_variant(arg0: typing.SupportsInt | typing.SupportsIndex | str | typing.SupportsFloat | typing.SupportsIndex | None) -> str" ) @@ -368,7 +368,7 @@ def test_variant_monostate(doc): assert ( doc(m.load_monostate_variant) - == "load_monostate_variant(arg0: None | typing.SupportsInt | str) -> str" + == "load_monostate_variant(arg0: None | typing.SupportsInt | typing.SupportsIndex | str) -> str" ) @@ -388,7 +388,7 @@ def test_stl_pass_by_pointer(msg): msg(excinfo.value) == """ stl_pass_by_pointer(): incompatible function arguments. The following argument types are supported: - 1. (v: collections.abc.Sequence[typing.SupportsInt] = None) -> list[int] + 1. (v: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = None) -> list[int] Invoked with: """ @@ -400,7 +400,7 @@ def test_stl_pass_by_pointer(msg): msg(excinfo.value) == """ stl_pass_by_pointer(): incompatible function arguments. The following argument types are supported: - 1. (v: collections.abc.Sequence[typing.SupportsInt] = None) -> list[int] + 1. (v: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = None) -> list[int] Invoked with: None """ @@ -615,7 +615,7 @@ class FormalSequenceLike(BareSequenceLike, Sequence): # convert mode assert ( doc(m.roundtrip_std_vector_int) - == "roundtrip_std_vector_int(arg0: collections.abc.Sequence[typing.SupportsInt]) -> list[int]" + == "roundtrip_std_vector_int(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> list[int]" ) assert m.roundtrip_std_vector_int([1, 2, 3]) == [1, 2, 3] assert m.roundtrip_std_vector_int((1, 2, 3)) == [1, 2, 3] @@ -668,7 +668,7 @@ class FormalMappingLike(BareMappingLike, Mapping): # convert mode assert ( doc(m.roundtrip_std_map_str_int) - == "roundtrip_std_map_str_int(arg0: collections.abc.Mapping[str, typing.SupportsInt]) -> dict[str, int]" + == "roundtrip_std_map_str_int(arg0: collections.abc.Mapping[str, typing.SupportsInt | typing.SupportsIndex]) -> dict[str, int]" ) assert m.roundtrip_std_map_str_int(a1b2c3) == a1b2c3 assert m.roundtrip_std_map_str_int(FormalMappingLike(**a1b2c3)) == a1b2c3 @@ -714,7 +714,7 @@ class FormalSetLike(BareSetLike, Set): # convert mode assert ( doc(m.roundtrip_std_set_int) - == "roundtrip_std_set_int(arg0: collections.abc.Set[typing.SupportsInt]) -> set[int]" + == "roundtrip_std_set_int(arg0: collections.abc.Set[typing.SupportsInt | typing.SupportsIndex]) -> set[int]" ) assert m.roundtrip_std_set_int({1, 2, 3}) == {1, 2, 3} assert m.roundtrip_std_set_int(FormalSetLike(1, 2, 3)) == {1, 2, 3} diff --git a/pybind11/tests/test_stl_binders.cpp b/pybind11/tests/test_stl_binders.cpp index f846ae8..f399ec0 100644 --- a/pybind11/tests/test_stl_binders.cpp +++ b/pybind11/tests/test_stl_binders.cpp @@ -55,7 +55,7 @@ template Map *times_ten(int n) { auto *m = new Map(); for (int i = 1; i <= n; i++) { - m->emplace(int(i), E_nc(10 * i)); + m->emplace(i, E_nc(10 * i)); } return m; } @@ -65,7 +65,7 @@ NestMap *times_hundred(int n) { auto *m = new NestMap(); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { - (*m)[i].emplace(int(j * 10), E_nc(100 * j)); + (*m)[i].emplace(j * 10, E_nc(100 * j)); } } return m; diff --git a/pybind11/tests/test_stl_binders.py b/pybind11/tests/test_stl_binders.py index 9856ba4..518f2df 100644 --- a/pybind11/tests/test_stl_binders.py +++ b/pybind11/tests/test_stl_binders.py @@ -258,7 +258,7 @@ def test_noncopyable_containers(): assert nvnc[i][j].value == j + 1 # Note: maps do not have .values() - for _, v in nvnc.items(): + for v in nvnc.values(): for i, j in enumerate(v, start=1): assert j.value == i @@ -269,7 +269,7 @@ def test_noncopyable_containers(): assert nmnc[i][j].value == 10 * j vsum = 0 - for _, v_o in nmnc.items(): + for v_o in nmnc.values(): for k_i, v_i in v_o.items(): assert v_i.value == 10 * k_i vsum += v_i.value @@ -283,7 +283,7 @@ def test_noncopyable_containers(): assert numnc[i][j].value == 10 * j vsum = 0 - for _, v_o in numnc.items(): + for v_o in numnc.values(): for k_i, v_i in v_o.items(): assert v_i.value == 10 * k_i vsum += v_i.value diff --git a/pybind11/tests/test_tagbased_polymorphic.cpp b/pybind11/tests/test_tagbased_polymorphic.cpp index 13e5ed3..8a8a328 100644 --- a/pybind11/tests/test_tagbased_polymorphic.cpp +++ b/pybind11/tests/test_tagbased_polymorphic.cpp @@ -17,6 +17,8 @@ struct Animal { // (https://github.com/pybind/pybind11/pull/2016/). virtual ~Animal() = default; + Animal(const Animal &) = delete; + // Enum for tag-based polymorphism. enum class Kind { Unknown = 0, diff --git a/pybind11/tests/test_thread.cpp b/pybind11/tests/test_thread.cpp index eabf39a..131bd87 100644 --- a/pybind11/tests/test_thread.cpp +++ b/pybind11/tests/test_thread.cpp @@ -15,6 +15,10 @@ #include #include +#if defined(PYBIND11_HAS_STD_BARRIER) +# include +#endif + namespace py = pybind11; namespace { @@ -34,7 +38,6 @@ EmptyStruct SharedInstance; } // namespace TEST_SUBMODULE(thread, m) { - py::class_(m, "IntStruct").def(py::init([](const int i) { return IntStruct(i); })); // implicitly_convertible uses loader_life_support when an implicit @@ -67,6 +70,39 @@ TEST_SUBMODULE(thread, m) { py::class_(m, "EmptyStruct") .def_readonly_static("SharedInstance", &SharedInstance); +#if defined(PYBIND11_HAS_STD_BARRIER) + // In the free-threaded build, during PyThreadState_Clear, removing the thread from the biased + // reference counting table may call destructors. Make sure that it doesn't crash. + m.def("test_pythread_state_clear_destructor", [](py::type cls) { + py::handle obj; + + std::barrier barrier{2}; + std::thread thread1{[&]() { + py::gil_scoped_acquire gil; + obj = cls().release(); + barrier.arrive_and_wait(); + }}; + std::thread thread2{[&]() { + py::gil_scoped_acquire gil; + barrier.arrive_and_wait(); + // ob_ref_shared becomes negative; transition to the queued state + obj.dec_ref(); + }}; + + // jthread is not supported by Apple Clang + thread1.join(); + thread2.join(); + }); +#endif + + m.attr("defined_PYBIND11_HAS_STD_BARRIER") = +#ifdef PYBIND11_HAS_STD_BARRIER + true; +#else + false; +#endif + m.def("acquire_gil", []() { py::gil_scoped_acquire gil_acquired; }); + // NOTE: std::string_view also uses loader_life_support to ensure that // the string contents remain alive, but that's a C++ 17 feature. } diff --git a/pybind11/tests/test_thread.py b/pybind11/tests/test_thread.py index e9d7baf..d302c38 100644 --- a/pybind11/tests/test_thread.py +++ b/pybind11/tests/test_thread.py @@ -5,6 +5,7 @@ import pytest +import env from pybind11_tests import thread as m @@ -66,3 +67,14 @@ def access_shared_instance(): thread.start() for thread in threads: thread.join() + + +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") +@pytest.mark.skipif(not m.defined_PYBIND11_HAS_STD_BARRIER, reason="no ") +@pytest.mark.skipif(env.sys_is_gil_enabled(), reason="Deadlock with the GIL") +def test_pythread_state_clear_destructor(): + class Foo: + def __del__(self): + m.acquire_gil() + + m.test_pythread_state_clear_destructor(Foo) diff --git a/pybind11/tests/test_type_caster_pyobject_ptr.py b/pybind11/tests/test_type_caster_pyobject_ptr.py index 5df8ca0..f9abd00 100644 --- a/pybind11/tests/test_type_caster_pyobject_ptr.py +++ b/pybind11/tests/test_type_caster_pyobject_ptr.py @@ -103,7 +103,8 @@ def test_return_list_pyobject_ptr_reference(): def test_type_caster_name_via_incompatible_function_arguments_type_error(): with pytest.raises( - TypeError, match=r"1\. \(arg0: object, arg1: typing.SupportsInt\) -> None" + TypeError, + match=r"1\. \(arg0: object, arg1: typing.SupportsInt \| typing.SupportsIndex\) -> None", ): m.pass_pyobject_ptr_and_int(ValueHolder(101), ValueHolder(202)) diff --git a/pybind11/tests/test_unnamed_namespace_a.py b/pybind11/tests/test_unnamed_namespace_a.py index fabf131..514a812 100644 --- a/pybind11/tests/test_unnamed_namespace_a.py +++ b/pybind11/tests/test_unnamed_namespace_a.py @@ -5,7 +5,10 @@ from pybind11_tests import unnamed_namespace_a as m from pybind11_tests import unnamed_namespace_b as mb -XFAIL_CONDITION = "not m.defined_WIN32_or__WIN32 and (m.defined___clang__ or m.defined__LIBCPP_VERSION)" +XFAIL_CONDITION = ( + "m.defined__LIBCPP_VERSION or " + "(not m.defined_WIN32_or__WIN32 and m.defined___clang__)" +) XFAIL_REASON = "Known issues: https://github.com/pybind/pybind11/pull/4319" diff --git a/pybind11/tests/test_virtual_functions.cpp b/pybind11/tests/test_virtual_functions.cpp index a6164eb..78a4fdf 100644 --- a/pybind11/tests/test_virtual_functions.cpp +++ b/pybind11/tests/test_virtual_functions.cpp @@ -173,8 +173,7 @@ struct AdderBase { using DataVisitor = std::function; virtual void - operator()(const Data &first, const Data &second, const DataVisitor &visitor) const - = 0; + operator()(const Data &first, const Data &second, const DataVisitor &visitor) const = 0; virtual ~AdderBase() = default; AdderBase() = default; AdderBase(const AdderBase &) = delete; diff --git a/pybind11/tests/test_embed/CMakeLists.txt b/pybind11/tests/test_with_catch/CMakeLists.txt similarity index 79% rename from pybind11/tests/test_embed/CMakeLists.txt rename to pybind11/tests/test_with_catch/CMakeLists.txt index 23c3336..e6a9f67 100644 --- a/pybind11/tests/test_embed/CMakeLists.txt +++ b/pybind11/tests/test_with_catch/CMakeLists.txt @@ -33,10 +33,11 @@ if(PYBIND11_TEST_SMART_HOLDER) -DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE) endif() -add_executable(test_embed catch.cpp test_interpreter.cpp test_subinterpreter.cpp) -pybind11_enable_warnings(test_embed) +add_executable(test_with_catch catch.cpp test_args_convert_vector.cpp test_argument_vector.cpp + test_interpreter.cpp test_subinterpreter.cpp) +pybind11_enable_warnings(test_with_catch) -target_link_libraries(test_embed PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads) +target_link_libraries(test_with_catch PRIVATE pybind11::embed Catch2::Catch2 Threads::Threads) if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) file(COPY test_interpreter.py test_trampoline.py DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") @@ -44,9 +45,11 @@ endif() add_custom_target( cpptest - COMMAND "$" - DEPENDS test_embed - WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + COMMAND "$" + DEPENDS test_with_catch + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + USES_TERMINAL # Ensures output is shown immediately (not buffered and possibly lost on crash) +) pybind11_add_module(external_module THIN_LTO external_module.cpp) set_target_properties(external_module PROPERTIES LIBRARY_OUTPUT_DIRECTORY diff --git a/pybind11/tests/test_with_catch/catch.cpp b/pybind11/tests/test_with_catch/catch.cpp new file mode 100644 index 0000000..8959593 --- /dev/null +++ b/pybind11/tests/test_with_catch/catch.cpp @@ -0,0 +1,175 @@ +// The Catch implementation is compiled here. This is a standalone +// translation unit to avoid recompiling it for every test change. + +#include + +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +# include +#endif + +// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to +// catch 2.0.1; this should be fixed in the next catch release after 2.0.1). +PYBIND11_WARNING_DISABLE_MSVC(4996) + +// Catch uses _ internally, which breaks gettext style defines +#ifdef _ +# undef _ +#endif + +#define CATCH_CONFIG_RUNNER +#define CATCH_CONFIG_DEFAULT_REPORTER "progress" +#include "catch_skip.h" + +#include + +namespace py = pybind11; + +// Simple progress reporter that prints a line per test case. +namespace { + +class ProgressReporter : public Catch::StreamingReporterBase { +public: + using StreamingReporterBase::StreamingReporterBase; + + static std::string getDescription() { return "Simple progress reporter (one line per test)"; } + + void testCaseStarting(Catch::TestCaseInfo const &testInfo) override { + print_python_version_once(); + auto &os = Catch::cout(); + os << "[ RUN ] " << testInfo.name << '\n'; + os.flush(); + } + + void testCaseEnded(Catch::TestCaseStats const &stats) override { + bool failed = stats.totals.assertions.failed > 0; + auto &os = Catch::cout(); + os << (failed ? "[ FAILED ] " : "[ OK ] ") << stats.testInfo.name << '\n'; + os.flush(); + } + + void noMatchingTestCases(std::string const &spec) override { + auto &os = Catch::cout(); + os << "[ NO TEST ] no matching test cases for spec: " << spec << '\n'; + os.flush(); + } + + void reportInvalidArguments(std::string const &arg) override { + auto &os = Catch::cout(); + os << "[ ERROR ] invalid Catch2 arguments: " << arg << '\n'; + os.flush(); + } + + void assertionStarting(Catch::AssertionInfo const &) override {} + + bool assertionEnded(Catch::AssertionStats const &) override { return false; } + + void testRunEnded(Catch::TestRunStats const &stats) override { + auto &os = Catch::cout(); + auto passed = stats.totals.testCases.passed; + auto failed = stats.totals.testCases.failed; + auto total = passed + failed; + auto assertions = stats.totals.assertions.passed + stats.totals.assertions.failed; + if (failed == 0) { + os << "[ PASSED ] " << total << " test cases, " << assertions << " assertions.\n"; + } else { + os << "[ FAILED ] " << failed << " of " << total << " test cases, " << assertions + << " assertions.\n"; + } + os.flush(); + } + +private: + void print_python_version_once() { + if (printed_) { + return; + } + printed_ = true; + auto &os = Catch::cout(); + os << "[ PYTHON ] " << Py_GetVersion() << '\n'; + os.flush(); + } + + bool printed_ = false; +}; + +} // namespace + +CATCH_REGISTER_REPORTER("progress", ProgressReporter) + +namespace { + +std::string get_utc_timestamp() { + auto now = std::chrono::system_clock::now(); + auto time_t_now = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + + std::tm utc_tm{}; +#if defined(_WIN32) + gmtime_s(&utc_tm, &time_t_now); +#else + gmtime_r(&time_t_now, &utc_tm); +#endif + + std::ostringstream oss; + oss << std::put_time(&utc_tm, "%Y-%m-%d %H:%M:%S") << '.' << std::setfill('0') << std::setw(3) + << ms.count() << 'Z'; + return oss.str(); +} + +#ifndef _WIN32 +// Signal handler to print a message when the process is terminated. +// Uses only async-signal-safe functions. +void termination_signal_handler(int sig) { + const char *msg = "[ SIGNAL ] Process received SIGTERM\n"; + // write() is async-signal-safe, unlike std::cout + ssize_t written = write(STDOUT_FILENO, msg, strlen(msg)); + (void) written; // suppress "unused variable" warnings + // Re-raise with default handler to get proper exit status + std::signal(sig, SIG_DFL); + std::raise(sig); +} +#endif + +} // namespace + +int main(int argc, char *argv[]) { +#ifndef _WIN32 + std::signal(SIGTERM, termination_signal_handler); +#endif + + // Setup for TEST_CASE in test_interpreter.cpp, tagging on a large random number: + std::string updated_pythonpath("pybind11_test_with_catch_PYTHONPATH_2099743835476552"); + const char *preexisting_pythonpath = getenv("PYTHONPATH"); + if (preexisting_pythonpath != nullptr) { +#if defined(_WIN32) + updated_pythonpath += ';'; +#else + updated_pythonpath += ':'; +#endif + updated_pythonpath += preexisting_pythonpath; + } +#if defined(_WIN32) + _putenv_s("PYTHONPATH", updated_pythonpath.c_str()); +#else + setenv("PYTHONPATH", updated_pythonpath.c_str(), /*replace=*/1); +#endif + + std::cout << "[ STARTING ] " << get_utc_timestamp() << '\n'; + std::cout.flush(); + + py::scoped_interpreter guard{}; + + auto result = Catch::Session().run(argc, argv); + + std::cout << "[ DONE ] " << get_utc_timestamp() << " (result " << result << ")\n"; + std::cout.flush(); + + return result < 0xff ? result : 0xff; +} diff --git a/pybind11/tests/test_with_catch/catch_skip.h b/pybind11/tests/test_with_catch/catch_skip.h new file mode 100644 index 0000000..9e2954d --- /dev/null +++ b/pybind11/tests/test_with_catch/catch_skip.h @@ -0,0 +1,21 @@ +// Macro to skip a test at runtime with a visible message. +// Catch2 v2 doesn't have native skip support (v3 does with SKIP()). +// The test will count as "passed" in totals, but the output clearly shows it was skipped. + +#pragma once + +#include + +#include + +#define PYBIND11_CATCH2_SKIP_IF(condition, reason) \ + do { \ + PYBIND11_WARNING_PUSH \ + PYBIND11_WARNING_DISABLE_MSVC(4127) \ + if (condition) { \ + Catch::cout() << "[ SKIPPED ] " << (reason) << '\n'; \ + Catch::cout().flush(); \ + return; \ + } \ + PYBIND11_WARNING_POP \ + } while (0) diff --git a/pybind11/tests/test_embed/external_module.cpp b/pybind11/tests/test_with_catch/external_module.cpp similarity index 50% rename from pybind11/tests/test_embed/external_module.cpp rename to pybind11/tests/test_with_catch/external_module.cpp index 3465e8b..933ee3a 100644 --- a/pybind11/tests/test_embed/external_module.cpp +++ b/pybind11/tests/test_with_catch/external_module.cpp @@ -6,11 +6,26 @@ namespace py = pybind11; * modules aren't preserved over a finalize/initialize. */ +namespace { +// Compare unsafe_reset_internals_for_single_interpreter in +// test_subinterpreter.cpp. +void unsafe_reset_local_internals() { + // NOTE: This code is NOT SAFE unless the caller guarantees no other threads are alive + // NOTE: This code is tied to the precise implementation of the internals holder + + py::detail::get_local_internals_pp_manager().unref(); + py::detail::get_local_internals(); +} +} // namespace + PYBIND11_MODULE(external_module, m, py::mod_gil_not_used(), py::multiple_interpreters::per_interpreter_gil()) { - + // At least one test ("Single Subinterpreter") wants to reset + // internals. We have separate local internals because we are a + // separate DSO, so ours need to be reset too! + unsafe_reset_local_internals(); class A { public: explicit A(int value) : v{value} {}; diff --git a/pybind11/tests/test_with_catch/test_args_convert_vector.cpp b/pybind11/tests/test_with_catch/test_args_convert_vector.cpp new file mode 100644 index 0000000..7ce2d71 --- /dev/null +++ b/pybind11/tests/test_with_catch/test_args_convert_vector.cpp @@ -0,0 +1,80 @@ +#include "pybind11/pybind11.h" +#include "catch.hpp" + +namespace py = pybind11; + +using args_convert_vector = py::detail::args_convert_vector; + +namespace { +template +std::vector get_sample_vectors() { + std::vector result; + result.emplace_back(); + for (const auto sz : {0, 4, 5, 6, 31, 32, 33, 63, 64, 65}) { + for (const bool b : {false, true}) { + result.emplace_back(static_cast(sz), b); + } + } + return result; +} + +void require_vector_matches_sample(const args_convert_vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for (size_t ii = 0; ii < actual.size(); ++ii) { + REQUIRE(actual[ii] == expected[ii]); + } +} + +template +void mutation_test_with_samples(ActualMutationFunc actual_mutation_func, + ExpectedMutationFunc expected_mutation_func) { + auto sample_contents = get_sample_vectors>(); + auto samples = get_sample_vectors(); + for (size_t ii = 0; ii < samples.size(); ++ii) { + auto &actual = samples[ii]; + auto &expected = sample_contents[ii]; + + actual_mutation_func(actual); + expected_mutation_func(expected); + require_vector_matches_sample(actual, expected); + } +} +} // namespace + +// I would like to write [capture](auto& vec) block inline, but we +// have to work with C++11, which doesn't have generic lambdas. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define MUTATION_LAMBDA(capture, block) \ + [capture](args_convert_vector & vec) block, [capture](std::vector & vec) block +// NOLINTEND(bugprone-macro-parentheses) + +// For readability, rather than having ugly empty arguments. +#define NO_CAPTURE + +TEST_CASE("check sample args_convert_vector contents") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { (void) vec; })); +} + +TEST_CASE("args_convert_vector push_back") { + for (const bool b : {false, true}) { + mutation_test_with_samples(MUTATION_LAMBDA(b, { vec.push_back(b); })); + } +} + +TEST_CASE("args_convert_vector reserve") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { vec.reserve(ii); })); + } +} + +TEST_CASE("args_convert_vector reserve then push_back") { + for (std::size_t ii = 0; ii < 4; ++ii) { + for (const bool b : {false, true}) { + mutation_test_with_samples(MUTATION_LAMBDA(=, { + vec.reserve(ii); + vec.push_back(b); + })); + } + } +} diff --git a/pybind11/tests/test_with_catch/test_argument_vector.cpp b/pybind11/tests/test_with_catch/test_argument_vector.cpp new file mode 100644 index 0000000..9cf302a --- /dev/null +++ b/pybind11/tests/test_with_catch/test_argument_vector.cpp @@ -0,0 +1,94 @@ +#include "pybind11/pybind11.h" +#include "catch.hpp" + +namespace py = pybind11; + +// 2 is chosen because it is the smallest number (keeping tests short) +// where we can create non-empty vectors whose size is the inline size +// plus or minus 1. +using argument_vector = py::detail::argument_vector<2>; + +namespace { +argument_vector to_argument_vector(const std::vector &v) { + argument_vector result; + result.reserve(v.size()); + for (const auto x : v) { + result.push_back(x); + } + return result; +} + +std::vector> get_sample_argument_vector_contents() { + return std::vector>{ + {}, + {py::handle(Py_None)}, + {py::handle(Py_None), py::handle(Py_False)}, + {py::handle(Py_None), py::handle(Py_False), py::handle(Py_True)}, + }; +} + +std::vector get_sample_argument_vectors() { + std::vector result; + for (const auto &vec : get_sample_argument_vector_contents()) { + result.push_back(to_argument_vector(vec)); + } + return result; +} + +void require_vector_matches_sample(const argument_vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for (size_t ii = 0; ii < actual.size(); ++ii) { + REQUIRE(actual[ii].ptr() == expected[ii].ptr()); + } +} + +template +void mutation_test_with_samples(ActualMutationFunc actual_mutation_func, + ExpectedMutationFunc expected_mutation_func) { + auto sample_contents = get_sample_argument_vector_contents(); + auto samples = get_sample_argument_vectors(); + for (size_t ii = 0; ii < samples.size(); ++ii) { + auto &actual = samples[ii]; + auto &expected = sample_contents[ii]; + + actual_mutation_func(actual); + expected_mutation_func(expected); + require_vector_matches_sample(actual, expected); + } +} + +} // namespace + +// I would like to write [capture](auto& vec) block inline, but we +// have to work with C++11, which doesn't have generic lambdas. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define MUTATION_LAMBDA(capture, block) \ + [capture](argument_vector & vec) block, [capture](std::vector & vec) block +// NOLINTEND(bugprone-macro-parentheses) + +// For readability, rather than having ugly empty arguments. +#define NO_CAPTURE + +TEST_CASE("check sample argument_vector contents") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { (void) vec; })); +} + +TEST_CASE("argument_vector push_back") { + mutation_test_with_samples(MUTATION_LAMBDA(NO_CAPTURE, { vec.emplace_back(Py_None); })); +} + +TEST_CASE("argument_vector reserve") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { vec.reserve(ii); })); + } +} + +TEST_CASE("argument_vector reserve then push_back") { + for (std::size_t ii = 0; ii < 4; ++ii) { + mutation_test_with_samples(MUTATION_LAMBDA(ii, { + vec.reserve(ii); + vec.emplace_back(Py_True); + })); + } +} diff --git a/pybind11/tests/test_embed/test_interpreter.cpp b/pybind11/tests/test_with_catch/test_interpreter.cpp similarity index 92% rename from pybind11/tests/test_embed/test_interpreter.cpp rename to pybind11/tests/test_with_catch/test_interpreter.cpp index 0e6c17a..4103c0f 100644 --- a/pybind11/tests/test_embed/test_interpreter.cpp +++ b/pybind11/tests/test_with_catch/test_interpreter.cpp @@ -5,6 +5,8 @@ // catch 2.0.1; this should be fixed in the next catch release after 2.0.1). PYBIND11_WARNING_DISABLE_MSVC(4996) +#include "catch_skip.h" + #include #include #include @@ -84,6 +86,14 @@ PYBIND11_EMBEDDED_MODULE(trampoline_module, m) { .def("func", &test_override_cache_helper::func); } +enum class SomeEnum { value1, value2 }; // Added in PR #6015 + +PYBIND11_EMBEDDED_MODULE(enum_module, m, py::multiple_interpreters::per_interpreter_gil()) { + py::enum_(m, "SomeEnum") + .value("value1", SomeEnum::value1) + .value("value2", SomeEnum::value2); +} + PYBIND11_EMBEDDED_MODULE(throw_exception, ) { throw std::runtime_error("C++ Error"); } PYBIND11_EMBEDDED_MODULE(throw_error_already_set, ) { @@ -94,8 +104,9 @@ PYBIND11_EMBEDDED_MODULE(throw_error_already_set, ) { TEST_CASE("PYTHONPATH is used to update sys.path") { // The setup for this TEST_CASE is in catch.cpp! auto sys_path = py::str(py::module_::import("sys").attr("path")).cast(); - REQUIRE_THAT(sys_path, - Catch::Matchers::Contains("pybind11_test_embed_PYTHONPATH_2099743835476552")); + REQUIRE_THAT( + sys_path, + Catch::Matchers::Contains("pybind11_test_with_catch_PYTHONPATH_2099743835476552")); } TEST_CASE("Pass classes and data between modules defined in C++ and Python") { @@ -342,6 +353,24 @@ TEST_CASE("Restart the interpreter") { REQUIRE(py_widget.attr("the_message").cast() == "Hello after restart"); } +TEST_CASE("Enum module survives restart") { // Added in PR #6015 + // Regression test for gh-5976: py::enum_ uses def_property_static, which + // calls process_attributes::init after initialize_generic's strdup loop, + // leaving arg names as string literals. Without the fix, destruct() would + // call free() on those literals during interpreter finalization. + PYBIND11_CATCH2_SKIP_IF(PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION == 12, + "Pre-existing crash in enum cleanup during finalize on Python 3.12"); + + auto enum_mod = py::module_::import("enum_module"); + REQUIRE(enum_mod.attr("SomeEnum").attr("value1").attr("name").cast() == "value1"); + + py::finalize_interpreter(); + py::initialize_interpreter(); + + enum_mod = py::module_::import("enum_module"); + REQUIRE(enum_mod.attr("SomeEnum").attr("value2").attr("name").cast() == "value2"); +} + TEST_CASE("Execution frame") { // When the interpreter is embedded, there is no execution frame, but `py::exec` // should still function by using reasonable globals: `__main__.__dict__`. diff --git a/pybind11/tests/test_embed/test_interpreter.py b/pybind11/tests/test_with_catch/test_interpreter.py similarity index 100% rename from pybind11/tests/test_embed/test_interpreter.py rename to pybind11/tests/test_with_catch/test_interpreter.py diff --git a/pybind11/tests/test_embed/test_subinterpreter.cpp b/pybind11/tests/test_with_catch/test_subinterpreter.cpp similarity index 70% rename from pybind11/tests/test_embed/test_subinterpreter.cpp rename to pybind11/tests/test_with_catch/test_subinterpreter.cpp index 3c7c35b..35b7f02 100644 --- a/pybind11/tests/test_embed/test_subinterpreter.cpp +++ b/pybind11/tests/test_with_catch/test_subinterpreter.cpp @@ -1,11 +1,14 @@ #include #ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT +# include # include // Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to // catch 2.0.1; this should be fixed in the next catch release after 2.0.1). PYBIND11_WARNING_DISABLE_MSVC(4996) +# include "catch_skip.h" + # include # include # include @@ -28,7 +31,7 @@ void unsafe_reset_internals_for_single_interpreter() { py::detail::get_local_internals_pp_manager().unref(); // we know there are no other interpreters, so we can lower this. SUPER DANGEROUS - py::detail::get_num_interpreters_seen() = 1; + py::detail::has_seen_non_main_interpreter() = false; // now we unref the static global singleton internals py::detail::get_internals_pp_manager().unref(); @@ -39,6 +42,30 @@ void unsafe_reset_internals_for_single_interpreter() { py::detail::get_local_internals(); } +py::object &get_dict_type_object() { + PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store storage; + return storage + .call_once_and_store_result( + []() -> py::object { return py::module_::import("builtins").attr("dict"); }) + .get_stored(); +} + +py::object &get_ordered_dict_type_object() { + PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store storage; + return storage + .call_once_and_store_result( + []() -> py::object { return py::module_::import("collections").attr("OrderedDict"); }) + .get_stored(); +} + +py::object &get_default_dict_type_object() { + PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store storage; + return storage + .call_once_and_store_result( + []() -> py::object { return py::module_::import("collections").attr("defaultdict"); }) + .get_stored(); +} + TEST_CASE("Single Subinterpreter") { unsafe_reset_internals_for_single_interpreter(); @@ -104,14 +131,21 @@ TEST_CASE("Move Subinterpreter") { py::module_::import("external_module"); } - std::thread([&]() { + auto t = std::thread([&]() { // Use it again { py::subinterpreter_scoped_activate activate(*sub); py::module_::import("external_module"); } sub.reset(); - }).join(); + }); + + // on 3.14.1+ destructing a sub-interpreter does a stop-the-world. we need to detach our + // thread state in order for that to be possible. + { + py::gil_scoped_release nogil; + t.join(); + } REQUIRE(!sub); @@ -299,6 +333,103 @@ TEST_CASE("Multiple Subinterpreters") { unsafe_reset_internals_for_single_interpreter(); } +// Test that gil_safe_call_once_and_store provides per-interpreter storage. +// Without the per-interpreter storage fix, the subinterpreter would see the value +// cached by the main interpreter, which is invalid (different interpreter's object). +TEST_CASE("gil_safe_call_once_and_store per-interpreter isolation") { + unsafe_reset_internals_for_single_interpreter(); + + // This static simulates a typical usage pattern where a module caches + // an imported object using gil_safe_call_once_and_store. + PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store storage; + + // Get the interpreter ID in the main interpreter + auto main_interp_id = PyInterpreterState_GetID(PyInterpreterState_Get()); + + // Store a value in the main interpreter - we'll store the interpreter ID as a Python int + auto &main_value = storage + .call_once_and_store_result([]() { + return py::int_(PyInterpreterState_GetID(PyInterpreterState_Get())); + }) + .get_stored(); + REQUIRE(main_value.cast() == main_interp_id); + + py::object dict_type = get_dict_type_object(); + py::object ordered_dict_type = get_ordered_dict_type_object(); + py::object default_dict_type = get_default_dict_type_object(); + + int64_t sub_interp_id = -1; + int64_t sub_cached_value = -1; + + bool sub_default_dict_type_destroyed = false; + + // Create a subinterpreter and check that it gets its own storage + { + py::scoped_subinterpreter ssi; + + sub_interp_id = PyInterpreterState_GetID(PyInterpreterState_Get()); + REQUIRE(sub_interp_id != main_interp_id); + + // Access the same static storage from the subinterpreter. + // With per-interpreter storage, this should call the lambda again + // and cache a NEW value for this interpreter. + // Without per-interpreter storage, this would return main's cached value. + auto &sub_value + = storage + .call_once_and_store_result([]() { + return py::int_(PyInterpreterState_GetID(PyInterpreterState_Get())); + }) + .get_stored(); + + sub_cached_value = sub_value.cast(); + + // The cached value should be the SUBINTERPRETER's ID, not the main interpreter's. + // This would fail without per-interpreter storage. + REQUIRE(sub_cached_value == sub_interp_id); + REQUIRE(sub_cached_value != main_interp_id); + + py::object sub_dict_type = get_dict_type_object(); + py::object sub_ordered_dict_type = get_ordered_dict_type_object(); + py::object sub_default_dict_type = get_default_dict_type_object(); + + // Verify that the subinterpreter has its own cached type objects. + // For static types, they should be the same object across interpreters. + // See also: https://docs.python.org/3/c-api/typeobj.html#static-types + REQUIRE(sub_dict_type.is(dict_type)); // dict is a static type + REQUIRE(sub_ordered_dict_type.is(ordered_dict_type)); // OrderedDict is a static type + // For heap types, they are dynamically created per-interpreter. + // See also: https://docs.python.org/3/c-api/typeobj.html#heap-types + REQUIRE_FALSE(sub_default_dict_type.is(default_dict_type)); // defaultdict is a heap type + + // Set up a weakref callback to detect when the subinterpreter's cached default_dict_type + // is destroyed so the gil_safe_call_once_and_store storage is not leaked when the + // subinterpreter is shutdown. + (void) py::weakref(sub_default_dict_type, + py::cpp_function([&](py::handle weakref) -> void { + sub_default_dict_type_destroyed = true; + weakref.dec_ref(); + })) + .release(); + } + + // Back in main interpreter, verify main's value is unchanged + auto &main_value_after = storage.get_stored(); + REQUIRE(main_value_after.cast() == main_interp_id); + + // Verify that the types cached in main are unchanged + py::object dict_type_after = get_dict_type_object(); + py::object ordered_dict_type_after = get_ordered_dict_type_object(); + py::object default_dict_type_after = get_default_dict_type_object(); + REQUIRE(dict_type_after.is(dict_type)); + REQUIRE(ordered_dict_type_after.is(ordered_dict_type)); + REQUIRE(default_dict_type_after.is(default_dict_type)); + + // Verify that the subinterpreter's cached default_dict_type was destroyed + REQUIRE(sub_default_dict_type_destroyed); + + unsafe_reset_internals_for_single_interpreter(); +} + # ifdef Py_MOD_PER_INTERPRETER_GIL_SUPPORTED TEST_CASE("Per-Subinterpreter GIL") { auto main_int @@ -370,15 +501,21 @@ TEST_CASE("Per-Subinterpreter GIL") { // wait for something to set sync to our thread number // we are holding our subinterpreter's GIL - while (sync != num) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + py::gil_scoped_release nogil; + while (sync != num) + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } // now change it so the next thread can move on ++sync; // but keep holding the GIL until after the next thread moves on as well - while (sync == num + 1) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + py::gil_scoped_release nogil; + while (sync == num + 1) + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } // one last check before quitting the thread, the internals should be different auto sub_int diff --git a/pybind11/tests/test_embed/test_trampoline.py b/pybind11/tests/test_with_catch/test_trampoline.py similarity index 100% rename from pybind11/tests/test_embed/test_trampoline.py rename to pybind11/tests/test_with_catch/test_trampoline.py diff --git a/pybind11/tools/pybind11Common.cmake b/pybind11/tools/pybind11Common.cmake index 1c1d4f8..da887a2 100644 --- a/pybind11/tools/pybind11Common.cmake +++ b/pybind11/tools/pybind11Common.cmake @@ -41,7 +41,18 @@ set(pybind11_INCLUDE_DIRS "${pybind11_INCLUDE_DIR}" CACHE INTERNAL "Include directory for pybind11 (Python not requested)") -if(CMAKE_CROSSCOMPILING AND PYBIND11_USE_CROSSCOMPILING) +# CMP0190 prohibits calling FindPython with both Interpreter and Development components +# when cross-compiling, unless the CMAKE_CROSSCOMPILING_EMULATOR variable is defined. +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.1") + cmake_policy(GET CMP0190 _pybind11_cmp0190) + if(_pybind11_cmp0190 STREQUAL "NEW") + set(PYBIND11_USE_CROSSCOMPILING "ON") + endif() +endif() + +if(CMAKE_CROSSCOMPILING + AND PYBIND11_USE_CROSSCOMPILING + AND NOT DEFINED CMAKE_CROSSCOMPILING_EMULATOR) set(_PYBIND11_CROSSCOMPILING ON CACHE INTERNAL "") diff --git a/pybind11/tools/pybind11GuessPythonExtSuffix.cmake b/pybind11/tools/pybind11GuessPythonExtSuffix.cmake index b8c351e..f145739 100644 --- a/pybind11/tools/pybind11GuessPythonExtSuffix.cmake +++ b/pybind11/tools/pybind11GuessPythonExtSuffix.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) function(pybind11_guess_python_module_extension python) @@ -14,15 +14,23 @@ function(pybind11_guess_python_module_extension python) STRING "Extension suffix for Python extension modules (Initialized from SETUPTOOLS_EXT_SUFFIX)") endif() + + # The final extension depends on the system + set(_PY_BUILD_EXTENSION "${CMAKE_SHARED_MODULE_SUFFIX}") + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(_PY_BUILD_EXTENSION ".pyd") + endif() + + # If running under scikit-build-core, use the SKBUILD_SOABI variable: + if(NOT DEFINED PYTHON_MODULE_EXT_SUFFIX AND DEFINED SKBUILD_SOABI) + message(STATUS "Determining Python extension suffix based on SKBUILD_SOABI: ${SKBUILD_SOABI}") + set(PYTHON_MODULE_EXT_SUFFIX ".${SKBUILD_SOABI}${_PY_BUILD_EXTENSION}") + endif() + # If that didn't work, use the Python_SOABI variable: if(NOT DEFINED PYTHON_MODULE_EXT_SUFFIX AND DEFINED ${python}_SOABI) message( STATUS "Determining Python extension suffix based on ${python}_SOABI: ${${python}_SOABI}") - # The final extension depends on the system - set(_PY_BUILD_EXTENSION "${CMAKE_SHARED_MODULE_SUFFIX}") - if(CMAKE_SYSTEM_NAME STREQUAL "Windows") - set(_PY_BUILD_EXTENSION ".pyd") - endif() # If the SOABI already has an extension, use it as the full suffix # (used for debug versions of Python on Windows) if(${python}_SOABI MATCHES "\\.") @@ -43,9 +51,9 @@ function(pybind11_guess_python_module_extension python) # If we could not deduce the extension suffix, unset the results: if(NOT DEFINED PYTHON_MODULE_EXT_SUFFIX) - unset(PYTHON_MODULE_DEBUG_POSTFIX PARENT_SCOPE) - unset(PYTHON_MODULE_EXTENSION PARENT_SCOPE) - unset(PYTHON_IS_DEBUG PARENT_SCOPE) + unset(PYTHON_MODULE_DEBUG_POSTFIX CACHE) + unset(PYTHON_MODULE_EXTENSION CACHE) + unset(PYTHON_IS_DEBUG CACHE) return() endif() @@ -75,12 +83,12 @@ function(pybind11_guess_python_module_extension python) # Return results set(PYTHON_MODULE_DEBUG_POSTFIX "${_PYTHON_MODULE_DEBUG_POSTFIX}" - PARENT_SCOPE) + CACHE INTERNAL "") set(PYTHON_MODULE_EXTENSION "${_PYTHON_MODULE_EXTENSION}" - PARENT_SCOPE) + CACHE INTERNAL "") set(PYTHON_IS_DEBUG "${_PYTHON_IS_DEBUG}" - PARENT_SCOPE) + CACHE INTERNAL "") endfunction() diff --git a/pybind11/tools/pybind11NewTools.cmake b/pybind11/tools/pybind11NewTools.cmake index e881ca7..5ab3142 100644 --- a/pybind11/tools/pybind11NewTools.cmake +++ b/pybind11/tools/pybind11NewTools.cmake @@ -106,18 +106,7 @@ if(PYBIND11_MASTER_PROJECT) endif() endif() -if(NOT _PYBIND11_CROSSCOMPILING) - # If a user finds Python, they may forget to include the Interpreter component - # and the following two steps require it. It is highly recommended by CMake - # when finding development libraries anyway, so we will require it. - if(NOT DEFINED ${_Python}_EXECUTABLE) - message( - FATAL_ERROR - "${_Python} was found without the Interpreter component. Pybind11 requires this component." - ) - - endif() - +if(NOT _PYBIND11_CROSSCOMPILING AND DEFINED ${_Python}_EXECUTABLE) if(DEFINED PYBIND11_PYTHON_EXECUTABLE_LAST AND NOT ${_Python}_EXECUTABLE STREQUAL PYBIND11_PYTHON_EXECUTABLE_LAST) # Detect changes to the Python version/binary in subsequent CMake runs, and refresh config if needed @@ -190,15 +179,15 @@ else() include("${CMAKE_CURRENT_LIST_DIR}/pybind11GuessPythonExtSuffix.cmake") pybind11_guess_python_module_extension("${_Python}") endif() - # When cross-compiling, we cannot query the Python interpreter, so we require - # the user to set these variables explicitly. if(NOT DEFINED PYTHON_IS_DEBUG OR NOT DEFINED PYTHON_MODULE_EXTENSION OR NOT DEFINED PYTHON_MODULE_DEBUG_POSTFIX) message( FATAL_ERROR - "When cross-compiling, you should set the PYTHON_IS_DEBUG, PYTHON_MODULE_EXTENSION and PYTHON_MODULE_DEBUG_POSTFIX \ - variables appropriately before loading pybind11 (e.g. in your CMake toolchain file)") + "A Python interpreter was not found, or you are cross-compiling, and the " + "PYTHON_IS_DEBUG, PYTHON_MODULE_EXTENSION and PYTHON_MODULE_DEBUG_POSTFIX " + "variables could not be guessed. Set these variables appropriately before " + "loading pybind11 (e.g. in your CMake toolchain file)") endif() endif() @@ -248,10 +237,7 @@ if(TARGET ${_Python}::Module) # files. get_target_property(module_target_type ${_Python}::Module TYPE) if(ANDROID AND module_target_type STREQUAL INTERFACE_LIBRARY) - set_property( - TARGET ${_Python}::Module - APPEND - PROPERTY INTERFACE_LINK_LIBRARIES "${${_Python}_LIBRARIES}") + target_link_libraries(${_Python}::Module INTERFACE ${${_Python}_LIBRARIES}) endif() set_property( diff --git a/pybind11/tools/test-pybind11GuessPythonExtSuffix.cmake b/pybind11/tools/test-pybind11GuessPythonExtSuffix.cmake index a66282d..1976abf 100644 --- a/pybind11/tools/test-pybind11GuessPythonExtSuffix.cmake +++ b/pybind11/tools/test-pybind11GuessPythonExtSuffix.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15...4.0) +cmake_minimum_required(VERSION 3.15...4.2) # Tests for pybind11_guess_python_module_extension # Run using `cmake -P tools/test-pybind11GuessPythonExtSuffix.cmake` @@ -87,6 +87,30 @@ unset(PYTHON_MODULE_EXT_SUFFIX) unset(PYTHON_MODULE_EXT_SUFFIX CACHE) unset(ENV{SETUPTOOLS_EXT_SUFFIX}) +# Check the priority of the possible suffix sources. +set(ENV{SETUPTOOLS_EXT_SUFFIX} ".from-setuptools.pyd") +set(SKBUILD_SOABI "from-skbuild") +set(Python3_SOABI "from-python3") +pybind11_guess_python_module_extension("Python3") +expect_streq("${PYTHON_MODULE_EXTENSION}" ".from-setuptools.pyd") + +unset(PYTHON_MODULE_EXT_SUFFIX CACHE) +unset(ENV{SETUPTOOLS_EXT_SUFFIX}) +pybind11_guess_python_module_extension("Python3") +expect_streq("${PYTHON_MODULE_EXTENSION}" ".from-skbuild.pyd") + +unset(SKBUILD_SOABI) +pybind11_guess_python_module_extension("Python3") +expect_streq("${PYTHON_MODULE_EXTENSION}" ".from-python3.pyd") + +set(Python3_SOABI "") +pybind11_guess_python_module_extension("Python3") +expect_streq("${PYTHON_MODULE_EXTENSION}" ".pyd") + +unset(Python3_SOABI) +pybind11_guess_python_module_extension("Python3") +expect_streq("${PYTHON_MODULE_EXTENSION}" "") + # macOS set(CMAKE_SYSTEM_NAME "Darwin") set(CMAKE_SHARED_MODULE_SUFFIX ".so")