diff --git a/Doc/c-api/module.rst b/Doc/c-api/module.rst index c8edcecc5b419f..29d5cd8d5391a3 100644 --- a/Doc/c-api/module.rst +++ b/Doc/c-api/module.rst @@ -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: diff --git a/Doc/c-api/stable.rst b/Doc/c-api/stable.rst index 9b65e0b8d23d93..6549566fc00616 100644 --- a/Doc/c-api/stable.rst +++ b/Doc/c-api/stable.rst @@ -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,151 @@ 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 ` +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. +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 diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 0d0dfb3843260e..641f7bb3804471 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -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,, diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index 2cda9587975ddc..6eb5c243b873c9 100644 --- a/Doc/using/configure.rst +++ b/Doc/using/configure.rst @@ -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. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 89644a509a0bb4..803381111259cb 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -540,6 +540,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 ---------------------- diff --git a/Include/cpython/modsupport.h b/Include/cpython/modsupport.h index d3b88f58c82ca3..6134442106474f 100644 --- a/Include/cpython/modsupport.h +++ b/Include/cpython/modsupport.h @@ -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 diff --git a/Include/internal/pycore_unicodeobject.h b/Include/internal/pycore_unicodeobject.h index 3791b913c17546..8dfcaedd5ef2e8 100644 --- a/Include/internal/pycore_unicodeobject.h +++ b/Include/internal/pycore_unicodeobject.h @@ -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( diff --git a/Include/modsupport.h b/Include/modsupport.h index af995f567b004c..d4e4e92ebb18a2 100644 --- a/Include/modsupport.h +++ b/Include/modsupport.h @@ -56,55 +56,14 @@ PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def); #define Py_CLEANUP_SUPPORTED 0x20000 +/* The API and ABI versions are left for backwards compatibility. + They've not been updated since 2006 and 2010, respectively. + API/ABI versioning is now tied to the CPython version. + The *_VERSION and *_STRING symbols should define the same value; as + number and string literal respectively. Make sure the definitions match. +*/ #define PYTHON_API_VERSION 1013 #define PYTHON_API_STRING "1013" -/* The API version is maintained (independently from the Python version) - so we can detect mismatches between the interpreter and dynamically - loaded modules. These are diagnosed by an error message but - the module is still loaded (because the mismatch can only be tested - after loading the module). The error message is intended to - explain the core dump a few seconds later. - - The symbol PYTHON_API_STRING defines the same value as a string - literal. *** PLEASE MAKE SURE THE DEFINITIONS MATCH. *** - - Please add a line or two to the top of this log for each API - version change: - - 22-Feb-2006 MvL 1013 PEP 353 - long indices for sequence lengths - - 19-Aug-2002 GvR 1012 Changes to string object struct for - interning changes, saving 3 bytes. - - 17-Jul-2001 GvR 1011 Descr-branch, just to be on the safe side - - 25-Jan-2001 FLD 1010 Parameters added to PyCode_New() and - PyFrame_New(); Python 2.1a2 - - 14-Mar-2000 GvR 1009 Unicode API added - - 3-Jan-1999 GvR 1007 Decided to change back! (Don't reuse 1008!) - - 3-Dec-1998 GvR 1008 Python 1.5.2b1 - - 18-Jan-1997 GvR 1007 string interning and other speedups - - 11-Oct-1996 GvR renamed Py_Ellipses to Py_Ellipsis :-( - - 30-Jul-1996 GvR Slice and ellipses syntax added - - 23-Jul-1996 GvR For 1.4 -- better safe than sorry this time :-) - - 7-Nov-1995 GvR Keyword arguments (should've been done at 1.3 :-( ) - - 10-Jan-1995 GvR Renamed globals to new naming scheme - - 9-Jan-1995 GvR Initial version (incompatible with older API) -*/ - -/* The PYTHON_ABI_VERSION is introduced in PEP 384. For the lifetime of - Python 3, it will stay at the value of 3; changes to the limited API - must be performed in a strictly backwards-compatible manner. */ #define PYTHON_ABI_VERSION 3 #define PYTHON_ABI_STRING "3" @@ -134,6 +93,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" diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 17634a93f8fa6f..e3afac0a343be1 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -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 index 00000000000000..1520489f843826 --- /dev/null +++ b/Lib/test/test_capi/test_modsupport.py @@ -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) diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 5a6ba9de337904..cbec7e43a7c9fb 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -45,6 +45,7 @@ def test_windows_feature_macros(self): 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 index 00000000000000..0f7336ec7caa81 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-07-29-18-00-22.gh-issue-137210.DD4VEm.rst @@ -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`. diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 1f323cc03973e5..bc0fab48528115 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2575,6 +2575,7 @@ added = '3.14' [function.Py_PACK_VERSION] added = '3.14' + [function.PySys_GetAttr] added = '3.15' [function.PySys_GetAttrString] @@ -2583,3 +2584,24 @@ 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' diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 7f4c4a806737ac..5365e68101cf4c 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @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 diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 01039dfeec0719..30a202dfb49da0 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -7495,6 +7495,7 @@ _datetime_exec(PyObject *module) } 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 index 00000000000000..6746eb9eb1e94a --- /dev/null +++ b/Modules/_testcapi/modsupport.c @@ -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; +} diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index af6400162daf2b..32915d04bd3635 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -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); diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index d0c0b45c20cb36..4f22a70802009a 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -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; } diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 862395e7881870..8dd0e82dce6cf3 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -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: diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 425e4681f0a4dc..ca996e757f202b 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -3271,14 +3271,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; diff --git a/PC/python3dll.c b/PC/python3dll.c index 8ec791f8280f13..05c86e6d5924d4 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -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) diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index a68f15d25aabb7..a355a5fc25707a 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -125,6 +125,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index 21091e9dc1aa16..05128d3ac36efc 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -108,6 +108,9 @@ Source Files + + Source Files + Source Files diff --git a/Python/modsupport.c b/Python/modsupport.c index 437ad412027e28..fd74c4133abcc9 100644 --- a/Python/modsupport.c +++ b/Python/modsupport.c @@ -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; @@ -655,6 +656,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 */