]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-137210: Add a struct, slot & function for checking an extension's ABI (GH-137212)
authorPetr Viktorin <encukou@gmail.com>
Fri, 5 Sep 2025 14:23:18 +0000 (16:23 +0200)
committerGitHub <noreply@github.com>
Fri, 5 Sep 2025 14:23:18 +0000 (16:23 +0200)
Co-authored-by: Steve Dower <steve.dower@microsoft.com>
24 files changed:
Doc/c-api/module.rst
Doc/c-api/stable.rst
Doc/data/stable_abi.dat
Doc/using/configure.rst
Doc/whatsnew/3.15.rst
Include/cpython/modsupport.h
Include/internal/pycore_unicodeobject.h
Include/modsupport.h
Include/moduleobject.h
Lib/test/test_capi/test_modsupport.py [new file with mode: 0644]
Lib/test/test_stable_abi_ctypes.py
Misc/NEWS.d/next/C_API/2025-07-29-18-00-22.gh-issue-137210.DD4VEm.rst [new file with mode: 0644]
Misc/stable_abi.toml
Modules/Setup.stdlib.in
Modules/_datetimemodule.c
Modules/_testcapi/modsupport.c [new file with mode: 0644]
Modules/_testcapi/parts.h
Modules/_testcapimodule.c
Objects/moduleobject.c
Objects/unicodeobject.c
PC/python3dll.c
PCbuild/_testcapi.vcxproj
PCbuild/_testcapi.vcxproj.filters
Python/modsupport.c

index c8edcecc5b419f33bf7bf0c657d1f9a7ea457bb7..29d5cd8d5391a392b078bdd2c355d92e128ed8ca 100644 (file)
@@ -388,6 +388,28 @@ The available slot types are:
 
    .. versionadded:: 3.13
 
+.. c:macro:: Py_mod_abi
+
+   A pointer to a :c:struct:`PyABIInfo` structure that describes the ABI that
+   the extension is using.
+
+   When the module is loaded, the :c:struct:`!PyABIInfo` in this slot is checked
+   using :c:func:`PyABIInfo_Check`.
+
+   A suitable :c:struct:`!PyABIInfo` variable can be defined using the
+   :c:macro:`PyABIInfo_VAR` macro, as in:
+
+   .. code-block:: c
+
+      PyABIInfo_VAR(abi_info);
+
+      static PyModuleDef_Slot mymodule_slots[] = {
+         {Py_mod_abi, &abi_info},
+         ...
+      };
+
+   .. versionadded:: 3.15
+
 
 .. _moduledef-dynamic:
 
index 9b65e0b8d23d93f299bd7122272d052ed09d212c..8fed4b69b25dd8ca0ebcdb299b24d3994208245d 100644 (file)
@@ -2,9 +2,9 @@
 
 .. _stable:
 
-***************
-C API Stability
-***************
+***********************
+C API and ABI Stability
+***********************
 
 Unless documented otherwise, Python's C API is covered by the Backwards
 Compatibility Policy, :pep:`387`.
@@ -199,6 +199,162 @@ This is the case with Windows and macOS releases from ``python.org`` and many
 third-party distributors.
 
 
