diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2f4337b4..c61eb0820 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: # Clang format the codebase automatically - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v19.1.4" + rev: "v19.1.6" 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.8.1 + rev: v0.8.6 hooks: - id: ruff args: ["--fix", "--show-fixes"] @@ -40,7 +40,7 @@ repos: # Check static types with mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.13.0" + rev: "v1.14.1" hooks: - id: mypy args: [] @@ -144,7 +144,7 @@ repos: # PyLint has native support - not always usable, but works for us - repo: https://github.com/PyCQA/pylint - rev: "v3.3.2" + rev: "v3.3.3" hooks: - id: pylint files: ^pybind11 diff --git a/docs/requirements.txt b/docs/requirements.txt index 4e53f0352..f2b57c5c7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -130,9 +130,9 @@ imagesize==1.4.1 \ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a # via sphinx -jinja2==3.1.4 \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d +jinja2==3.1.5 \ + --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ + --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb # via sphinx markupsafe==2.1.5 \ --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index c13732df7..f39a34f05 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -312,7 +312,31 @@ 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_IsImmortal(obj)) { + return; + } + for (;;) { + Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&obj->ob_ref_shared); + if ((shared & _Py_REF_SHARED_FLAG_MASK) != 0) { + // Nothing to do if it's in WEAKREFS, QUEUED, or MERGED states. + return; + } + if (_Py_atomic_compare_exchange_ssize( + &obj->ob_ref_shared, &shared, shared | _Py_REF_MAYBE_WEAKREF)) { + return; + } + } +} +#endif + inline bool register_instance_impl(void *ptr, instance *self) { +#ifdef Py_GIL_DISABLED + enable_try_inc_ref(reinterpret_cast<PyObject *>(self)); +#endif with_instance_map(ptr, [&](instance_map &instances) { instances.emplace(ptr, self); }); return true; // unused, but gives the same signature as the deregister func } diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 217de49e0..ed32e9c31 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -245,6 +245,49 @@ PYBIND11_NOINLINE handle get_type_handle(const std::type_info &tp, bool throw_if return handle(type_info ? ((PyObject *) 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 + // 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); + local += 1; + if (local == 0) { + // immortal + return true; + } + if (_Py_IsOwnedByCurrentThread(obj)) { + _Py_atomic_store_uint32_relaxed(&obj->ob_ref_local, local); +# ifdef Py_REF_DEBUG + _Py_INCREF_IncRefTotal(); +# endif + return true; + } + Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&obj->ob_ref_shared); + for (;;) { + // If the shared refcount is zero and the object is either merged + // or may not have weak references, then we cannot incref it. + if (shared == 0 || shared == _Py_REF_MERGED) { + return false; + } + + if (_Py_atomic_compare_exchange_ssize( + &obj->ob_ref_shared, &shared, shared + (1 << _Py_REF_SHARED_SHIFT))) { +# ifdef Py_REF_DEBUG + _Py_INCREF_IncRefTotal(); +# endif + return true; + } + } +#else + assert(Py_REFCNT(obj) > 0); + Py_INCREF(obj); + return true; +#endif +} + // Searches the inheritance graph for a registered Python instance, using all_type_info(). PYBIND11_NOINLINE handle find_registered_python_instance(void *src, const detail::type_info *tinfo) { @@ -253,7 +296,10 @@ PYBIND11_NOINLINE handle find_registered_python_instance(void *src, for (auto it_i = it_instances.first; it_i != it_instances.second; ++it_i) { for (auto *instance_type : detail::all_type_info(Py_TYPE(it_i->second))) { if (instance_type && same_type(*instance_type->cpptype, *tinfo->cpptype)) { - return handle((PyObject *) it_i->second).inc_ref(); + auto *wrapper = reinterpret_cast<PyObject *>(it_i->second); + if (try_incref(wrapper)) { + return handle(wrapper); + } } } } diff --git a/tests/test_thread.cpp b/tests/test_thread.cpp index e727109d7..eabf39afa 100644 --- a/tests/test_thread.cpp +++ b/tests/test_thread.cpp @@ -28,6 +28,9 @@ struct IntStruct { int value; }; +struct EmptyStruct {}; +EmptyStruct SharedInstance; + } // namespace TEST_SUBMODULE(thread, m) { @@ -61,6 +64,9 @@ TEST_SUBMODULE(thread, m) { }, py::call_guard<py::gil_scoped_release>()); + py::class_<EmptyStruct>(m, "EmptyStruct") + .def_readonly_static("SharedInstance", &SharedInstance); + // 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/tests/test_thread.py b/tests/test_thread.py index f12020e41..e9d7bafb2 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -47,3 +47,22 @@ def test_implicit_conversion_no_gil(): x.start() for x in [c, b, a]: x.join() + + +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") +def test_bind_shared_instance(): + nb_threads = 4 + b = threading.Barrier(nb_threads) + + def access_shared_instance(): + b.wait() + for _ in range(1000): + m.EmptyStruct.SharedInstance # noqa: B018 + + threads = [ + threading.Thread(target=access_shared_instance) for _ in range(nb_threads) + ] + for thread in threads: + thread.start() + for thread in threads: + thread.join()