Skip to content

Commit af6b16c

Browse files
committed
Fix lock discipline; use newer graalpy with fewer workarounds needed
1 parent 9222b81 commit af6b16c

6 files changed

Lines changed: 79 additions & 55 deletions

File tree

.codespell-ignore-lines

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ template <op_id id, op_type ot, typename L = undefined_t, typename R = undefined
22
template <typename ThisT>
33
auto &this_ = static_cast<ThisT &>(*this);
44
if (load_impl<ThisT>(temp, false)) {
5-
return load_impl<ThisT>(src, false);
5+
return load_impl<ThisT>(src, false);
66
ssize_t nd = 0;
77
auto trivial = broadcast(buffers, nd, shape);
88
auto ndim = (size_t) nd;

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
python-version: 'pypy-3.11-v7.3.23'
5252
cmake-args: -DCMAKE_CXX_STANDARD=17
5353
- runs-on: ubuntu-latest
54-
python-version: 'graalpy-25.0'
54+
python-version: 'graalpy-25.2.4'
5555
cmake-args: -DCMAKE_CXX_STANDARD=20
5656
- runs-on: ubuntu-latest
5757
python-version: '3.15'
@@ -112,7 +112,7 @@ jobs:
112112
- runs-on: macos-latest
113113
python-version: 'pypy-3.11-v7.3.23'
114114
- runs-on: macos-latest
115-
python-version: 'graalpy-25.0'
115+
python-version: 'graalpy-25.2.4'
116116

117117
- runs-on: windows-latest
118118
python-version: '3.9'

include/pybind11/contrib/pymetabind.h

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
* This functionality is intended to be used by the framework itself,
88
* rather than by users of the framework.
99
*
10-
* This is version 0.4 of pymetabind. Changelog:
10+
* This is version 0.4+dev of pymetabind. Changelog:
11+
*
12+
* Unreleased: Use critical section API to avoid lock ordering issues.
1113
*
1214
* Version 0.4: Properly return NULL if registry capsule creation fails.
1315
* 2026-01-23 Support concurrent calls to `pymb_get_registry`.
@@ -270,20 +272,47 @@ struct pymb_registry {
270272
#endif
271273
};
272274

273-
#if defined(Py_GIL_DISABLED)
274-
PYMB_INLINE void pymb_lock_registry(struct pymb_registry* registry) {
275-
PyMutex_Lock(&registry->mutex);
275+
#if !defined(Py_GIL_DISABLED)
276+
277+
struct pymb_lock_ticket {
278+
char unused;
279+
};
280+
PYMB_INLINE void pymb_lock_registry(struct pymb_lock_ticket* ticket,
281+
struct pymb_registry* registry) {
282+
(void) ticket;
283+
(void) registry;
284+
}
285+
PYMB_INLINE void pymb_unlock_registry(struct pymb_lock_ticket* ticket) {
286+
(void) ticket;
276287
}
277-
PYMB_INLINE void pymb_unlock_registry(struct pymb_registry* registry) {
278-
PyMutex_Unlock(&registry->mutex);
288+
289+
#elif PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 has PyCriticalSection_BeginMutex
290+
291+
struct pymb_lock_ticket {
292+
PyCriticalSection cs;
293+
};
294+
PYMB_INLINE void pymb_lock_registry(struct pymb_lock_ticket* ticket,
295+
struct pymb_registry* registry) {
296+
PyCriticalSection_BeginMutex(&ticket->cs, &registry->mutex);
279297
}
298+
PYMB_INLINE void pymb_unlock_registry(struct pymb_lock_ticket* ticket) {
299+
PyCriticalSection_End(&ticket->cs);
300+
}
301+
280302
#else
281-
PYMB_INLINE void pymb_lock_registry(struct pymb_registry* registry) {
282-
(void) registry;
303+
304+
struct pymb_lock_ticket {
305+
struct pymb_registry* registry;
306+
};
307+
PYMB_INLINE void pymb_lock_registry(struct pymb_lock_ticket* ticket,
308+
struct pymb_registry* registry) {
309+
PyMutex_Lock(&registry->mutex);
310+
ticket->registry = registry;
283311
}
284-
PYMB_INLINE void pymb_unlock_registry(struct pymb_registry* registry) {
285-
(void) registry;
312+
PYMB_INLINE void pymb_unlock_registry(struct pymb_lock_ticket* ticket) {
313+
PyMutex_Unlock(&ticket->registry->mutex);
286314
}
315+
287316
#endif
288317

289318
struct pymb_binding;
@@ -908,7 +937,9 @@ PYMB_FUNC void pymb_add_framework(struct pymb_registry* registry,
908937
framework->link.next = NULL;
909938
framework->link.prev = NULL;
910939
framework->registry = registry;
911-
pymb_lock_registry(registry);
940+
941+
struct pymb_lock_ticket ticket;
942+
pymb_lock_registry(&ticket, registry);
912943
PYMB_LIST_FOREACH(struct pymb_framework*, other, registry->frameworks) {
913944
// Intern `abi_extra` strings so they can be compared by pointer
914945
if (other->abi_extra && framework->abi_extra &&
@@ -930,7 +961,7 @@ PYMB_FUNC void pymb_add_framework(struct pymb_registry* registry,
930961
framework->add_foreign_binding(binding);
931962
}
932963
}
933-
pymb_unlock_registry(registry);
964+
pymb_unlock_registry(&ticket);
934965
}
935966

936967
/*
@@ -1036,14 +1067,15 @@ PYMB_FUNC void pymb_add_binding(struct pymb_binding* binding,
10361067
}
10371068
Py_DECREF(binding->capsule); // keep only a borrowed reference
10381069

1039-
pymb_lock_registry(registry);
1070+
struct pymb_lock_ticket ticket;
1071+
pymb_lock_registry(&ticket, registry);
10401072
pymb_list_append(&registry->bindings, &binding->link);
10411073
PYMB_LIST_FOREACH(struct pymb_framework*, other, registry->frameworks) {
10421074
if (other != binding->framework) {
10431075
other->add_foreign_binding(binding);
10441076
}
10451077
}
1046-
pymb_unlock_registry(registry);
1078+
pymb_unlock_registry(&ticket);
10471079
return;
10481080

10491081
error:
@@ -1083,11 +1115,12 @@ PYMB_FUNC void pymb_remove_binding_internal(struct pymb_binding* binding,
10831115

10841116
// Since we need to obtain it anyway, use the registry lock to serialize
10851117
// concurrent attempts to remove the same binding
1086-
pymb_lock_registry(registry);
1118+
struct pymb_lock_ticket ticket;
1119+
pymb_lock_registry(&ticket, registry);
10871120
if (!binding->capsule) {
10881121
// Binding was concurrently removed from multiple places; the first
10891122
// one to get the registry lock wins.
1090-
pymb_unlock_registry(registry);
1123+
pymb_unlock_registry(&ticket);
10911124
return;
10921125
}
10931126

@@ -1130,7 +1163,7 @@ PYMB_FUNC void pymb_remove_binding_internal(struct pymb_binding* binding,
11301163
other->remove_foreign_binding(binding);
11311164
}
11321165
}
1133-
pymb_unlock_registry(registry);
1166+
pymb_unlock_registry(&ticket);
11341167

11351168
#if !defined(Py_GIL_DISABLED)
11361169
// On GIL builds, there's no need to delay deallocation

include/pybind11/detail/foreign.h

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -684,14 +684,17 @@ inline void foreign_internals::enable_autoimport() {
684684
});
685685
if (enabled_by_us) {
686686
// Note lock order: pymb_registry lock is 'outside' our internals lock.
687-
pymb_lock_registry(registry);
687+
// (This doesn't matter after 3.14 since the critical section API fixes
688+
// lock order issues.)
689+
struct pymb_lock_ticket ticket;
690+
pymb_lock_registry(&ticket, registry);
688691
// NOLINTNEXTLINE(modernize-use-auto)
689692
PYMB_LIST_FOREACH(struct pymb_binding *, binding, registry->bindings) {
690693
if (binding->framework != self.get()) {
691694
foreign_cb_add_foreign_binding(binding);
692695
}
693696
}
694-
pymb_unlock_registry(registry);
697+
pymb_unlock_registry(&ticket);
695698
}
696699
}
697700

@@ -814,17 +817,18 @@ export_to_foreign(const std::type_info *cpptype, PyTypeObject *pytype, type_info
814817
++foreign_internals.bindings_update_count;
815818
lst.push_back(binding);
816819

817-
#ifdef Py_GIL_DISABLED
818-
// Call pymb_add_binding() with unlocked internals in order to maintain
819-
// consistent lock order: the pymb_registry lock is locked outside our
820+
#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX < 0x030E00C1
821+
// This interpreter doesn't have PyCriticalSection_BeginMutex(), so
822+
// we must ensure a consistent lock order between our internals lock
823+
// and the pymb_registry lock. The pymb_registry lock is locked outside our
820824
// internals lock in enable_autoimport(), so it must not be locked inside
821825
// our internals lock here. pymb_add_binding() is noexcept so we don't
822826
// need a scope guard.
823827
auto &internals = get_internals();
824828
internals.mutex.unlock();
825829
#endif
826830
pymb_add_binding(binding, /* tp_finalize_will_remove */ 0);
827-
#ifdef Py_GIL_DISABLED
831+
#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX < 0x030E00C1
828832
internals.mutex.lock();
829833
#endif
830834
}
@@ -881,7 +885,9 @@ PYBIND11_NOINLINE void *try_foreign_bindings(const std::type_info *type,
881885
{
882886
#ifdef Py_GIL_DISABLED
883887
// attempt() might execute Python code; drop the internals lock
884-
// to avoid a deadlock
888+
// to avoid a deadlock. This can be removed once we require
889+
// 3.14, since using PyCriticalSection for the internals lock
890+
// is already sufficient to prevent a deadlock.
885891
auto guard = lock.temporarily_drop();
886892
#endif
887893
void *result = attempt(closure, binding);

tests/test_foreign.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import collections
5+
import contextlib
56
import gc
67
import itertools
78
import sys
@@ -58,8 +59,7 @@ def delattr_and_ensure_destroyed(*specs):
5859
@pytest.fixture(autouse=True)
5960
def clean_after():
6061
yield
61-
if sys.implementation.name != "graalpy":
62-
t3.clear_foreign_bindings()
62+
t3.clear_foreign_bindings()
6363

6464
if not types_are_immortal:
6565
delattr_and_ensure_destroyed(
@@ -75,11 +75,9 @@ def clean_after():
7575
# this helps prevent different tests from interfering with each other
7676
for mod in (t1, t2, t3, t4, t5):
7777
for name in ("Shared", "SharedEnum", "RawShared"):
78-
if (
79-
(ty := getattr(mod, name, None))
80-
and hasattr(ty, "__pymetabind_binding__")
81-
):
82-
del ty.__pymetabind_binding__
78+
if ty := getattr(mod, name, None):
79+
with contextlib.suppress(AttributeError):
80+
del ty.__pymetabind_binding__
8381

8482
if sys.implementation.name in ("pypy", "graalpy"):
8583
pytest.gc_collect()
@@ -89,12 +87,9 @@ def clean_after():
8987

9088

9189
def check_stats(mod, **entries):
92-
if mod is None or sys.implementation.name == "graalpy":
93-
# graalpy seems to not do a full collection when we gc_collect(); there
94-
# will be too few destructions in one check_stats() and then correspondingly
95-
# too many in the next one for the same module, so just skip the check
90+
if mod is None:
9691
return
97-
if sys.implementation.name == "pypy":
92+
if sys.implementation.name in ("pypy", "graalpy"):
9893
pytest.gc_collect()
9994
stats = mod.pull_stats()
10095
if 0 < (move_diff := stats["move"] - entries.get("move", 0)) <= 2:
@@ -356,12 +351,10 @@ def test_import_export_errors():
356351
t3.create_raw_binding()
357352

358353
# Can't import a type that doesn't have __pymetabind_binding__
359-
try:
354+
with contextlib.suppress(AttributeError):
360355
# Convertible gets a binding because it's in an export-by-default
361356
# module, but we don't rely on that binding anywhere
362357
del t1.Convertible.__pymetabind_binding__
363-
except AttributeError:
364-
pass
365358
with pytest.raises(
366359
RuntimeError, match="type does not define a __pymetabind_binding__"
367360
):
@@ -851,12 +844,3 @@ def test_on_request_with_local_binding():
851844
# But t4 can now accept t1's types
852845
obj1 = t1.make(30)
853846
assert t4.check(obj1) == 30
854-
855-
856-
# =====================================================================
857-
# GraalPy has issues with GC-dependent tests
858-
# =====================================================================
859-
860-
if sys.implementation.name == "graalpy":
861-
del test_implicit_conversion_from_foreign
862-
del test_three_module_interop

tests/test_foreign_3.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,21 +213,22 @@ PYBIND11_MODULE(test_foreign_3, m, py::mod_gil_not_used()) {
213213

214214
m.def("clear_foreign_bindings", [hm]() {
215215
py::list bound;
216-
pymb_lock_registry(registry);
216+
struct pymb_lock_ticket ticket;
217+
pymb_lock_registry(&ticket, registry);
217218
// NOLINTNEXTLINE(modernize-use-auto)
218219
PYMB_LIST_FOREACH(struct pymb_binding *, binding, registry->bindings) {
219220
bound.append(py::reinterpret_borrow<py::object>((PyObject *) binding->pytype));
220221
}
221-
pymb_unlock_registry(registry);
222+
pymb_unlock_registry(&ticket);
222223
for (auto type : bound) {
223224
py::delattr(type, "__pymetabind_binding__");
224225
}
225226

226227
bool bindings_removed = false;
227228
for (int i = 0; i < 5; ++i) {
228-
pymb_lock_registry(registry);
229+
pymb_lock_registry(&ticket, registry);
229230
bindings_removed = pymb_list_is_empty(&registry->bindings);
230-
pymb_unlock_registry(registry);
231+
pymb_unlock_registry(&ticket);
231232
if (bindings_removed) {
232233
break;
233234
}

0 commit comments

Comments
 (0)