diff --git a/docs/api_extra.rst b/docs/api_extra.rst index 903aa586..02582d3f 100644 --- a/docs/api_extra.rst +++ b/docs/api_extra.rst @@ -1412,6 +1412,8 @@ functions: Py_INCREF(o); }, [](PyObject * o) noexcept { + if (!nb::is_alive()) + return; nb::gil_scoped_acquire guard; Py_DECREF(o); }); @@ -1419,6 +1421,10 @@ functions: // ... } + The liveness check in the destructor is necessary in a multi-threaded + environment because when the interpreter is shutting down, trying to + (re-) take the GIL will throw a cancellation exception. + .. cpp:function:: inline void inc_ref(intrusive_base * o) noexcept Reference counting helper function that calls ``o->inc_ref()`` if ``o`` is diff --git a/docs/changelog.rst b/docs/changelog.rst index 530a6b2a..f4f51b8a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -73,6 +73,11 @@ Version TBD (not yet released) binding abstractions that "feel like" the built-in ones. (PR `#884 `__) +- Added :cpp:func:`nb::is_alive() ` checks to prevent destructors + from trying to re-acquire the GIL when the interpreter shuts down, + as that triggers a cancellation exception which isn't allowed in + destructors. + Version 2.4.0 (Dec 6, 2024) --------------------------- diff --git a/docs/ownership_adv.rst b/docs/ownership_adv.rst index bede6974..a61c2539 100644 --- a/docs/ownership_adv.rst +++ b/docs/ownership_adv.rst @@ -172,6 +172,8 @@ bindings are an optional component). Py_INCREF(o); }, [](PyObject *o) noexcept { + if (!nb::is_alive()) + return; nb::gil_scoped_acquire guard; Py_DECREF(o); }); diff --git a/include/nanobind/intrusive/counter.h b/include/nanobind/intrusive/counter.h index cb154644..1bd9738a 100644 --- a/include/nanobind/intrusive/counter.h +++ b/include/nanobind/intrusive/counter.h @@ -140,6 +140,8 @@ NAMESPACE_BEGIN(nanobind) * Py_INCREF(o); * }, * [](PyObject *o) noexcept { + * if (!nb::is_alive()) + * return; * nb::gil_scoped_acquire guard; * Py_DECREF(o); * }); diff --git a/include/nanobind/stl/function.h b/include/nanobind/stl/function.h index b9623402..f5314f6e 100644 --- a/include/nanobind/stl/function.h +++ b/include/nanobind/stl/function.h @@ -34,7 +34,7 @@ struct pyfunc_wrapper { } ~pyfunc_wrapper() { - if (f) { + if (f && is_alive()) { gil_scoped_acquire acq; Py_DECREF(f); } diff --git a/src/error.cpp b/src/error.cpp index 5d1d6666..8dafc2a7 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -28,7 +28,7 @@ python_error::python_error() { } python_error::~python_error() { - if (m_value) { + if (m_value && detail::is_alive()) { gil_scoped_acquire acq; /* With GIL held */ { // Clear error status in case the following executes Python code @@ -73,7 +73,7 @@ python_error::python_error() { } python_error::~python_error() { - if (m_type) { + if (m_type && detail::is_alive()) { gil_scoped_acquire acq; /* With GIL held */ { // Clear error status in case the following executes Python code diff --git a/src/nb_ndarray.cpp b/src/nb_ndarray.cpp index 6cbb9e19..b8054c2c 100644 --- a/src/nb_ndarray.cpp +++ b/src/nb_ndarray.cpp @@ -293,6 +293,8 @@ static PyObject *dlpack_from_buffer_protocol(PyObject *o, bool ro) { } mt->deleter = [](managed_dltensor *mt2) { + if (!is_alive()) + return; gil_scoped_acquire guard; Py_buffer *buf = (Py_buffer *) mt2->manager_ctx; PyBuffer_Release(buf); @@ -617,7 +619,7 @@ void ndarray_dec_ref(ndarray_handle *th) noexcept { if (rc_value == 0) { check(false, "ndarray_dec_ref(): reference count became negative!"); - } else if (rc_value == 1) { + } else if (rc_value == 1 && is_alive()) { gil_scoped_acquire guard; Py_XDECREF(th->owner); @@ -662,6 +664,8 @@ ndarray_handle *ndarray_create(void *value, size_t ndim, const size_t *shape_in, scoped_pymalloc shape(ndim), strides(ndim); auto deleter = [](managed_dltensor *mt) { + if (!is_alive()) + return; gil_scoped_acquire guard; ndarray_handle *th = (ndarray_handle *) mt->manager_ctx; ndarray_dec_ref(th);