Skip to content

Commit c95645f

Browse files
committed
fix segfaults
1 parent 747b709 commit c95645f

File tree

1 file changed

+67
-22
lines changed

1 file changed

+67
-22
lines changed

lib/python/bind_operators.h

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -189,54 +189,87 @@ template <class RHSSetup> struct OpBinder {
189189
// work in Python (assigning return value to this). This avoids extra
190190
// copies, and additionally ensures that all references to the object keep
191191
// referencing the same object after the operation.
192-
// WARNING: It is crucial to explicitly return 'py::object &' here.
193-
// Otherwise the py::object is returned by value, which increments the
194-
// reference count, which is not only suboptimal but also incorrect since
195-
// we have released the GIL via py::gil_scoped_release.
192+
// NOTE: We do NOT use py::call_guard<py::gil_scoped_release>() here
193+
// because a.cast<T &>() must run with the Python thread state attached.
194+
// On free-threaded Python (3.13t+), PyEval_SaveThread() detaches the
195+
// thread state, and a.cast<T &>() may trigger PyCriticalSection_Begin
196+
// which requires the thread state, causing a segfault. Instead, we
197+
// extract the C++ reference first, then release the GIL only around the
198+
// pure C++ operation.
196199
c.def(
197200
"__iadd__",
198201
[](py::object &a, Other &b) -> py::object & {
199-
a.cast<T &>() += RHSSetup{}(b);
202+
auto &self = a.cast<T &>();
203+
auto &&rhs = RHSSetup{}(b);
204+
{
205+
py::gil_scoped_release release;
206+
self += rhs;
207+
}
200208
return a;
201209
},
202-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
210+
py::is_operator());
203211
c.def(
204212
"__isub__",
205213
[](py::object &a, Other &b) -> py::object & {
206-
a.cast<T &>() -= RHSSetup{}(b);
214+
auto &self = a.cast<T &>();
215+
auto &&rhs = RHSSetup{}(b);
216+
{
217+
py::gil_scoped_release release;
218+
self -= rhs;
219+
}
207220
return a;
208221
},
209-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
222+
py::is_operator());
210223
c.def(
211224
"__imul__",
212225
[](py::object &a, Other &b) -> py::object & {
213-
a.cast<T &>() *= RHSSetup{}(b);
226+
auto &self = a.cast<T &>();
227+
auto &&rhs = RHSSetup{}(b);
228+
{
229+
py::gil_scoped_release release;
230+
self *= rhs;
231+
}
214232
return a;
215233
},
216-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
234+
py::is_operator());
217235
c.def(
218236
"__itruediv__",
219237
[](py::object &a, Other &b) -> py::object & {
220-
a.cast<T &>() /= RHSSetup{}(b);
238+
auto &self = a.cast<T &>();
239+
auto &&rhs = RHSSetup{}(b);
240+
{
241+
py::gil_scoped_release release;
242+
self /= rhs;
243+
}
221244
return a;
222245
},
223-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
246+
py::is_operator());
224247
if constexpr (!(std::is_same_v<T, Dataset> ||
225248
std::is_same_v<Other, Dataset>)) {
226249
c.def(
227250
"__imod__",
228251
[](py::object &a, Other &b) -> py::object & {
229-
a.cast<T &>() %= RHSSetup{}(b);
252+
auto &self = a.cast<T &>();
253+
auto &&rhs = RHSSetup{}(b);
254+
{
255+
py::gil_scoped_release release;
256+
self %= rhs;
257+
}
230258
return a;
231259
},
232-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
260+
py::is_operator());
233261
c.def(
234262
"__ifloordiv__",
235263
[](py::object &a, Other &b) -> py::object & {
236-
floor_divide_equals(a.cast<T &>(), RHSSetup{}(b));
264+
auto &self = a.cast<T &>();
265+
auto &&rhs = RHSSetup{}(b);
266+
{
267+
py::gil_scoped_release release;
268+
floor_divide_equals(self, rhs);
269+
}
237270
return a;
238271
},
239-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
272+
py::is_operator());
240273
if constexpr (!(std::is_same_v<T, DataArray> ||
241274
std::is_same_v<Other, DataArray>)) {
242275
c.def(
@@ -419,22 +452,34 @@ void bind_logical(pybind11::class_<T, Ignored...> &c) {
419452
c.def(
420453
"__ior__",
421454
[](const py::object &a, const T2 &b) -> const py::object & {
422-
a.cast<T &>() |= b;
455+
auto &self = a.cast<T &>();
456+
{
457+
py::gil_scoped_release release;
458+
self |= b;
459+
}
423460
return a;
424461
},
425-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
462+
py::is_operator());
426463
c.def(
427464
"__ixor__",
428465
[](const py::object &a, const T2 &b) -> const py::object & {
429-
a.cast<T &>() ^= b;
466+
auto &self = a.cast<T &>();
467+
{
468+
py::gil_scoped_release release;
469+
self ^= b;
470+
}
430471
return a;
431472
},
432-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
473+
py::is_operator());
433474
c.def(
434475
"__iand__",
435476
[](const py::object &a, const T2 &b) -> const py::object & {
436-
a.cast<T &>() &= b;
477+
auto &self = a.cast<T &>();
478+
{
479+
py::gil_scoped_release release;
480+
self &= b;
481+
}
437482
return a;
438483
},
439-
py::is_operator(), py::call_guard<py::gil_scoped_release>());
484+
py::is_operator());
440485
}

0 commit comments

Comments
 (0)