https://github.com/python/cpython/commit/da72cc70ca00f9677d219631ec397d5f47c926df
commit: da72cc70ca00f9677d219631ec397d5f47c926df
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-19T18:06:36+03:00
summary:

gh-157487: Add structured version info for ctypes (GH-157488)

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) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-14-12-05-02.gh-issue-157487.ID-qIv.rst
M Doc/library/ctypes.rst
M Doc/whatsnew/3.16.rst
M Lib/ctypes/__init__.py
M Lib/test/pythoninfo.py
M Lib/test/test_ctypes/test_values.py
M Modules/_ctypes/_ctypes.c

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 d24fed49d1f95e3..7e02924babe9523 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;
 }
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to