+ABI Checking
+============
+
+.. versionadded:: next
+
+Python includes a rudimentary check for ABI compatibility.
+
+This check is not comprehensive.
+It only guards against common cases of incompatible modules being
+installed for the wrong interpreter.
+It also does not take :ref:`platform incompatibilities <stable-abi-platform>`
+into account.
+It can only be done after an extension is successfully loaded.
+
+Despite these limitations, it is recommended that extension modules use this
+mechanism, so that detectable incompatibilities raise exceptions rather than
+crash.
+
+Most modules can use this check via the :c:data:`Py_mod_abi`
+slot and the :c:macro:`PyABIInfo_VAR` macro, for example like this:
+
+.. code-block:: c
+
+   PyABIInfo_VAR(abi_info);
+
+   static PyModuleDef_Slot mymodule_slots[] = {
+      {Py_mod_abi, &abi_info},
+      ...
+   };
+
+
+The full API is described below for advanced use cases.
+
+.. c:function:: int PyABIInfo_Check(PyABIInfo *info, const char *module_name)
+
+   Verify that the given *info* is compatible with the currently running
+   interpreter.
+
+   Return 0 on success. On failure, raise an exception and return -1.
+
+   If the ABI is incompatible, the raised exception will be :py:exc:`ImportError`.
+
+   The *module_name* argument can be ``NULL``, or point to a NUL-terminated
+   UTF-8-encoded string used for error messages.
+
+   Note that if *info* describes the ABI that the current code uses (as defined
+   by :c:macro:`PyABIInfo_VAR`, for example), using any other Python C API
+   may lead to crashes.
+   In particular, it is not safe to examine the raised exception.
+
+   .. versionadded:: next
+
+.. c:macro:: PyABIInfo_VAR(NAME)
+
+   Define a static :c:struct:`PyABIInfo` variable with the given *NAME* that
+   describes the ABI that the current code will use.
+   This macro expands to:
+
+   .. code-block:: c
+
+      static PyABIInfo NAME = {
+          1, 0,
+          PyABIInfo_DEFAULT_FLAGS,
+          PY_VERSION_HEX,
+          PyABIInfo_DEFAULT_ABI_VERSION
+      }
+
+   .. versionadded:: next
+
+.. c:type:: PyABIInfo
+
+   .. c:member:: uint8_t abiinfo_major_version
+
+      The major version of :c:struct:`PyABIInfo`. Can be set to:
+
+      * ``0`` to skip all checking, or
+      * ``1`` to specify this version of :c:struct:`!PyABIInfo`.
+
+   .. c:member:: uint8_t abiinfo_minor_version
+
+      The major version of :c:struct:`PyABIInfo`.
+      Must be set to ``0``; larger values are reserved for backwards-compatible
+      future versions of :c:struct:`!PyABIInfo`.
+
+   .. c:member:: uint16_t flags
+
+      .. c:namespace:: NULL
+
+      This field is usually set to the following macro:
+
+      .. c:macro:: PyABIInfo_DEFAULT_FLAGS
+
+         Default flags, based on current values of macros such as
+         :c:macro:`Py_LIMITED_API` and :c:macro:`Py_GIL_DISABLED`.
+
+      Alternately, the field can be set to to the following flags, combined
+      by bitwise OR.
+      Unused bits must be set to zero.
+
+      ABI variant -- one of:
+
+         .. c:macro:: PyABIInfo_STABLE
+
+            Specifies that the stable ABI is used.
+
+         .. c:macro:: PyABIInfo_INTERNAL
+
+            Specifies ABI specific to a particular build of CPython.
+            Internal use only.
+
+      Free-threading compatibility -- one of:
+
+         .. c:macro:: PyABIInfo_FREETHREADED
+
+            Specifies ABI compatible with free-threading builds of CPython.
+            (That is, ones compiled with :option:`--disable-gil`; with ``t``
+            in :py:data:`sys.abiflags`)
+
+         .. c:macro:: PyABIInfo_GIL
+
+            Specifies ABI compatible with non-free-threading builds of CPython
+            (ones compiled *without* :option:`--disable-gil`).
+
+   .. c:member:: uint32_t build_version
+
+      The version of the Python headers used to build the code, in the format
+      used by :c:macro:`PY_VERSION_HEX`.
+
+      This can be set to ``0`` to skip any checks related to this field.
+      This option is meant mainly for projects that do not use the CPython
+      headers directly, and do not emulate a specific version of them.
+
+   .. c:member:: uint32_t abi_version
+
+      The ABI version.
+
+      For the Stable ABI, this field should be the value of
+      :c:macro:`Py_LIMITED_API`
+      (except if :c:macro:`Py_LIMITED_API` is ``3``; use
+      :c:expr:`Py_PACK_VERSION(3, 2)` in that case).
+
+      Otherwise, it should be set to :c:macro:`PY_VERSION_HEX`.
+
+      It can also be set to ``0`` to skip any checks related to this field.
+
+      .. c:namespace:: NULL
+
+      .. c:macro:: PyABIInfo_DEFAULT_ABI_VERSION
+
+         The value that should be used for this field, based on current
+         values of macros such as :c:macro:`Py_LIMITED_API`,
+         :c:macro:`PY_VERSION_HEX` and :c:macro:`Py_GIL_DISABLED`.
+
+   .. versionadded:: next
+
+
 .. _limited-api-list:
 
 Contents of Limited API
index 0d0dfb3843260e524ec02fa670bc30eaf4a9362e..641f7bb380447158b961cafe9d8e7139ef5d1727 100644 (file)
@@ -1,5 +1,8 @@
 role,name,added,ifdef_note,struct_abi_kind
 macro,PY_VECTORCALL_ARGUMENTS_OFFSET,3.12,,
+type,PyABIInfo,3.15,,full-abi
+func,PyABIInfo_Check,3.15,,
+macro,PyABIInfo_VAR,3.15,,
 func,PyAIter_Check,3.10,,
 func,PyArg_Parse,3.2,,
 func,PyArg_ParseTuple,3.2,,
