From feb9f5a117c1793ddeb25b3675318b4d3df2f90a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 10 Aug 2026 10:43:09 +0300 Subject: [PATCH 1/2] 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 b7df08f6079cd31..26f7a032c3974c4 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 11a0744f5a8e365..24e5f42c63257d3 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 adfdf44e53604e3..6c07a9eaad7be39 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()) { From a58b4e4dadca9ed9bf951aa5252676e267dba022 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 11 Aug 2026 11:58:30 +0300 Subject: [PATCH 2/2] address review: use native types --- Lib/test/test_ctypes/test_pep3118.py | 41 +++++++++++++++------------- Modules/_ctypes/_ctypes.c | 14 ++++++---- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_ctypes/test_pep3118.py b/Lib/test/test_ctypes/test_pep3118.py index 24e5f42c63257d3..8e282064ad4281c 100644 --- a/Lib/test/test_ctypes/test_pep3118.py +++ b/Lib/test/test_ctypes/test_pep3118.py @@ -119,15 +119,15 @@ class Complete(Structure): # # Platform-specific type codes -s_bool = {1: '?', 2: 'H', 4: 'L', 8: 'Q'}[sizeof(c_bool)] -s_short = {2: 'h', 4: 'l', 8: 'q'}[sizeof(c_short)] -s_ushort = {2: 'H', 4: 'L', 8: 'Q'}[sizeof(c_ushort)] -s_int = {2: 'h', 4: 'i', 8: 'q'}[sizeof(c_int)] -s_uint = {2: 'H', 4: 'I', 8: 'Q'}[sizeof(c_uint)] -s_long = {4: 'l', 8: 'q'}[sizeof(c_long)] -s_ulong = {4: 'L', 8: 'Q'}[sizeof(c_ulong)] -s_longlong = "q" -s_ulonglong = "Q" +s_bool = c_bool._type_ +s_short = c_short._type_ +s_ushort = c_ushort._type_ +s_int = c_int._type_ +s_uint = c_uint._type_ +s_long = c_long._type_ +s_ulong = c_ulong._type_ +s_longlong = c_longlong._type_ +s_ulonglong = c_ulonglong._type_ s_float = "f" s_double = "d" s_longdouble = "g" @@ -194,9 +194,9 @@ class Complete(Structure): (Point, "T{l:x:l:y:}".replace('l', s_long), (), Point), (PackedPoint, "T{l:x:l:y:}".replace('l', s_long), (), PackedPoint), (PointMidPad, "T{b:x:3xI:y:}".replace('I', s_uint), (), PointMidPad), - (PackedPointMidPad, "T{b:x:xQ:y:}", (), PackedPointMidPad), + (PackedPointMidPad, "T{b:x:xQ:y:}".replace('Q', s_ulonglong), (), PackedPointMidPad), (PointEndPad, "T{I:x:b:y:3x}".replace('I', s_uint), (), PointEndPad), - (PackedPointEndPad, "T{Q:x:b:y:x}", (), PackedPointEndPad), + (PackedPointEndPad, "T{Q:x:b:y:x}".replace('Q', s_ulonglong), (), PackedPointEndPad), (EmptyStruct, "T{}", (), EmptyStruct), # the pep doesn't support unions (aUnion, "B", (), aUnion), @@ -229,21 +229,24 @@ class LEPoint(LittleEndianStructure): _fields_ = [("x", c_long), ("y", c_long)] +s_long2 = {4: 'l', 8: 'q'}[sizeof(c_long)] + + # This table contains format strings as they really look, on both big # and little endian machines. if sys.byteorder == "little": endian_types = [ - (BEPoint, "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)), + (BEPoint, "T{>l:x:>l:y:}".replace('l', s_long2), (), BEPoint), + (LEPoint * 1, "T{l:x:l:y:}", (1,), LEPoint), + (POINTER(BEPoint), "&T{>l:x:>l:y:}".replace('l', s_long2), (), POINTER(BEPoint)), + (POINTER(LEPoint), "&T{l:x:l:y:}", (), POINTER(LEPoint)), ] else: endian_types = [ - (BEPoint * 1, "T{l:x:l:y:}".replace('l', s_long), (1,), BEPoint), - (LEPoint, "T{' : '<'; + strcpy(result + 1, pep_code); return result; } @@ -2404,11 +2405,12 @@ PyCSimpleType_init(PyObject *self, PyObject *args, PyObject *kwds) stginfo->size = fmt->pffi_type->size; stginfo->setfunc = fmt->setfunc; stginfo->getfunc = fmt->getfunc; - stginfo->format = _ctypes_alloc_format_string_for_type(proto_str); + stginfo->format = PyMem_Malloc(1 + strlen(proto_str)); if (stginfo->format == NULL) { Py_DECREF(proto); return -1; } + strcpy(stginfo->format, proto_str); stginfo->paramfunc = PyCSimpleType_paramfunc; /* @@ -2499,14 +2501,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); + sw_info->format = _ctypes_alloc_format_string_for_type(stginfo->format, 0); #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); + sw_info->format = _ctypes_alloc_format_string_for_type(stginfo->format, 1); #endif Py_DECREF(swapped); if (PyErr_Occurred()) {