From 0d566a88c35ba087998fd2cc2a484bb36bf9bfa9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Sep 2026 15:10:13 +0300 Subject: [PATCH] gh-157487: Add structured version info for ctypes Add constants ctypes.LIBFFI_VERSION, ctypes.libffi_version, ctypes.LIBFFI_VERSION_INFO and ctypes.libffi_version_info, which provide information about the version of the libffi library in use. They are only available with libffi 3.5 or later. Co-authored-by: Claude Opus 5 (1M context) --- Doc/library/ctypes.rst | 44 ++++++++++ Doc/whatsnew/3.16.rst | 7 ++ Lib/ctypes/__init__.py | 6 ++ Lib/test/pythoninfo.py | 11 +++ Lib/test/test_ctypes/test_values.py | 34 +++++++ ...-09-14-12-05-02.gh-issue-157487.ID-qIv.rst | 4 + Modules/_ctypes/_ctypes.c | 88 +++++++++++++++++++ 7 files changed, 194 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-12-05-02.gh-issue-157487.ID-qIv.rst diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 169459881328ab6..8946ee7a02da480 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -3383,3 +3383,47 @@ Exceptions .. availability:: Windows .. versionadded:: 3.14 + + +Library version +^^^^^^^^^^^^^^^ + +The following constants are only available if :mod:`!ctypes` was built with +libffi 3.5 or later, which is the first version providing this information. + +.. data:: LIBFFI_VERSION + + The version string of the libffi library that was used for building + the module, like ``'3.5.2'``. + This may be different from the libffi library actually used at runtime, + which is available as :const:`libffi_version`. + + .. versionadded:: next + +.. data:: libffi_version + + The version string of the libffi library actually loaded by the interpreter. + + .. versionadded:: next + +.. data:: LIBFFI_VERSION_INFO + + A named tuple containing the three components of the libffi library + version that was used for building the module: + *major*, *minor*, and *patch*. + All values are integers. + The components can also be accessed by name, + so ``ctypes.LIBFFI_VERSION_INFO[0]`` is equivalent to + ``ctypes.LIBFFI_VERSION_INFO.major`` and so on. + This may be different from the libffi library actually used at runtime, + which is available as :const:`libffi_version_info`. + + .. versionadded:: next + +.. data:: libffi_version_info + + A named tuple containing the version of the libffi library + actually loaded by the interpreter, + with the same fields as :const:`LIBFFI_VERSION_INFO`. + + .. versionadded:: next diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 53983637f520c8a..d6ff8a3d1e3e7c3 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -184,6 +184,13 @@ ctypes through a function signature. (Contributed by Peter Bierma in :gh:`153903`.) +* Added constants :const:`~ctypes.LIBFFI_VERSION`, + :const:`~ctypes.libffi_version`, :const:`~ctypes.LIBFFI_VERSION_INFO` + and :const:`~ctypes.libffi_version_info`, which provide information + about the version of the libffi library in use. + They are only available with libffi 3.5 or later. + (Contributed by Serhiy Storchaka in :gh:`157487`.) + curses ------ diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 890168cc9809fd6..2e6893e3b8e513e 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -12,6 +12,12 @@ from _ctypes import RTLD_LOCAL, RTLD_GLOBAL from _ctypes import ArgumentError from _ctypes import SIZEOF_TIME_T +try: + from _ctypes import (LIBFFI_VERSION, libffi_version, + LIBFFI_VERSION_INFO, libffi_version_info) +except ImportError: + # libffi < 3.5 does not provide version information. + pass from _ctypes import CField from struct import calcsize as _calcsize diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index b59e2acb9376f1e..a6099cda28f07c5 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -745,6 +745,16 @@ def collect_zstd(info_add): copy_attributes(info_add, _zstd, 'zstd.%s', attributes) +def collect_ctypes(info_add): + try: + import _ctypes + except ImportError: + return + + attributes = ('LIBFFI_VERSION', 'libffi_version') + copy_attributes(info_add, _ctypes, 'ctypes.%s', attributes) + + def collect_expat(info_add): try: from xml.parsers import expat @@ -1353,6 +1363,7 @@ def collect_info(info): collect_curses, collect_datetime, collect_decimal, + collect_ctypes, collect_expat, collect_fips, collect_gdb, diff --git a/Lib/test/test_ctypes/test_values.py b/Lib/test/test_ctypes/test_values.py index 8d1ee25ace5479b..82c928c9f6406c2 100644 --- a/Lib/test/test_ctypes/test_values.py +++ b/Lib/test/test_ctypes/test_values.py @@ -3,14 +3,48 @@ """ import _imp +import ctypes import importlib.util import sys import unittest from ctypes import (Structure, CDLL, POINTER, pythonapi, c_ubyte, c_char_p, c_int) +from test import support from test.support import import_helper, thread_unsafe +class LibffiVersionTest(unittest.TestCase): + + def _test_libffi_version(self, v, string): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 3) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + self.assertEqual(string, '%d.%d.%d' % v) + + @unittest.skipUnless(hasattr(ctypes, 'LIBFFI_VERSION_INFO'), + 'requires libffi >= 3.5') + def test_libffi_version(self): + if support.verbose: + print(f'LIBFFI_VERSION = {ctypes.LIBFFI_VERSION}', flush=True) + print(f'libffi_version = {ctypes.libffi_version}', flush=True) + print(f'LIBFFI_VERSION_INFO = {ctypes.LIBFFI_VERSION_INFO}', flush=True) + print(f'libffi_version_info = {ctypes.libffi_version_info}', flush=True) + self._test_libffi_version(ctypes.LIBFFI_VERSION_INFO, ctypes.LIBFFI_VERSION) + self._test_libffi_version(ctypes.libffi_version_info, ctypes.libffi_version) + self.assertEqual(ctypes.LIBFFI_VERSION_INFO[0], ctypes.libffi_version_info[0]) + + class ValuesTestCase(unittest.TestCase): def setUp(self): diff --git a/Misc/NEWS.d/next/Library/2026-09-14-12-05-02.gh-issue-157487.ID-qIv.rst b/Misc/NEWS.d/next/Library/2026-09-14-12-05-02.gh-issue-157487.ID-qIv.rst new file mode 100644 index 000000000000000..95aa332ca90859d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-12-05-02.gh-issue-157487.ID-qIv.rst @@ -0,0 +1,4 @@ +Add constants :const:`ctypes.LIBFFI_VERSION`, :const:`ctypes.libffi_version`, +:const:`ctypes.LIBFFI_VERSION_INFO` and :const:`ctypes.libffi_version_info`, +which provide information about the version of the libffi library in use. +They are only available with libffi 3.5 or later. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 3882b9a5ddff3c6..97f33b1be8561fc 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -6386,6 +6386,89 @@ _ctypes_add_objects(PyObject *mod) } +#ifdef FFI_VERSION_NUMBER +PyDoc_STRVAR(libffi_version_info__doc__, +"ctypes.libffi_version_info\n\ +\n\ +libffi version information as a named tuple."); + +static PyStructSequence_Field libffi_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {0} +}; + +static PyStructSequence_Desc libffi_version_info_desc = { + "ctypes.libffi_version_info", /* name */ + libffi_version_info__doc__, /* doc */ + libffi_version_info_fields, /* fields */ + 3 +}; + +static PyObject * +make_libffi_version_info(PyTypeObject *type, unsigned long number) +{ + PyObject *version; + int pos = 0; + unsigned long major = number / 10000; + unsigned long minor = (number % 10000) / 100; + unsigned long patch = number % 100; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromUnsignedLong(major)) + SetItem(PyLong_FromUnsignedLong(minor)) + SetItem(PyLong_FromUnsignedLong(patch)) +#undef SetItem + + return version; +} + +static int +_ctypes_add_version_constants(PyObject *mod) +{ + if (PyModule_AddStringConstant(mod, "LIBFFI_VERSION", + FFI_VERSION_STRING) < 0) { + return -1; + } + if (PyModule_AddStringConstant(mod, "libffi_version", + ffi_get_version()) < 0) { + return -1; + } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&libffi_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(mod, "LIBFFI_VERSION_INFO", + make_libffi_version_info(version_type, FFI_VERSION_NUMBER)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(mod, "libffi_version_info", + make_libffi_version_info(version_type, + ffi_get_version_number())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} +#endif + static int _ctypes_mod_exec(PyObject *mod) { @@ -6440,6 +6523,11 @@ _ctypes_mod_exec(PyObject *mod) if (_ctypes_add_objects(mod) < 0) { return -1; } +#ifdef FFI_VERSION_NUMBER + if (_ctypes_add_version_constants(mod) < 0) { + return -1; + } +#endif return 0; }