index 2cda9587975ddcd8a70c7fe3391d966b456be313..6eb5c243b873c953e9eb0fe3d77d57af5cc273e7 100644 (file)
@@ -293,6 +293,9 @@ General Options
 
 .. option:: --disable-gil
 
+   .. c:macro:: Py_GIL_DISABLED
+      :no-typesetting:
+
    Enables support for running Python without the :term:`global interpreter
    lock` (GIL): free threading build.
 
index 61203686326c392e32dfe24382062778d1797dbd..351ba0cd2afdfe12fb52c68333b92808b89d13da 100644 (file)
@@ -670,6 +670,11 @@ New features
   a string. See the documentation for caveats.
   (Contributed by Petr Viktorin in :gh:`131510`)
 
+* Add API for checking an extension module's ABI compatibility:
+  :c:data:`Py_mod_abi`, :c:func:`PyABIInfo_Check`, :c:macro:`PyABIInfo_VAR`
+  and :c:data:`Py_mod_abi`.
+  (Contributed by Petr Viktorin in :gh:`137210`)
+
 
 Porting to Python 3.15
 ----------------------
index d3b88f58c82ca3e923cb7756871113f1dfcafec7..6134442106474fda96105d12846f7815decb2f5d 100644 (file)
@@ -24,3 +24,15 @@ typedef struct _PyArg_Parser {
 
 PyAPI_FUNC(int) _PyArg_ParseTupleAndKeywordsFast(PyObject *, PyObject *,
                                                  struct _PyArg_Parser *, ...);
+
+#ifdef Py_BUILD_CORE
+// Internal; defined here to avoid explicitly including pycore_modsupport.h
+#define _Py_INTERNAL_ABI_SLOT                             \
+    {Py_mod_abi, (void*) &(PyABIInfo) {                   \
+        .abiinfo_major_version = 1,                       \
+        .abiinfo_minor_version = 0,                       \
+        .flags = PyABIInfo_INTERNAL,                      \
+        .build_version = PY_VERSION_HEX,                  \
+        .abi_version = PY_VERSION_HEX }}                  \
+    ///////////////////////////////////////////////////////
+#endif
index 3791b913c175467623a1946d9f9a0b3c1db4263d..8dfcaedd5ef2e8341ec253eeb2846a8d38ad97a9 100644 (file)
@@ -82,6 +82,12 @@ extern int _PyUnicode_FormatAdvancedWriter(
     Py_ssize_t start,
     Py_ssize_t end);
 
+/* PyUnicodeWriter_Format, with va_list instead of `...` */
+extern int _PyUnicodeWriter_FormatV(
+    PyUnicodeWriter *writer,
+    const char *format,
+    va_list vargs);
+
 /* --- UTF-7 Codecs ------------------------------------------------------- */
 
 extern PyObject* _PyUnicode_EncodeUTF7(
index af995f567b004c9a288f57118e16dab74be47878..45aea017dfe83855fddd9e0cbdfb54ab86ccead2 100644 (file)
@@ -134,6 +134,72 @@ PyAPI_FUNC(PyObject *) PyModule_FromDefAndSpec2(PyModuleDef *def,
 
 #endif /* New in 3.5 */
 
+/* ABI info & checking (new in 3.15) */
+#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030f0000
+typedef struct PyABIInfo {
+    uint8_t abiinfo_major_version;
+    uint8_t abiinfo_minor_version;
+    uint16_t flags;
+    uint32_t build_version;
+    uint32_t abi_version;
+} PyABIInfo;
+#define PyABIInfo_STABLE        0x0001
+#define PyABIInfo_GIL           0x0002
+#define PyABIInfo_FREETHREADED  0x0004
+#define PyABIInfo_INTERNAL      0x0008
+
+#define PyABIInfo_FREETHREADING_AGNOSTIC (PyABIInfo_GIL|PyABIInfo_FREETHREADED)
+
+PyAPI_FUNC(int) PyABIInfo_Check(PyABIInfo *info, const char *module_name);
+
+// Define the defaults
+#ifdef Py_LIMITED_API
+    #define _PyABIInfo_DEFAULT_FLAG_STABLE PyABIInfo_STABLE
+    #if Py_LIMITED_API == 3
+        #define PyABIInfo_DEFAULT_ABI_VERSION _Py_PACK_VERSION(3, 2)
+    #else
+        #define PyABIInfo_DEFAULT_ABI_VERSION Py_LIMITED_API
+    #endif
+#else
+    #define _PyABIInfo_DEFAULT_FLAG_STABLE 0
+    #define PyABIInfo_DEFAULT_ABI_VERSION PY_VERSION_HEX
+#endif
+#if defined(Py_LIMITED_API) && defined(_Py_OPAQUE_PYOBJECT)
+    #define _PyABIInfo_DEFAULT_FLAG_FT PyABIInfo_FREETHREADING_AGNOSTIC
+#elif defined(Py_GIL_DISABLED)
+    #define _PyABIInfo_DEFAULT_FLAG_FT PyABIInfo_FREETHREADED
+#else
+    #define _PyABIInfo_DEFAULT_FLAG_FT PyABIInfo_GIL
+#endif
+#if defined(Py_BUILD_CORE)
+    #define _PyABIInfo_DEFAULT_FLAG_INTERNAL PyABIInfo_INTERNAL
+#else
+    #define _PyABIInfo_DEFAULT_FLAG_INTERNAL 0
+#endif
+
+#define PyABIInfo_DEFAULT_FLAGS (                           \
+        _PyABIInfo_DEFAULT_FLAG_STABLE                      \
+        | _PyABIInfo_DEFAULT_FLAG_FT                        \
+        | _PyABIInfo_DEFAULT_FLAG_INTERNAL                  \
+    )                                                       \
+    /////////////////////////////////////////////////////////
+
+#define _PyABIInfo_DEFAULT() {                              \
+        1, 0,                                               \
+        PyABIInfo_DEFAULT_FLAGS,                            \
+        PY_VERSION_HEX,                                     \
+        PyABIInfo_DEFAULT_ABI_VERSION }                     \
+    /////////////////////////////////////////////////////////
+
+#define PyABIInfo_VAR(NAME) \
+    static PyABIInfo NAME = _PyABIInfo_DEFAULT;
+
+#undef _PyABIInfo_DEFAULT_STABLE
+#undef _PyABIInfo_DEFAULT_FT
+#undef _PyABIInfo_DEFAULT_INTERNAL
+
+#endif /* ABI info (new in 3.15) */
+
 #ifndef Py_LIMITED_API
 #  define Py_CPYTHON_MODSUPPORT_H
 #  include "cpython/modsupport.h"
index 17634a93f8fa6f563dfd0da3bee3aabd30bf2047..e3afac0a343be1b2f647f71e7151b0c42869ea15 100644 (file)
@@ -81,10 +81,13 @@ struct PyModuleDef_Slot {
 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
 #  define Py_mod_gil 4
 #endif
+#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15)
+#  define Py_mod_abi 5
+#endif
 
 
 #ifndef Py_LIMITED_API
-#define _Py_mod_LAST_SLOT 4
+#define _Py_mod_LAST_SLOT 5
 #endif
 
 #endif /* New in 3.5 */
diff --git a/Lib/test/test_capi/test_modsupport.py b/Lib/test/test_capi/test_modsupport.py
new file mode 100644 (file)
index 0000000..1520489
--- /dev/null
@@ -0,0 +1,154 @@
+import sys
+import unittest
+import sysconfig
+
+from test.support import subTests
+from test.support import import_helper
+
+_testcapi = import_helper.import_module('_testcapi')
+
+
+class Test_ABIInfo_Check(unittest.TestCase):
+    @subTests('modname', (None, 'test_mod'))
+    def test_zero(self, modname):
+        _testcapi.pyabiinfo_check(modname, 0, 0, 0, 0, 0)
+        _testcapi.pyabiinfo_check(modname, 1, 0, 0, 0, 0)
+
+    def test_large_major_version(self):
+        with self.assertRaisesRegex(ImportError,
+                                    '^PyABIInfo version too high$'):
+            _testcapi.pyabiinfo_check(None, 2, 0, 0, 0, 0)
+        with self.assertRaisesRegex(ImportError,
+                                    '^test_mod: PyABIInfo version too high$'):
+            _testcapi.pyabiinfo_check("test_mod", 2, 0, 0, 0, 0)
+
+    @subTests('modname', (None, 'test_mod'))
+    def test_large_minor_version(self, modname):
+        _testcapi.pyabiinfo_check(modname, 1, 2, 0, 0, 0)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('major', (0, 1))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    def test_positive_regular(self, modname, major, minor, build):
+        ver = sys.hexversion
+        truncated = ver & 0xffff0000
+        filled = truncated | 0x12b8
+        maxed = truncated | 0xffff
+        for abi_version in (0, ver, truncated, filled, maxed):
+            with self.subTest(abi_version=abi_version):
+                _testcapi.pyabiinfo_check(modname, major, minor, 0,
+                                          build, abi_version)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    @subTests('offset', (+0x00010000, -0x00010000))
+    def test_negative_regular(self, modname, minor, build, offset):
+        ver = sys.hexversion + offset
+        truncated = ver & 0xffff0000
+        filled = truncated | 0x12b8
+        maxed = truncated | 0xffff
+        for abi_version in (ver, truncated, filled, maxed):
+            with self.subTest(abi_version=abi_version):
+                with self.assertRaisesRegex(
+                        ImportError,
+                        r'incompatible ABI version \(3\.\d+\)$'):
+                    _testcapi.pyabiinfo_check(modname, 1, minor, 0,
+                                              build,
+                                              abi_version)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('major', (0, 1))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    @subTests('abi_version', (
+        0,
+        0x03020000,
+        sys.hexversion,
+        sys.hexversion & 0xffff0000,
+        sys.hexversion - 0x00010000,
+    ))
+    def test_positive_stable(self, modname, major, minor, build, abi_version):
+        _testcapi.pyabiinfo_check(modname, major, minor,
+                                  _testcapi.PyABIInfo_STABLE,
+                                  build,
+                                  abi_version)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    @subTests('abi_version_and_msg', (
+        (1, 'invalid'),
+        (3, 'invalid'),
+        (0x0301ffff, 'invalid'),
+        ((sys.hexversion & 0xffff0000) + 0x00010000, 'incompatible future'),
+        (sys.hexversion + 0x00010000, 'incompatible future'),
+        (0x04000000, 'incompatible future'),
+    ))
+    def test_negative_stable(self, modname, minor, build, abi_version_and_msg):
+        abi_version, msg = abi_version_and_msg
+        with self.assertRaisesRegex(
+                ImportError,
+                rf'{msg} stable ABI version \(\d+\.\d+\)$'):
+            _testcapi.pyabiinfo_check(modname, 1, minor,
+                                      _testcapi.PyABIInfo_STABLE,
+                                      build,
+                                      abi_version)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('major', (0, 1))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    @subTests('abi_version', (0, sys.hexversion))
+    def test_positive_internal(self, modname, major, minor, build, abi_version):
+        _testcapi.pyabiinfo_check(modname, major, minor,
+                                  _testcapi.PyABIInfo_INTERNAL,
+                                  build,
+                                  abi_version)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    @subTests('abi_version', (
+        sys.hexversion - 0x00010000,
+        sys.hexversion - 1,
+        sys.hexversion + 1,
+        sys.hexversion + 0x00010000,
+    ))
+    def test_negative_internal(self, modname, minor, build, abi_version):
+        with self.assertRaisesRegex(
+                ImportError,
+                r'incompatible internal ABI \(0x[\da-f]+ != 0x[\da-f]+\)$'):
+            _testcapi.pyabiinfo_check(modname, 1, minor,
+                                      _testcapi.PyABIInfo_INTERNAL,
+                                      build,
+                                      abi_version)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    @subTests('ft_flag', (
+        0,
+        (_testcapi.PyABIInfo_FREETHREADED
+            if sysconfig.get_config_var("Py_GIL_DISABLED")
+            else _testcapi.PyABIInfo_GIL),
+        _testcapi.PyABIInfo_FREETHREADING_AGNOSTIC,
+    ))
+    def test_positive_freethreading(self, modname, minor, build, ft_flag):
+        self.assertEqual(ft_flag & _testcapi.PyABIInfo_FREETHREADING_AGNOSTIC,
+                         ft_flag)
+        _testcapi.pyabiinfo_check(modname, 1, minor, ft_flag, build, 0)
+
+    @subTests('modname', (None, 'test_mod'))
+    @subTests('minor', (0, 1, 9))
+    @subTests('build', (0, sys.hexversion))
+    def test_negative_freethreading(self, modname, minor, build):
+        if sysconfig.get_config_var("Py_GIL_DISABLED"):
+            ft_flag = _testcapi.PyABIInfo_GIL
+            msg = "incompatible with free-threaded CPython"
+        else:
+            ft_flag = _testcapi.PyABIInfo_FREETHREADED
+            msg = "only compatible with free-threaded CPython"
+        with self.assertRaisesRegex(ImportError, msg):
+            _testcapi.pyabiinfo_check(modname, 1, minor, ft_flag, build, 0)
index 5a6ba9de337904a157e12629bef0d62dff39c1f6..cbec7e43a7c9fbd8ac3a222f23133b0752820eb1 100644 (file)
@@ -45,6 +45,7 @@ class TestStableABIAvailability(unittest.TestCase):
 
 SYMBOL_NAMES = (
 
+    "PyABIInfo_Check",
     "PyAIter_Check",
     "PyArg_Parse",
     "PyArg_ParseTuple",
diff --git a/Misc/NEWS.d/next/C_API/2025-07-29-18-00-22.gh-issue-137210.DD4VEm.rst b/Misc/NEWS.d/next/C_API/2025-07-29-18-00-22.gh-issue-137210.DD4VEm.rst
new file mode 100644 (file)
index 0000000..0f7336e
--- /dev/null
@@ -0,0 +1,3 @@
+Add API for checking an extension module's ABI compatibility:
+:c:data:`Py_mod_abi`, :c:func:`PyABIInfo_Check`, :c:macro:`PyABIInfo_VAR`
+and :c:data:`Py_mod_abi`.
index 1f323cc03973e5827284cf104aa3dc01a319fc0d..bc0fab4852811510eabe6b09df83ec38d2cb1ffd 100644 (file)
     added = '3.14'
 [function.Py_PACK_VERSION]
     added = '3.14'
+
 [function.PySys_GetAttr]
     added = '3.15'
 [function.PySys_GetAttrString]
     added = '3.15'
 [function.PySys_GetOptionalAttrString]
     added = '3.15'
+[function.PyABIInfo_Check]
+    added = '3.15'
+[macro.PyABIInfo_VAR]
+    added = '3.15'
+[struct.PyABIInfo]
+    added = '3.15'
+    struct_abi_kind = 'full-abi'
+[const.Py_mod_abi]
+    added = '3.15'
+[const.PyABIInfo_DEFAULT_ABI_VERSION]
+    added = '3.15'
+[const.PyABIInfo_DEFAULT_FLAGS]
+    added = '3.15'
+[const.PyABIInfo_STABLE]
+    added = '3.15'
+[const.PyABIInfo_FREETHREADED]
+    added = '3.15'
+[const.PyABIInfo_GIL]
+    added = '3.15'
+[const.PyABIInfo_FREETHREADING_AGNOSTIC]
+    added = '3.15'
index 7f4c4a806737acbe7a998c087e3e16a96496f867..5365e68101cf4c76234adedddbfc083e2020d1a0 100644 (file)
 @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
 @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
 @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c
-@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c
+@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c
 @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c
 @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
 @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
index eb52eb72614fa549a8582e3bbc789233a0d80668..8f1ddf330f3940c7ed9cefce92e51268dfc1a08b 100644 (file)
@@ -7616,6 +7616,7 @@ finally:
 }
 
 static PyModuleDef_Slot module_slots[] = {
+    _Py_INTERNAL_ABI_SLOT,
     {Py_mod_exec, _datetime_exec},
     {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
     {Py_mod_gil, Py_MOD_GIL_NOT_USED},
diff --git a/Modules/_testcapi/modsupport.c b/Modules/_testcapi/modsupport.c
new file mode 100644 (file)
index 0000000..6746eb9
--- /dev/null
@@ -0,0 +1,55 @@
+#include "parts.h"
+
+
+
+static PyObject *
+pyabiinfo_check(PyObject *Py_UNUSED(module), PyObject *args)
+{
+    const char *modname;
+    unsigned long maj, min, flags, buildver, abiver;
+
+    if (!PyArg_ParseTuple(args, "zkkkkk",
+        &modname, &maj, &min, &flags, &buildver, &abiver))
+    {
+        return NULL;
+    }
+    PyABIInfo info = {
+        .abiinfo_major_version = (uint8_t)maj,
+        .abiinfo_minor_version = (uint8_t)min,
+        .flags = (uint16_t)flags,
+        .build_version = (uint32_t)buildver,
+        .abi_version = (uint32_t)abiver};
+    if (PyABIInfo_Check(&info, modname) < 0) {
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+static PyMethodDef TestMethods[] = {
+    {"pyabiinfo_check", pyabiinfo_check, METH_VARARGS},
+    {NULL},
+};
+
+int
+_PyTestCapi_Init_Modsupport(PyObject *m)
+{
+    if (PyModule_AddIntMacro(m, PyABIInfo_STABLE) < 0) {
+        return -1;
+    }
+    if (PyModule_AddIntMacro(m, PyABIInfo_INTERNAL) < 0) {
+        return -1;
+    }
+    if (PyModule_AddIntMacro(m, PyABIInfo_GIL) < 0) {
+        return -1;
+    }
+    if (PyModule_AddIntMacro(m, PyABIInfo_FREETHREADED) < 0) {
+        return -1;
+    }
+    if (PyModule_AddIntMacro(m, PyABIInfo_FREETHREADING_AGNOSTIC) < 0) {
+        return -1;
+    }
+    if (PyModule_AddFunctions(m, TestMethods) < 0) {
+        return -1;
+    }
+    return 0;
+}
index af6400162daf2b4f6b661e5361a4e5549cec4de3..32915d04bd3635c71f0614c472ed9cad3544c501 100644 (file)
@@ -58,6 +58,7 @@ int _PyTestCapi_Init_Immortal(PyObject *module);
 int _PyTestCapi_Init_GC(PyObject *module);
 int _PyTestCapi_Init_Hash(PyObject *module);
 int _PyTestCapi_Init_Time(PyObject *module);
+int _PyTestCapi_Init_Modsupport(PyObject *module);
 int _PyTestCapi_Init_Monitoring(PyObject *module);
 int _PyTestCapi_Init_Object(PyObject *module);
 int _PyTestCapi_Init_Config(PyObject *mod);
index d0c0b45c20cb36c49ada3a4e9c1b97aa7c5aca95..4f22a70802009a497474d2dcbc813e18536c1549 100644 (file)
@@ -3465,6 +3465,9 @@ PyInit__testcapi(void)
     if (_PyTestCapi_Init_Time(m) < 0) {
         return NULL;
     }
+    if (_PyTestCapi_Init_Modsupport(m) < 0) {
+        return NULL;
+    }
     if (_PyTestCapi_Init_Monitoring(m) < 0) {
         return NULL;
     }
index 47681e4251849c04572852219d2850ee08284924..8e22ee68a3e0fd19cef456e92fe3e0babdcc7c72 100644 (file)
@@ -340,6 +340,11 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio
                 gil_slot = cur_slot->value;
                 has_gil_slot = 1;
                 break;
+            case Py_mod_abi:
+                if (PyABIInfo_Check((PyABIInfo *)cur_slot->value, name) < 0) {
+                    goto error;
+                }
+                break;
             default:
                 assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT);
                 PyErr_Format(
@@ -514,6 +519,7 @@ PyModule_ExecDef(PyObject *module, PyModuleDef *def)
                 break;
             case Py_mod_multiple_interpreters:
             case Py_mod_gil:
+            case Py_mod_abi:
                 /* handled in PyModule_FromDefAndSpec2 */
                 break;
             default:
index 9300a99a72144d9c7a07d0ac0549167c085e4e84..4c88e4c1fdca2eb116dc61a7aa969bd6afd2f8a0 100644 (file)
@@ -3263,14 +3263,22 @@ PyUnicode_FromFormat(const char *format, ...)
 
 int
 PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...)
+{
+    va_list vargs;
+    va_start(vargs, format);
+    int res = _PyUnicodeWriter_FormatV(writer, format, vargs);
+    va_end(vargs);
+    return res;
+}
+
+int
+_PyUnicodeWriter_FormatV(PyUnicodeWriter *writer, const char *format,
+                         va_list vargs)
 {
     _PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
     Py_ssize_t old_pos = _writer->pos;
 
-    va_list vargs;
-    va_start(vargs, format);
     int res = unicode_from_format(_writer, format, vargs);
-    va_end(vargs);
 
     if (res < 0) {
         _writer->pos = old_pos;
index 8ec791f8280f139dd001869049c0544113ccf8d9..05c86e6d5924d48d65d858e0e552445dcbafad46 100755 (executable)
@@ -93,6 +93,7 @@ EXPORT_FUNC(Py_SetRecursionLimit)
 EXPORT_FUNC(Py_TYPE)
 EXPORT_FUNC(Py_VaBuildValue)
 EXPORT_FUNC(Py_XNewRef)
+EXPORT_FUNC(PyABIInfo_Check)
 EXPORT_FUNC(PyAIter_Check)
 EXPORT_FUNC(PyArg_Parse)
 EXPORT_FUNC(PyArg_ParseTuple)
index a68f15d25aabb76e881beedf4d03e8f5d5115906..a355a5fc25707a40323d1bf418c3d49659a3ada3 100644 (file)
     <ClCompile Include="..\Modules\_testcapi\immortal.c" />
     <ClCompile Include="..\Modules\_testcapi\gc.c" />
     <ClCompile Include="..\Modules\_testcapi\run.c" />
+    <ClCompile Include="..\Modules\_testcapi\modsupport.c" />
     <ClCompile Include="..\Modules\_testcapi\monitoring.c" />
     <ClCompile Include="..\Modules\_testcapi\config.c" />
     <ClCompile Include="..\Modules\_testcapi\import.c" />
index 21091e9dc1aa16605f6a960517cc80fe85822de9..05128d3ac36efccfbd865edcb8fa1f7260271358 100644 (file)
     <ClCompile Include="..\Modules\_testcapi\run.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Modules\_testcapi\modsupport.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Modules\_testcapi\monitoring.c">
       <Filter>Source Files</Filter>
     </ClCompile>
index 17b559e57fa1e794a4cd2af7f40ae75420773dc6..239c6c6a1b3bfa00a348ea7112bc319714ce87e1 100644 (file)
@@ -4,6 +4,7 @@
 #include "Python.h"
 #include "pycore_abstract.h"   // _PyIndex_Check()
 #include "pycore_object.h"     // _PyType_IsReady()
+#include "pycore_unicodeobject.h"     // _PyUnicodeWriter_FormatV()
 
 typedef double va_double;
 
@@ -668,6 +669,116 @@ PyModule_AddType(PyObject *module, PyTypeObject *type)
     return PyModule_AddObjectRef(module, name, (PyObject *)type);
 }
 
+static int _abiinfo_raise(const char *module_name, const char *format, ...)
+{
+    PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
+    if (!writer) {
+        return -1;
+    }
+    if (module_name) {
+        if (PyUnicodeWriter_WriteUTF8(writer, module_name, -1) < 0) {
+            PyUnicodeWriter_Discard(writer);
+            return -1;
+        }
+        if (PyUnicodeWriter_WriteASCII(writer, ": ", 2) < 0) {
+            PyUnicodeWriter_Discard(writer);
+            return -1;
+        }
+    }
+    va_list vargs;
+    va_start(vargs, format);
+    if (_PyUnicodeWriter_FormatV(writer, format, vargs) < 0) {
+        PyUnicodeWriter_Discard(writer);
+        return -1;
+    }
+    PyObject *message = PyUnicodeWriter_Finish(writer);
+    if (!message) {
+        return -1;
+    }
+    PyErr_SetObject(PyExc_ImportError, message);
+    Py_DECREF(message);
+    return -1;
+}
+
+int PyABIInfo_Check(PyABIInfo *info, const char *module_name)
+{
+    if (!info) {
+        return _abiinfo_raise(module_name, "NULL PyABIInfo");
+    }
+
+    /* abiinfo_major_version */
+    if (info->abiinfo_major_version == 0) {
+        return 0;
+    }
+    if (info->abiinfo_major_version > 1) {
+        return _abiinfo_raise(module_name, "PyABIInfo version too high");
+    }
+
+    /* Internal ABI */
+    if (info->flags & PyABIInfo_INTERNAL) {
+        if (info->abi_version && (info->abi_version != PY_VERSION_HEX)) {
+            return _abiinfo_raise(
+                module_name,
+                "incompatible internal ABI (0x%x != 0x%x)",
+                info->abi_version, PY_VERSION_HEX);
+        }
+    }
+
+#define XY_MASK 0xffff0000
+    if (info->flags & PyABIInfo_STABLE) {
+        /* Greater-than major.minor version check */
+        if (info->abi_version) {
+            if ((info->abi_version & XY_MASK) > (PY_VERSION_HEX & XY_MASK)) {
+                return _abiinfo_raise(
+                    module_name,
+                    "incompatible future stable ABI version (%d.%d)",
+                    ((info->abi_version) >> 24) % 0xff,
+                    ((info->abi_version) >> 16) % 0xff);
+            }
+            if (info->abi_version < Py_PACK_VERSION(3, 2)) {
+                return _abiinfo_raise(
+                    module_name,
+                    "invalid stable ABI version (%d.%d)",
+                    ((info->abi_version) >> 24) % 0xff,
+                    ((info->abi_version) >> 16) % 0xff);
+            }
+        }
+        if (info->flags & PyABIInfo_INTERNAL) {
+            return _abiinfo_raise(module_name,
+                                  "cannot use both internal and stable ABI");
+        }
+    }
+    else {
+        /* Exact major.minor version check */
+        if (info->abi_version) {
+            if ((info->abi_version & XY_MASK) != (PY_VERSION_HEX & XY_MASK)) {
+                return _abiinfo_raise(
+                    module_name,
+                    "incompatible ABI version (%d.%d)",
+                    ((info->abi_version) >> 24) % 0xff,
+                    ((info->abi_version) >> 16) % 0xff);
+            }
+        }
+    }
+#undef XY_MASK
+
+    /* Free-threading/GIL */
+    uint16_t gilflags = info->flags & (PyABIInfo_GIL | PyABIInfo_FREETHREADED);
+#if Py_GIL_DISABLED
+    if (gilflags == PyABIInfo_GIL) {
+        return _abiinfo_raise(module_name,
+                              "incompatible with free-threaded CPython");
+    }
+#else
+    if (gilflags == PyABIInfo_FREETHREADED) {
+        return _abiinfo_raise(module_name,
+                              "only compatible with free-threaded CPython");
+    }
+#endif
+
+    return 0;
+}
+
 
 /* Exported functions for version helper macros */