]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-149225: Expose Py_CriticalSection in Stable ABI (GH-149227)
authorPetr Viktorin <encukou@gmail.com>
Mon, 4 May 2026 15:32:17 +0000 (17:32 +0200)
committerGitHub <noreply@github.com>
Mon, 4 May 2026 15:32:17 +0000 (17:32 +0200)
Doc/c-api/synchronization.rst
Doc/data/stable_abi.dat
Doc/whatsnew/3.15.rst
Include/cpython/critical_section.h
Include/critical_section.h
Lib/test/test_cext/extension.c
Lib/test/test_stable_abi_ctypes.py
Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst [new file with mode: 0644]
Misc/stable_abi.toml
PC/python3dll.c

index 53c9faeae35464dcd0d772e2c7e9d8ccc3744ae4..7e9894f4d692d6b3f013a94dd0a859857afb03d0 100644 (file)
@@ -84,11 +84,6 @@ there is no :c:type:`PyObject` -- for example, when working with a C type that
 does not extend or wrap :c:type:`PyObject` but still needs to call into the C
 API in a manner that might lead to deadlocks.
 
-The functions and structs used by the macros are exposed for cases
-where C macros are not available. They should only be used as in the
-given macro expansions. Note that the sizes and contents of the structures may
-change in future Python versions.
-
 .. note::
 
    Operations that need to lock two objects at once must use
@@ -114,12 +109,15 @@ section API avoids potential deadlocks due to reentrancy and lock ordering
 by allowing the runtime to temporarily suspend the critical section if the
 code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`.
 
+.. _critical-section-macros:
+
 .. c:macro:: Py_BEGIN_CRITICAL_SECTION(op)
 
    Acquires the per-object lock for the object *op* and begins a
    critical section.
 
-   In the free-threaded build, this macro expands to::
+   In the free-threaded build, and when building for the
+   :ref:`Stable ABI <stable-abi>`, this macro expands to::
 
       {
           PyCriticalSection _py_cs;
@@ -150,7 +148,8 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`.
 
    Ends the critical section and releases the per-object lock.
 
-   In the free-threaded build, this macro expands to::
+   In the free-threaded build, and when building for the
+   :ref:`Stable ABI <stable-abi>`, this macro expands to::
 
           PyCriticalSection_End(&_py_cs);
       }
@@ -179,7 +178,8 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`.
 
    Locks the mutexes *m1* and *m2* and begins a critical section.
 
-   In the free-threaded build, this macro expands to::
+   In the free-threaded build, and when building for the
+   :ref:`Stable ABI <stable-abi>`, this macro expands to::
 
      {
           PyCriticalSection2 _py_cs2;
@@ -196,7 +196,8 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`.
 
    Ends the critical section and releases the per-object locks.
 
-   In the free-threaded build, this macro expands to::
+   In the free-threaded build, and when building for the
+   :ref:`Stable ABI <stable-abi>`, this macro expands to::
 
           PyCriticalSection2_End(&_py_cs2);
       }
