From feb9f5a117c1793ddeb25b3675318b4d3df2f90a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 10 Aug 2026 10:43:09 +0300 Subject: [PATCH] gh-112014: correct buffer protocol support in ctypes (use native formats) This allows better interoperation with the memoryview, e.g. support for ctypes arrays. Just like NumPy arrays, they also don't specify endianness: ```pycon >>> import numpy as np >>> memoryview(np.ndarray(3, dtype=np.float16)).format 'e' ``` But keep specified byteorder for byte-swapped types. --- Lib/test/test_ctypes/test_numbers.py | 5 +- Lib/test/test_ctypes/test_pep3118.py | 126 +++++++++--------- ...-08-10-10-35-19.gh-issue-112014.nLEBCv.rst | 4 + Modules/_ctypes/_ctypes.c | 17 +-- 4 files changed, 74 insertions(+), 78 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-10-10-35-19.gh-issue-112014.nLEBCv.rst diff --git a/Lib/test/test_ctypes/test_numbers.py b/Lib/test/test_ctypes/test_numbers.py index b7df08f6079cd3..26f7a032c3974c 100644 --- a/Lib/test/test_ctypes/test_numbers.py +++ b/Lib/test/test_ctypes/test_numbers.py @@ -131,11 +131,10 @@ def test_complex(self): self.assertEqual(t(FloatLike()).value, 2+0j) self.assertEqual(t(ComplexLike()).value, 1+1j) - prefix = '>' if sys.byteorder == 'big' else '<' num = t(1.0) - self.assertEqual(memoryview(num).format, prefix + format) + self.assertEqual(memoryview(num).format, format) array = (t * 3)() - self.assertEqual(memoryview(array).format, prefix + format) + self.assertEqual(memoryview(array).format, format) @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), "requires C11 complex type") diff --git a/Lib/test/test_ctypes/test_pep3118.py b/Lib/test/test_ctypes/test_pep3118.py index 11a0744f5a8e36..24e5f42c63257d 100644 --- a/Lib/test/test_ctypes/test_pep3118.py +++ b/Lib/test/test_ctypes/test_pep3118.py @@ -1,4 +1,3 @@ -import re import sys import unittest from ctypes import (CFUNCTYPE, POINTER, sizeof, Union, @@ -9,29 +8,12 @@ c_bool, c_float, c_double, c_longdouble, py_object) -if sys.byteorder == "little": - THIS_ENDIAN = "<" - OTHER_ENDIAN = ">" -else: - THIS_ENDIAN = ">" - OTHER_ENDIAN = "<" - - -def normalize(format): - # Remove current endian specifier and white space from a format - # string - if format is None: - return "" - format = format.replace(OTHER_ENDIAN, THIS_ENDIAN) - return re.sub(r"\s", "", format) - - class Test(unittest.TestCase): def test_native_types(self): for tp, fmt, shape, itemtp in native_types: ob = tp() v = memoryview(ob) - self.assertEqual(normalize(v.format), normalize(fmt)) + self.assertEqual(v.format, fmt) if shape: self.assertEqual(len(v), shape[0]) else: @@ -73,6 +55,15 @@ def test_endian_types(self): n = n * dim self.assertEqual(n * v.itemsize, len(v.tobytes())) + def test_memoryview_supports_ctypes_arrays(self): + ArrayType = c_int * 5 + a = ArrayType(123, 42, 1, 2, 3) + m = memoryview(a) + self.assertEqual(m.shape, (5,)) + self.assertEqual(m.format, c_int._type_) + self.assertEqual(list(m), [123, 42, 1, 2, 3]) + self.assertEqual(m[1], 42) + # define some structure classes @@ -124,8 +115,7 @@ class Complete(Structure): ################################################################ # -# This table contains format strings as they look on little endian -# machines. The test replaces '<' with '>' on big endian machines. +# This table contains format strings with native endianness. # # Platform-specific type codes @@ -160,59 +150,59 @@ class Complete(Structure): ## simple types - (c_char, "l:x:>l:y:}".replace('l', s_long), (), BEPoint), - (LEPoint * 1, "T{l:x:>l:y:}".replace('l', s_long), (), POINTER(BEPoint)), - (POINTER(LEPoint), "&T{l:x:>l:y:}".replace('l', s_long), (), BEPoint), + (LEPoint * 1, "T{l:x:l:y:}".replace('l', s_long), (1,), LEPoint), + (POINTER(BEPoint), "&T{>l:x:>l:y:}".replace('l', s_long), (), POINTER(BEPoint)), + (POINTER(LEPoint), "&T{l:x:l:y:}".replace('l', s_long), (), POINTER(LEPoint)), + ] +else: + endian_types = [ + (BEPoint * 1, "T{l:x:l:y:}".replace('l', s_long), (1,), BEPoint), + (LEPoint, "T{` support in the +:mod:`ctypes` module to use the machine’s native format and byte order, +rather than explicitly specify endianness (by ``'<'`` or ``'>'``). The +later kept for byte-swapped types. Patch by Sergey B Kirpichev. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index adfdf44e53604e..6c07a9eaad7be3 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -267,7 +267,7 @@ _PyDict_GetItemProxy(PyObject *dict, PyObject *key, PyObject **presult) later on. */ static char * -_ctypes_alloc_format_string_for_type(const char *code, int big_endian) +_ctypes_alloc_format_string_for_type(const char *code) { const char *pep_code = NULL; @@ -310,14 +310,13 @@ _ctypes_alloc_format_string_for_type(const char *code, int big_endian) break; } - char *result = PyMem_Malloc(1 + strlen(pep_code) + 1); + char *result = PyMem_Malloc(1 + strlen(pep_code)); if (result == NULL) { PyErr_NoMemory(); return NULL; } - result[0] = big_endian ? '>' : '<'; - strcpy(result + 1, pep_code); + strcpy(result, pep_code); return result; } @@ -2405,11 +2404,7 @@ PyCSimpleType_init(PyObject *self, PyObject *args, PyObject *kwds) stginfo->size = fmt->pffi_type->size; stginfo->setfunc = fmt->setfunc; stginfo->getfunc = fmt->getfunc; -#ifdef WORDS_BIGENDIAN - stginfo->format = _ctypes_alloc_format_string_for_type(proto_str, 1); -#else - stginfo->format = _ctypes_alloc_format_string_for_type(proto_str, 0); -#endif + stginfo->format = _ctypes_alloc_format_string_for_type(proto_str); if (stginfo->format == NULL) { Py_DECREF(proto); return -1; @@ -2504,14 +2499,14 @@ PyCSimpleType_init(PyObject *self, PyObject *args, PyObject *kwds) PyObject_SetAttrString(swapped, "__ctype_be__", self); PyObject_SetAttrString(swapped, "__ctype_le__", swapped); /* We are creating the type for the OTHER endian */ - sw_info->format = _ctypes_alloc_format_string("<", stginfo->format+1); + sw_info->format = _ctypes_alloc_format_string("<", stginfo->format); #else PyObject_SetAttrString(self, "__ctype_be__", swapped); PyObject_SetAttrString(self, "__ctype_le__", self); PyObject_SetAttrString(swapped, "__ctype_le__", self); PyObject_SetAttrString(swapped, "__ctype_be__", swapped); /* We are creating the type for the OTHER endian */ - sw_info->format = _ctypes_alloc_format_string(">", stginfo->format+1); + sw_info->format = _ctypes_alloc_format_string(">", stginfo->format); #endif Py_DECREF(swapped); if (PyErr_Occurred()) {