diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 8e3be97dfeefd1..51c78169e414ed 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1279,6 +1279,21 @@ with sub-interpreters: Hangs the current thread, rather than terminating it, if called while the interpreter is finalizing. + +.. c:function:: int PyGILState_EnsureOrFail(PyGILState_STATE *state) + + Similar to :c:func:`PyGILState_Ensure`, except that it returns with a status + code even in the case of failure. Specifically, it returns ``0`` when the + operation succeeded, and ``-1`` otherwise. In contrast to + :c:func:`PyGILState_Ensure`, *state* is an argument. + + In the case of failure, it is *unsafe* to use the Python API following the + call. Releasing the obtained *state* via :c:func:`PyGILState_Release` should + only be done in the case of success. + + .. versionadded:: next + + .. c:function:: void PyGILState_Release(PyGILState_STATE) Release any resources previously acquired. After this call, Python's state will diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 59e7a31bc2ef06..07ca6be4567f80 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -299,6 +299,7 @@ func,PyGC_Disable,3.10,, func,PyGC_Enable,3.10,, func,PyGC_IsEnabled,3.10,, func,PyGILState_Ensure,3.2,, +func,PyGILState_EnsureOrFail,3.14,, func,PyGILState_GetThisThreadState,3.2,, func,PyGILState_Release,3.2,, type,PyGILState_STATE,3.2,, diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 59c432d30a342b..3e053c0e59b310 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1334,6 +1334,9 @@ New features and get an attribute of the module. (Contributed by Victor Stinner in :gh:`128911`.) +* Add :c:func:`PyGILState_EnsureOrFail` function: similar to + :c:func:`PyGILState_Ensure`, but return ``-1`` if the thread must exit. + (Contributed by Victor Stinner in :gh:`124622`.) Limited C API changes --------------------- diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index fea8665ae39ab5..090725e6290464 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -327,6 +327,8 @@ void _Py_unset_eval_breaker_bit_all(PyInterpreterState *interp, uintptr_t bit); PyAPI_FUNC(PyObject *) _PyFloat_FromDouble_ConsumeInputs(_PyStackRef left, _PyStackRef right, double value); +extern int _PyEval_AcquireLockOrFail(PyThreadState *tstate); +extern int _PyEval_RestoreThreadOrFail(PyThreadState *tstate); #ifdef __cplusplus } diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index ff3b222b157810..da716325a2fc02 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -313,6 +313,8 @@ _Py_AssertHoldsTstateFunc(const char *func) #define _Py_AssertHoldsTstate() #endif +extern int _PyThreadState_AttachOrFail(PyThreadState *tstate); + #ifdef __cplusplus } #endif diff --git a/Include/pystate.h b/Include/pystate.h index 727b8fbfffe0e6..fcc3f54c1077c0 100644 --- a/Include/pystate.h +++ b/Include/pystate.h @@ -111,6 +111,11 @@ PyAPI_FUNC(PyGILState_STATE) PyGILState_Ensure(void); */ PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030e0000 +/* New in 3.14 */ +PyAPI_FUNC(int) PyGILState_EnsureOrFail(PyGILState_STATE *state); +#endif + /* Helper/diagnostic function - get the current thread state for this thread. May return NULL if no GILState API has been used on the current thread. Note that the main thread always has such a diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index f3724ce6d4d15a..418c45dc120065 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -338,6 +338,7 @@ def test_windows_feature_macros(self): "PyGC_Enable", "PyGC_IsEnabled", "PyGILState_Ensure", + "PyGILState_EnsureOrFail", "PyGILState_GetThisThreadState", "PyGILState_Release", "PyGetSetDescr_Type", diff --git a/Misc/NEWS.d/next/C_API/2025-02-05-15-01-23.gh-issue-124622.jnROuN.rst b/Misc/NEWS.d/next/C_API/2025-02-05-15-01-23.gh-issue-124622.jnROuN.rst new file mode 100644 index 00000000000000..20b26dc2602494 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-02-05-15-01-23.gh-issue-124622.jnROuN.rst @@ -0,0 +1,3 @@ +Add :c:func:`PyGILState_EnsureOrFail` function: similar to +:c:func:`PyGILState_Ensure`, but return ``-1`` if the thread must exit. +Patch by Victor Stinner. diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 9317be605f0065..8ba7492b83397d 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2545,3 +2545,5 @@ added = '3.14' [function.Py_PACK_VERSION] added = '3.14' +[function.PyGILState_EnsureOrFail] + added = '3.14' diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c84646ccf03fa7..ddbf967bc1f9db 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -1275,7 +1275,9 @@ temporary_c_thread(void *data) PyThread_release_lock(test_c_thread->start_event); /* Allocate a Python thread state for this thread */ - state = PyGILState_Ensure(); + if (PyGILState_EnsureOrFail(&state) < 0) { + abort(); + } res = PyObject_CallNoArgs(test_c_thread->callback); Py_CLEAR(test_c_thread->callback); diff --git a/PC/python3dll.c b/PC/python3dll.c index 84b3c735240b73..ddfdea1dbe7645 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -297,6 +297,7 @@ EXPORT_FUNC(PyGC_Disable) EXPORT_FUNC(PyGC_Enable) EXPORT_FUNC(PyGC_IsEnabled) EXPORT_FUNC(PyGILState_Ensure) +EXPORT_FUNC(PyGILState_EnsureOrFail) EXPORT_FUNC(PyGILState_GetThisThreadState) EXPORT_FUNC(PyGILState_Release) EXPORT_FUNC(PyImport_AddModule) diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 416eec01052224..23a4289a5147f2 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -277,12 +277,15 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate, int final_release) /* Take the GIL. + Return 0 on success. + Return -1 if the thread must exit. + The function saves errno at entry and restores its value at exit. It may hang rather than return if the interpreter has been finalized. tstate must be non-NULL. */ -static void -take_gil(PyThreadState *tstate) +static int +take_gil_or_fail(PyThreadState *tstate) { int err = errno; @@ -304,7 +307,7 @@ take_gil(PyThreadState *tstate) C++. gh-87135: The best that can be done is to hang the thread as the public APIs calling this have no error reporting mechanism (!). */ - PyThread_hang_thread(); + goto tstate_must_exit; } assert(_PyThreadState_CheckConsistency(tstate)); @@ -312,7 +315,7 @@ take_gil(PyThreadState *tstate) struct _gil_runtime_state *gil = interp->ceval.gil; #ifdef Py_GIL_DISABLED if (!_Py_atomic_load_int_relaxed(&gil->enabled)) { - return; + goto done; } #endif @@ -348,9 +351,7 @@ take_gil(PyThreadState *tstate) if (drop_requested) { _Py_unset_eval_breaker_bit(holder_tstate, _PY_GIL_DROP_REQUEST_BIT); } - // gh-87135: hang the thread as *thread_exit() is not a safe - // API. It lacks stack unwind and local variable destruction. - PyThread_hang_thread(); + goto tstate_must_exit; } assert(_PyThreadState_CheckConsistency(tstate)); @@ -366,7 +367,7 @@ take_gil(PyThreadState *tstate) // return. COND_SIGNAL(gil->cond); MUTEX_UNLOCK(gil->mutex); - return; + goto done; } #endif @@ -401,7 +402,7 @@ take_gil(PyThreadState *tstate) /* tstate could be a dangling pointer, so don't pass it to drop_gil(). */ drop_gil(interp, NULL, 1); - PyThread_hang_thread(); + goto tstate_must_exit; } assert(_PyThreadState_CheckConsistency(tstate)); @@ -411,8 +412,25 @@ take_gil(PyThreadState *tstate) MUTEX_UNLOCK(gil->mutex); +#ifdef Py_GIL_DISABLED +done: +#endif errno = err; - return; + return 0; + +tstate_must_exit: + errno = err; + return -1; +} + +static void +take_gil(PyThreadState *tstate) +{ + if (take_gil_or_fail(tstate) < 0) { + // gh-87135: hang the thread as *thread_exit() is not a safe + // API. It lacks stack unwind and local variable destruction. + PyThread_hang_thread(); + } } void _PyEval_SetSwitchInterval(unsigned long microseconds) @@ -586,6 +604,13 @@ _PyEval_AcquireLock(PyThreadState *tstate) take_gil(tstate); } +int +_PyEval_AcquireLockOrFail(PyThreadState *tstate) +{ + _Py_EnsureTstateNotNULL(tstate); + return take_gil_or_fail(tstate); +} + void _PyEval_ReleaseLock(PyInterpreterState *interp, PyThreadState *tstate, @@ -641,19 +666,32 @@ PyEval_SaveThread(void) return tstate; } -void -PyEval_RestoreThread(PyThreadState *tstate) + +int +_PyEval_RestoreThreadOrFail(PyThreadState *tstate) { #ifdef MS_WINDOWS int err = GetLastError(); #endif _Py_EnsureTstateNotNULL(tstate); - _PyThreadState_Attach(tstate); + if (_PyThreadState_AttachOrFail(tstate) < 0) { + return -1; + } #ifdef MS_WINDOWS SetLastError(err); #endif + return 0; +} + + +void +PyEval_RestoreThread(PyThreadState *tstate) +{ + if (_PyEval_RestoreThreadOrFail(tstate) < 0) { + PyThread_hang_thread(); + } } diff --git a/Python/pystate.c b/Python/pystate.c index e6770ef40df740..52ab55ad6b06dd 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2060,8 +2060,8 @@ tstate_wait_attach(PyThreadState *tstate) } while (!tstate_try_attach(tstate)); } -void -_PyThreadState_Attach(PyThreadState *tstate) +int +_PyThreadState_AttachOrFail(PyThreadState *tstate) { #if defined(Py_DEBUG) // This is called from PyEval_RestoreThread(). Similar @@ -2076,7 +2076,9 @@ _PyThreadState_Attach(PyThreadState *tstate) while (1) { - _PyEval_AcquireLock(tstate); + if (_PyEval_AcquireLockOrFail(tstate) < 0) { + return -1; + } // XXX assert(tstate_is_alive(tstate)); current_fast_set(&_PyRuntime, tstate); @@ -2111,6 +2113,15 @@ _PyThreadState_Attach(PyThreadState *tstate) #if defined(Py_DEBUG) errno = err; #endif + return 0; +} + +void +_PyThreadState_Attach(PyThreadState *tstate) +{ + if (_PyThreadState_AttachOrFail(tstate) < 0) { + PyThread_hang_thread(); + } } static void @@ -2730,8 +2741,9 @@ PyGILState_Check(void) return (tstate == tcur); } -PyGILState_STATE -PyGILState_Ensure(void) + +int +PyGILState_EnsureOrFail(PyGILState_STATE *state) { _PyRuntimeState *runtime = &_PyRuntime; @@ -2770,7 +2782,10 @@ PyGILState_Ensure(void) } if (!has_gil) { - PyEval_RestoreThread(tcur); + if (_PyEval_RestoreThreadOrFail(tcur) < 0) { + *state = PyGILState_UNLOCKED; + return -1; + } } /* Update our counter in the thread-state - no need for locks: @@ -2780,9 +2795,22 @@ PyGILState_Ensure(void) */ ++tcur->gilstate_counter; - return has_gil ? PyGILState_LOCKED : PyGILState_UNLOCKED; + *state = has_gil ? PyGILState_LOCKED : PyGILState_UNLOCKED; + return 0; +} + + +PyGILState_STATE +PyGILState_Ensure(void) +{ + PyGILState_STATE state; + if (PyGILState_EnsureOrFail(&state) < 0) { + PyThread_hang_thread(); + } + return state; } + void PyGILState_Release(PyGILState_STATE oldstate) {