@@ -205,6 +206,48 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`.
 
    .. versionadded:: 3.13
 
+Low-level critical section API
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following functions and structs are exposed for cases where C macros
+are not available.
+
+.. c:function:: void PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op)
+                void PyCriticalSection_End(PyCriticalSection *c)
+                void PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b)
+                void PyCriticalSection2_End(PyCriticalSection2 *c);
+
+   To be used only as in the macro expansions
+   listed :ref:`earlier in this section <critical-section-macros>`.
+
+   In non-:term:`free-threaded <free threading>` builds of CPython, these
+   functions do nothing.
+
+   .. versionadded:: 3.13
+
+.. c:type:: PyCriticalSection
+            PyCriticalSection2
+
+   To be used only as in the macro expansions
+   listed :ref:`earlier in this section <critical-section-macros>`.
+   Note that the contents of the structures are private and their meaning may
+   change in future Python versions.
+
+   .. versionadded:: 3.13
+
+.. c:function:: void PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m);
+                void PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2);
+
+   .. (These need to be in a separate section without a Stable ABI anotation.)
+
+   To be used only as in the macro expansions
+   listed :ref:`earlier in this section <critical-section-macros>`.
+
+   In non-:term:`free-threaded <free threading>` builds of CPython, these
+   functions do nothing.
+
+   .. versionadded:: 3.14
+
 
 Legacy locking APIs
 -------------------
index 2ef2bccf2b7728c4bb4f0ddb011fc9353c0a5900..49277b57b877d40602326fdc4aef43ce55c4d41c 100644 (file)
@@ -129,6 +129,12 @@ func,PyComplex_FromDoubles,3.2,,
 func,PyComplex_ImagAsDouble,3.2,,
 func,PyComplex_RealAsDouble,3.2,,
 data,PyComplex_Type,3.2,,
+type,PyCriticalSection,3.15,,full-abi
+type,PyCriticalSection2,3.15,,full-abi
+func,PyCriticalSection2_Begin,3.15,,
+func,PyCriticalSection2_End,3.15,,
+func,PyCriticalSection_Begin,3.15,,
+func,PyCriticalSection_End,3.15,,
 func,PyDescr_NewClassMethod,3.2,,
 func,PyDescr_NewGetSet,3.2,,
 func,PyDescr_NewMember,3.2,,
@@ -906,6 +912,8 @@ macro,Py_AUDIT_READ,3.12,,
 func,Py_AddPendingCall,3.2,,
 func,Py_AtExit,3.2,,
 macro,Py_BEGIN_ALLOW_THREADS,3.2,,
+macro,Py_BEGIN_CRITICAL_SECTION,3.15,,
+macro,Py_BEGIN_CRITICAL_SECTION2,3.15,,
 macro,Py_BLOCK_THREADS,3.2,,
 func,Py_BuildValue,3.2,,
 func,Py_BytesMain,3.8,,
@@ -913,6 +921,8 @@ func,Py_CompileString,3.2,,
 func,Py_DecRef,3.2,,
 func,Py_DecodeLocale,3.7,,
 macro,Py_END_ALLOW_THREADS,3.2,,
+macro,Py_END_CRITICAL_SECTION,3.15,,
+macro,Py_END_CRITICAL_SECTION2,3.15,,
 func,Py_EncodeLocale,3.7,,
 func,Py_EndInterpreter,3.2,,
 func,Py_EnterRecursiveCall,3.9,,
index 29576d15b3b181de580f42aea5686777aff3278b..3baae5340414469e265ece7464b508ea338bba00 100644 (file)
@@ -2098,6 +2098,11 @@ New features
 
   (Contributed by Victor Stinner in :gh:`129813`.)
 
+* :c:type:`PyCriticalSection` and related functions are added to the Stable
+  ABI.
+
+  (Contributed in :gh:`149227`.)
+
 * Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
   a module from a *spec* and *initfunc*.
   (Contributed by Itamar Oren in :gh:`116146`.)
index 4fc46fefb93a24a5f385a38f4cfe9b108263e209..bcba32da412f3264be6f247929106aaee232ba05 100644 (file)
@@ -2,15 +2,6 @@
 #  error "this header file must not be included directly"
 #endif
 
-// Python critical sections
-//
-// Conceptually, critical sections are a deadlock avoidance layer on top of
-// per-object locks. These helpers, in combination with those locks, replace
-// our usage of the global interpreter lock to provide thread-safety for
-// otherwise thread-unsafe objects, such as dict.
-//
-// NOTE: These APIs are no-ops in non-free-threaded builds.
-//
 // Straightforward per-object locking could introduce deadlocks that were not
 // present when running with the GIL. Threads may hold locks for multiple
 // objects simultaneously because Python operations can nest. If threads were
 // `_PyThreadState_Attach()`, it resumes the top-most (i.e., most recent)
 // critical section by reacquiring the associated lock or locks.  See
 // `_PyCriticalSection_Resume()`.
-//
-// NOTE: Only the top-most critical section is guaranteed to be active.
-// Operations that need to lock two objects at once must use
-// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections
-// to lock more than one object at once, because the inner critical section
-// may  suspend the outer critical sections. This API does not provide a way
-// to lock more than two objects at once (though it could be added later
-// if actually needed).
-//
-// NOTE: Critical sections implicitly behave like reentrant locks because
-// attempting to acquire the same lock will suspend any outer (earlier)
-// critical sections. However, they are less efficient for this use case than
-// purposefully designed reentrant locks.
-//
-// Example usage:
-//  Py_BEGIN_CRITICAL_SECTION(op);
-//  ...
-//  Py_END_CRITICAL_SECTION();
-//
-// To lock two objects at once:
-//  Py_BEGIN_CRITICAL_SECTION2(op1, op2);
-//  ...
-//  Py_END_CRITICAL_SECTION2();
-
-typedef struct PyCriticalSection PyCriticalSection;
-typedef struct PyCriticalSection2 PyCriticalSection2;
-
-PyAPI_FUNC(void)
-PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op);
 
 PyAPI_FUNC(void)
 PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m);
 
-PyAPI_FUNC(void)
-PyCriticalSection_End(PyCriticalSection *c);
-
-PyAPI_FUNC(void)
-PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b);
-
 PyAPI_FUNC(void)
 PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2);
 
-PyAPI_FUNC(void)
-PyCriticalSection2_End(PyCriticalSection2 *c);
-
 #ifndef Py_GIL_DISABLED
+#undef Py_BEGIN_CRITICAL_SECTION
+#undef Py_END_CRITICAL_SECTION
+#undef Py_BEGIN_CRITICAL_SECTION2
+#undef Py_END_CRITICAL_SECTION2
+
 # define Py_BEGIN_CRITICAL_SECTION(op)      \
     {
 # define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex)    \
@@ -101,54 +59,17 @@ PyCriticalSection2_End(PyCriticalSection2 *c);
     {
 # define Py_END_CRITICAL_SECTION2()         \
     }
-#else /* !Py_GIL_DISABLED */
-
-// NOTE: the contents of this struct are private and may change betweeen
-// Python releases without a deprecation period.
-struct PyCriticalSection {
-    // Tagged pointer to an outer active critical section (or 0).
-    uintptr_t _cs_prev;
-
-    // Mutex used to protect critical section
-    PyMutex *_cs_mutex;
-};
-
-// A critical section protected by two mutexes. Use
-// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2.
-// NOTE: the contents of this struct are private and may change betweeen
-// Python releases without a deprecation period.
-struct PyCriticalSection2 {
-    PyCriticalSection _cs_base;
 
-    PyMutex *_cs_mutex2;
-};
-
-# define Py_BEGIN_CRITICAL_SECTION(op)                                  \
-    {                                                                   \
-        PyCriticalSection _py_cs;                                       \
-        PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op))
+#else /* !Py_GIL_DISABLED */
 
 # define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex)                         \
     {                                                                   \
         PyCriticalSection _py_cs;                                       \
         PyCriticalSection_BeginMutex(&_py_cs, mutex)
 
-# define Py_END_CRITICAL_SECTION()                                      \
-        PyCriticalSection_End(&_py_cs);                                 \
-    }
-
-# define Py_BEGIN_CRITICAL_SECTION2(a, b)                               \
-    {                                                                   \
-        PyCriticalSection2 _py_cs2;                                     \
-        PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b))
-
 # define Py_BEGIN_CRITICAL_SECTION2_MUTEX(m1, m2)                       \
     {                                                                   \
         PyCriticalSection2 _py_cs2;                                     \
         PyCriticalSection2_BeginMutex(&_py_cs2, m1, m2)
 
-# define Py_END_CRITICAL_SECTION2()                                     \
-        PyCriticalSection2_End(&_py_cs2);                               \
-    }
-
-#endif
+#endif /* !Py_GIL_DISABLED */
index 3b37615a8b17e2a457b2043c7f24165984e8a4ef..732bfab7ecf234992ca6ec668cfd2f3e8e5d7141 100644 (file)
@@ -4,6 +4,91 @@
 extern "C" {
 #endif
 
+// Python critical sections
+//
+// Conceptually, critical sections are a deadlock avoidance layer on top of
+// per-object locks. These helpers, in combination with those locks, replace
+// our usage of the global interpreter lock to provide thread-safety for
+// otherwise thread-unsafe objects, such as dict.
+//
+// NOTE: These APIs are no-ops in non-free-threaded builds.
+//
+// NOTE: Only the top-most critical section is guaranteed to be active.
+// Operations that need to lock two objects at once must use
+// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections
+// to lock more than one object at once, because the inner critical section
+// may suspend the outer critical sections. This API does not provide a way
+// to lock more than two objects at once (though it could be added later
+// if actually needed).
+//
+// NOTE: Critical sections implicitly behave like reentrant locks because
+// attempting to acquire the same lock will suspend any outer (earlier)
+// critical sections. However, they are less efficient for this use case than
+// purposefully designed reentrant locks.
+//
+// Example usage:
+//  Py_BEGIN_CRITICAL_SECTION(op);
+//  ...
+//  Py_END_CRITICAL_SECTION();
+//
+// To lock two objects at once:
+//  Py_BEGIN_CRITICAL_SECTION2(op1, op2);
+//  ...
+//  Py_END_CRITICAL_SECTION2();
+
+// NOTE: the contents of this struct are private and their meaning may
+// change betweeen Python releases without a deprecation period.
+typedef struct PyCriticalSection {
+    // Tagged pointer to an outer active critical section (or 0).
+    uintptr_t _cs_prev;
+
+    // Mutex used to protect critical section
+    struct PyMutex *_cs_mutex;
+} PyCriticalSection;
+
+// A critical section protected by two mutexes. Use
+// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2.
+// NOTE: the contents of this struct are private and may change betweeen
+// Python releases without a deprecation period.
+typedef struct PyCriticalSection2 {
+    PyCriticalSection _cs_base;
+
+    struct PyMutex *_cs_mutex2;
+} PyCriticalSection2;
+
+PyAPI_FUNC(void)
+PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op);
+
+PyAPI_FUNC(void)
+PyCriticalSection_End(PyCriticalSection *c);
+
+PyAPI_FUNC(void)
+PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b);
+
+PyAPI_FUNC(void)
+PyCriticalSection2_End(PyCriticalSection2 *c);
+
+// These are definitions for the stable ABI. For GIL-ful builds they're
+// conditionally redefined as no-ops in cpython/critical_section.h.
+
+# define Py_BEGIN_CRITICAL_SECTION(op)                                  \
+    {                                                                   \
+        PyCriticalSection _py_cs;                                       \
+        PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op))
+
+# define Py_END_CRITICAL_SECTION()                                      \
+        PyCriticalSection_End(&_py_cs);                                 \
+    }
+
+# define Py_BEGIN_CRITICAL_SECTION2(a, b)                               \
+    {                                                                   \
+        PyCriticalSection2 _py_cs2;                                     \
+        PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b))
+
+# define Py_END_CRITICAL_SECTION2()                                     \
+        PyCriticalSection2_End(&_py_cs2);                               \
+    }
+
 #ifndef Py_LIMITED_API
 #  define Py_CPYTHON_CRITICAL_SECTION_H
 #  include "cpython/critical_section.h"
index a880cb82811f783d81e833e1ee1dc17d1e763ebd..46d00f3845eaa62665cb0e1a0bbf6aabc2d6c1b7 100644 (file)
@@ -99,6 +99,10 @@ _testcext_exec(PyObject *module)
     obj = NULL;
     Py_CLEAR(obj);
 
+    // Test that Py_BEGIN_CRITICAL_SECTION is available
+    Py_BEGIN_CRITICAL_SECTION(module);
+    Py_END_CRITICAL_SECTION();
+
     return 0;
 }
 
index 5dae5dfccac5d323052d619b90b0e897e87ad707..7b348ff298a4b25adedac6ab9f1d52e8999eaf5e 100644 (file)
@@ -134,6 +134,10 @@ SYMBOL_NAMES = (
     "PyComplex_ImagAsDouble",
     "PyComplex_RealAsDouble",
     "PyComplex_Type",
+    "PyCriticalSection2_Begin",
+    "PyCriticalSection2_End",
+    "PyCriticalSection_Begin",
+    "PyCriticalSection_End",
     "PyDescr_NewClassMethod",
     "PyDescr_NewGetSet",
     "PyDescr_NewMember",
diff --git a/Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst b/Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst
new file mode 100644 (file)
index 0000000..98d716a
--- /dev/null
@@ -0,0 +1,2 @@
+:c:type:`PyCriticalSection` and related functions are added to the Stable
+ABI.
index e6c63227d20446a2da5acb9c8c073493bbef60de..bd3ea54582580997206f7cc41ca7cef560e949ee 100644 (file)
     added = '3.15'
 [function.PyType_GetModuleByToken_DuringGC]
     added = '3.15'
+[function.PyCriticalSection_Begin]
+    added = '3.15'
+[function.PyCriticalSection_End]
+    added = '3.15'
+[struct.PyCriticalSection]
+    added = '3.15'
+    struct_abi_kind = 'full-abi'
+[macro.Py_BEGIN_CRITICAL_SECTION]
+    added = '3.15'
+[macro.Py_END_CRITICAL_SECTION]
+    added = '3.15'
+[function.PyCriticalSection2_Begin]
+    added = '3.15'
+[function.PyCriticalSection2_End]
+    added = '3.15'
+[struct.PyCriticalSection2]
+    added = '3.15'
+    struct_abi_kind = 'full-abi'
+[macro.Py_BEGIN_CRITICAL_SECTION2]
+    added = '3.15'
+[macro.Py_END_CRITICAL_SECTION2]
+    added = '3.15'
 
 # PEP 757 import/export API.
 [function.PyLong_GetNativeLayout]
index 6b9ef0a4164c168768f8c0833fb7d9d258c85af4..3303fccf0261affdf59877817f45da6feda52f5a 100755 (executable)
@@ -174,6 +174,10 @@ EXPORT_FUNC(PyCodec_XMLCharRefReplaceErrors)
 EXPORT_FUNC(PyComplex_FromDoubles)
 EXPORT_FUNC(PyComplex_ImagAsDouble)
 EXPORT_FUNC(PyComplex_RealAsDouble)
+EXPORT_FUNC(PyCriticalSection2_Begin)
+EXPORT_FUNC(PyCriticalSection2_End)
+EXPORT_FUNC(PyCriticalSection_Begin)
+EXPORT_FUNC(PyCriticalSection_End)
 EXPORT_FUNC(PyDescr_NewClassMethod)
 EXPORT_FUNC(PyDescr_NewGetSet)
 EXPORT_FUNC(PyDescr_NewMember)