diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 0bf88b0e3b66c6..2825590400c70b 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2172,10 +2172,20 @@ Utility functions .. function:: POINTER(type, /) - Create and return a new ctypes pointer type. Pointer types are cached and + Create or return a ctypes pointer type. Pointer types are cached and reused internally, so calling this function repeatedly is cheap. *type* must be a ctypes type. + .. impl-detail:: + + The resulting pointer type is cached in the ``__pointer_type__`` + attribute of *type*. + It is possible to set this attribute before the first call to + ``POINTER`` in order to set a custom pointer type. + However, doing this is discouraged: manually creating a suitable + pointer type is difficult without relying on implementation + details that may change in future Python versions. + .. function:: pointer(obj, /) @@ -2340,6 +2350,16 @@ Data types library. *name* is the name of the symbol that exports the data, *library* is the loaded shared library. + Common class variables of ctypes data types: + + .. attribute:: __pointer_type__ + + The pointer type that was created by calling + :func:`POINTER` for corresponding ctypes data type. If a pointer type + was not yet created, the attribute is missing. + + .. versionadded:: next + Common instance variables of ctypes data types: .. attribute:: _b_base_ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 0d2e6533d05f8d..c8f97a2405ed5b 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -803,11 +803,16 @@ ctypes loaded by the current process. (Contributed by Brian Ward in :gh:`119349`.) +* Move :func:`ctypes.POINTER` types cache from a global internal cache + (``_pointer_type_cache``) to the :attr:`ctypes._CData.__pointer_type__` + attribute of the corresponding :mod:`ctypes` types. + This will stop the cache from growing without limits in some situations. + (Contributed by Sergey Miryanov in :gh:`100926`). + * The :class:`ctypes.py_object` type now supports subscription, making it a :term:`generic type`. (Contributed by Brian Schubert in :gh:`132168`.) - datetime -------- @@ -1675,6 +1680,13 @@ Deprecated :func:`codecs.open` is now deprecated. Use :func:`open` instead. (Contributed by Inada Naoki in :gh:`133036`.) +* :mod:`ctypes`: + Calling :func:`ctypes.POINTER` on a string is deprecated. + Use :ref:`ctypes-incomplete-types` for self-referential structures. + Also, the internal ``ctypes._pointer_type_cache`` is deprecated. + See :func:`ctypes.POINTER` for updated implementation details. + (Contributed by Sergey Myrianov in :gh:`100926`.) + * :mod:`functools`: Calling the Python implementation of :func:`functools.reduce` with *function* or *sequence* as keyword arguments is now deprecated. diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index b0e74f679ed18a..823a3692fd1bbf 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -266,7 +266,72 @@ class c_void_p(_SimpleCData): class c_bool(_SimpleCData): _type_ = "?" -from _ctypes import POINTER, pointer, _pointer_type_cache +def POINTER(cls): + """Create and return a new ctypes pointer type. + + Pointer types are cached and reused internally, + so calling this function repeatedly is cheap. + """ + if cls is None: + return c_void_p + try: + return cls.__pointer_type__ + except AttributeError: + pass + if isinstance(cls, str): + # handle old-style incomplete types (see test_ctypes.test_incomplete) + import warnings + warnings._deprecated("ctypes.POINTER with string", remove=(3, 19)) + try: + return _pointer_type_cache_fallback[cls] + except KeyError: + result = type(f'LP_{cls}', (_Pointer,), {}) + _pointer_type_cache_fallback[cls] = result + return result + + # create pointer type and set __pointer_type__ for cls + return type(f'LP_{cls.__name__}', (_Pointer,), {'_type_': cls}) + +def pointer(obj): + """Create a new pointer instance, pointing to 'obj'. + + The returned object is of the type POINTER(type(obj)). Note that if you + just want to pass a pointer to an object to a foreign function call, you + should use byref(obj) which is much faster. + """ + typ = POINTER(type(obj)) + return typ(obj) + +class _PointerTypeCache: + def __setitem__(self, cls, pointer_type): + import warnings + warnings._deprecated("ctypes._pointer_type_cache", remove=(3, 19)) + try: + cls.__pointer_type__ = pointer_type + except AttributeError: + _pointer_type_cache_fallback[cls] = pointer_type + + def __getitem__(self, cls): + import warnings + warnings._deprecated("ctypes._pointer_type_cache", remove=(3, 19)) + try: + return cls.__pointer_type__ + except AttributeError: + return _pointer_type_cache_fallback[cls] + + def get(self, cls, default=None): + import warnings + warnings._deprecated("ctypes._pointer_type_cache", remove=(3, 19)) + try: + return cls.__pointer_type__ + except AttributeError: + return _pointer_type_cache_fallback.get(cls, default) + + def __contains__(self, cls): + return hasattr(cls, '__pointer_type__') + +_pointer_type_cache_fallback = {} +_pointer_type_cache = _PointerTypeCache() class c_wchar_p(_SimpleCData): _type_ = "Z" @@ -277,7 +342,7 @@ class c_wchar(_SimpleCData): _type_ = "u" def _reset_cache(): - _pointer_type_cache.clear() + _pointer_type_cache_fallback.clear() _c_functype_cache.clear() if _os.name == "nt": _win_functype_cache.clear() @@ -285,7 +350,6 @@ def _reset_cache(): POINTER(c_wchar).from_param = c_wchar_p.from_param # _SimpleCData.c_char_p_from_param POINTER(c_char).from_param = c_char_p.from_param - _pointer_type_cache[None] = c_void_p def create_unicode_buffer(init, size=None): """create_unicode_buffer(aString) -> character array @@ -319,13 +383,7 @@ def create_unicode_buffer(init, size=None): def SetPointerType(pointer, cls): import warnings warnings._deprecated("ctypes.SetPointerType", remove=(3, 15)) - if _pointer_type_cache.get(cls, None) is not None: - raise RuntimeError("This type already exists in the cache") - if id(pointer) not in _pointer_type_cache: - raise RuntimeError("What's this???") pointer.set_type(cls) - _pointer_type_cache[cls] = pointer - del _pointer_type_cache[id(pointer)] def ARRAY(typ, len): return typ * len diff --git a/Lib/test/test_ctypes/test_byteswap.py b/Lib/test/test_ctypes/test_byteswap.py index 072c60d53dd8cb..9f9904282e451a 100644 --- a/Lib/test/test_ctypes/test_byteswap.py +++ b/Lib/test/test_ctypes/test_byteswap.py @@ -232,7 +232,6 @@ class TestStructure(parent): self.assertEqual(len(data), sizeof(TestStructure)) ptr = POINTER(TestStructure) s = cast(data, ptr)[0] - del ctypes._pointer_type_cache[TestStructure] self.assertEqual(s.point.x, 1) self.assertEqual(s.point.y, 2) @@ -371,7 +370,6 @@ class TestUnion(parent): self.assertEqual(len(data), sizeof(TestUnion)) ptr = POINTER(TestUnion) s = cast(data, ptr)[0] - del ctypes._pointer_type_cache[TestUnion] self.assertEqual(s.point.x, 1) self.assertEqual(s.point.y, 2) diff --git a/Lib/test/test_ctypes/test_c_simple_type_meta.py b/Lib/test/test_ctypes/test_c_simple_type_meta.py index 2328611856a1a0..fd261acf49741f 100644 --- a/Lib/test/test_ctypes/test_c_simple_type_meta.py +++ b/Lib/test/test_ctypes/test_c_simple_type_meta.py @@ -1,16 +1,15 @@ import unittest from test.support import MS_WINDOWS import ctypes -from ctypes import POINTER, c_void_p +from ctypes import POINTER, Structure, c_void_p -from ._support import PyCSimpleType +from ._support import PyCSimpleType, PyCPointerType, PyCStructType -class PyCSimpleTypeAsMetaclassTest(unittest.TestCase): - def tearDown(self): - # to not leak references, we must clean _pointer_type_cache - ctypes._reset_cache() +def set_non_ctypes_pointer_type(cls, pointer_type): + cls.__pointer_type__ = pointer_type +class PyCSimpleTypeAsMetaclassTest(unittest.TestCase): def test_creating_pointer_in_dunder_new_1(self): # Test metaclass whose instances are C types; when the type is # created it automatically creates a pointer type for itself. @@ -36,7 +35,7 @@ def __new__(cls, name, bases, namespace): else: ptr_bases = (self, POINTER(bases[0])) p = p_meta(f"POINTER({self.__name__})", ptr_bases, {}) - ctypes._pointer_type_cache[self] = p + set_non_ctypes_pointer_type(self, p) return self class p_meta(PyCSimpleType, ct_meta): @@ -45,20 +44,36 @@ class p_meta(PyCSimpleType, ct_meta): class PtrBase(c_void_p, metaclass=p_meta): pass + ptr_base_pointer = POINTER(PtrBase) + class CtBase(object, metaclass=ct_meta): pass + ct_base_pointer = POINTER(CtBase) + class Sub(CtBase): pass + sub_pointer = POINTER(Sub) + class Sub2(Sub): pass + sub2_pointer = POINTER(Sub2) + + self.assertIsNot(ptr_base_pointer, ct_base_pointer) + self.assertIsNot(ct_base_pointer, sub_pointer) + self.assertIsNot(sub_pointer, sub2_pointer) + self.assertIsInstance(POINTER(Sub2), p_meta) self.assertIsSubclass(POINTER(Sub2), Sub2) self.assertIsSubclass(POINTER(Sub2), POINTER(Sub)) self.assertIsSubclass(POINTER(Sub), POINTER(CtBase)) + self.assertIs(POINTER(Sub2), sub2_pointer) + self.assertIs(POINTER(Sub), sub_pointer) + self.assertIs(POINTER(CtBase), ct_base_pointer) + def test_creating_pointer_in_dunder_new_2(self): # A simpler variant of the above, used in `CoClass` of the `comtypes` # project. @@ -69,7 +84,7 @@ def __new__(cls, name, bases, namespace): if isinstance(self, p_meta): return self p = p_meta(f"POINTER({self.__name__})", (self, c_void_p), {}) - ctypes._pointer_type_cache[self] = p + set_non_ctypes_pointer_type(self, p) return self class p_meta(PyCSimpleType, ct_meta): @@ -78,15 +93,27 @@ class p_meta(PyCSimpleType, ct_meta): class Core(object): pass + with self.assertRaisesRegex(TypeError, "must have storage info"): + POINTER(Core) + class CtBase(Core, metaclass=ct_meta): pass + ct_base_pointer = POINTER(CtBase) + class Sub(CtBase): pass + sub_pointer = POINTER(Sub) + + self.assertIsNot(ct_base_pointer, sub_pointer) + self.assertIsInstance(POINTER(Sub), p_meta) self.assertIsSubclass(POINTER(Sub), Sub) + self.assertIs(POINTER(Sub), sub_pointer) + self.assertIs(POINTER(CtBase), ct_base_pointer) + def test_creating_pointer_in_dunder_init_1(self): class ct_meta(type): def __init__(self, name, bases, namespace): @@ -103,7 +130,7 @@ def __init__(self, name, bases, namespace): else: ptr_bases = (self, POINTER(bases[0])) p = p_meta(f"POINTER({self.__name__})", ptr_bases, {}) - ctypes._pointer_type_cache[self] = p + set_non_ctypes_pointer_type(self, p) class p_meta(PyCSimpleType, ct_meta): pass @@ -111,20 +138,37 @@ class p_meta(PyCSimpleType, ct_meta): class PtrBase(c_void_p, metaclass=p_meta): pass + ptr_base_pointer = POINTER(PtrBase) + class CtBase(object, metaclass=ct_meta): pass + ct_base_pointer = POINTER(CtBase) + class Sub(CtBase): pass + sub_pointer = POINTER(Sub) + class Sub2(Sub): pass + sub2_pointer = POINTER(Sub2) + + self.assertIsNot(ptr_base_pointer, ct_base_pointer) + self.assertIsNot(ct_base_pointer, sub_pointer) + self.assertIsNot(sub_pointer, sub2_pointer) + self.assertIsInstance(POINTER(Sub2), p_meta) self.assertIsSubclass(POINTER(Sub2), Sub2) self.assertIsSubclass(POINTER(Sub2), POINTER(Sub)) self.assertIsSubclass(POINTER(Sub), POINTER(CtBase)) + self.assertIs(POINTER(PtrBase), ptr_base_pointer) + self.assertIs(POINTER(CtBase), ct_base_pointer) + self.assertIs(POINTER(Sub), sub_pointer) + self.assertIs(POINTER(Sub2), sub2_pointer) + def test_creating_pointer_in_dunder_init_2(self): class ct_meta(type): def __init__(self, name, bases, namespace): @@ -135,7 +179,7 @@ def __init__(self, name, bases, namespace): if isinstance(self, p_meta): return p = p_meta(f"POINTER({self.__name__})", (self, c_void_p), {}) - ctypes._pointer_type_cache[self] = p + set_non_ctypes_pointer_type(self, p) class p_meta(PyCSimpleType, ct_meta): pass @@ -146,12 +190,21 @@ class Core(object): class CtBase(Core, metaclass=ct_meta): pass + ct_base_pointer = POINTER(CtBase) + class Sub(CtBase): pass + sub_pointer = POINTER(Sub) + + self.assertIsNot(ct_base_pointer, sub_pointer) + self.assertIsInstance(POINTER(Sub), p_meta) self.assertIsSubclass(POINTER(Sub), Sub) + self.assertIs(POINTER(CtBase), ct_base_pointer) + self.assertIs(POINTER(Sub), sub_pointer) + def test_bad_type_message(self): """Verify the error message that lists all available type codes""" # (The string is generated at runtime, so this checks the underlying @@ -168,3 +221,164 @@ class F(metaclass=PyCSimpleType): if not MS_WINDOWS: expected_type_chars.remove('X') self.assertIn("'" + ''.join(expected_type_chars) + "'", message) + + def test_creating_pointer_in_dunder_init_3(self): + """Check if interfcase subclasses properly creates according internal + pointer types. But not the same as external pointer types. + """ + + class StructureMeta(PyCStructType): + def __new__(cls, name, bases, dct, /, create_pointer_type=True): + assert len(bases) == 1, bases + return super().__new__(cls, name, bases, dct) + + def __init__(self, name, bases, dct, /, create_pointer_type=True): + + super().__init__(name, bases, dct) + if create_pointer_type: + p_bases = (POINTER(bases[0]),) + ns = {'_type_': self} + internal_pointer_type = PointerMeta(f"p{name}", p_bases, ns) + assert isinstance(internal_pointer_type, PyCPointerType) + assert self.__pointer_type__ is internal_pointer_type + + class PointerMeta(PyCPointerType): + def __new__(cls, name, bases, dct): + target = dct.get('_type_', None) + if target is None: + + # Create corresponding interface type and then set it as target + target = StructureMeta( + f"_{name}_", + (bases[0]._type_,), + {}, + create_pointer_type=False + ) + dct['_type_'] = target + + pointer_type = super().__new__(cls, name, bases, dct) + assert not hasattr(target, '__pointer_type__') + + return pointer_type + + def __init__(self, name, bases, dct, /, create_pointer_type=True): + target = dct.get('_type_', None) + assert not hasattr(target, '__pointer_type__') + super().__init__(name, bases, dct) + assert target.__pointer_type__ is self + + + class Interface(Structure, metaclass=StructureMeta, create_pointer_type=False): + pass + + class pInterface(POINTER(c_void_p), metaclass=PointerMeta): + _type_ = Interface + + class IUnknown(Interface): + pass + + class pIUnknown(pInterface): + pass + + self.assertTrue(issubclass(POINTER(IUnknown), pInterface)) + + self.assertIs(POINTER(Interface), pInterface) + self.assertIsNot(POINTER(IUnknown), pIUnknown) + + def test_creating_pointer_in_dunder_init_4(self): + """Check if interfcase subclasses properly creates according internal + pointer types, the same as external pointer types. + """ + class StructureMeta(PyCStructType): + def __new__(cls, name, bases, dct, /, create_pointer_type=True): + assert len(bases) == 1, bases + + return super().__new__(cls, name, bases, dct) + + def __init__(self, name, bases, dct, /, create_pointer_type=True): + + super().__init__(name, bases, dct) + if create_pointer_type: + p_bases = (POINTER(bases[0]),) + ns = {'_type_': self} + internal_pointer_type = PointerMeta(f"p{name}", p_bases, ns) + assert isinstance(internal_pointer_type, PyCPointerType) + assert self.__pointer_type__ is internal_pointer_type + + class PointerMeta(PyCPointerType): + def __new__(cls, name, bases, dct): + target = dct.get('_type_', None) + assert target is not None + pointer_type = getattr(target, '__pointer_type__', None) + + if pointer_type is None: + pointer_type = super().__new__(cls, name, bases, dct) + + return pointer_type + + def __init__(self, name, bases, dct, /, create_pointer_type=True): + target = dct.get('_type_', None) + if not hasattr(target, '__pointer_type__'): + # target.__pointer_type__ was created by super().__new__ + super().__init__(name, bases, dct) + + assert target.__pointer_type__ is self + + + class Interface(Structure, metaclass=StructureMeta, create_pointer_type=False): + pass + + class pInterface(POINTER(c_void_p), metaclass=PointerMeta): + _type_ = Interface + + class IUnknown(Interface): + pass + + class pIUnknown(pInterface): + _type_ = IUnknown + + self.assertTrue(issubclass(POINTER(IUnknown), pInterface)) + + self.assertIs(POINTER(Interface), pInterface) + self.assertIs(POINTER(IUnknown), pIUnknown) + + def test_custom_pointer_cache_for_ctypes_type1(self): + # Check if PyCPointerType.__init__() caches a pointer type + # customized in the metatype's __new__(). + class PointerMeta(PyCPointerType): + def __new__(cls, name, bases, namespace): + namespace["_type_"] = C + return super().__new__(cls, name, bases, namespace) + + def __init__(self, name, bases, namespace): + assert not hasattr(C, '__pointer_type__') + super().__init__(name, bases, namespace) + assert C.__pointer_type__ is self + + class C(c_void_p): # ctypes type + pass + + class P(ctypes._Pointer, metaclass=PointerMeta): + pass + + self.assertIs(P._type_, C) + self.assertIs(P, POINTER(C)) + + def test_custom_pointer_cache_for_ctypes_type2(self): + # Check if PyCPointerType.__init__() caches a pointer type + # customized in the metatype's __init__(). + class PointerMeta(PyCPointerType): + def __init__(self, name, bases, namespace): + self._type_ = namespace["_type_"] = C + assert not hasattr(C, '__pointer_type__') + super().__init__(name, bases, namespace) + assert C.__pointer_type__ is self + + class C(c_void_p): # ctypes type + pass + + class P(ctypes._Pointer, metaclass=PointerMeta): + pass + + self.assertIs(P._type_, C) + self.assertIs(P, POINTER(C)) diff --git a/Lib/test/test_ctypes/test_incomplete.py b/Lib/test/test_ctypes/test_incomplete.py index 9f859793d88a22..fefdfe9102e668 100644 --- a/Lib/test/test_ctypes/test_incomplete.py +++ b/Lib/test/test_ctypes/test_incomplete.py @@ -3,15 +3,20 @@ import warnings from ctypes import Structure, POINTER, pointer, c_char_p +# String-based "incomplete pointers" were implemented in ctypes 0.6.3 (2003, when +# ctypes was an external project). They made obsolete by the current +# incomplete *types* (setting `_fields_` late) in 0.9.5 (2005). +# ctypes was added to Python 2.5 (2006), without any mention in docs. -# The incomplete pointer example from the tutorial +# This tests incomplete pointer example from the old tutorial +# (https://svn.python.org/projects/ctypes/tags/release_0_6_3/ctypes/docs/tutorial.stx) class TestSetPointerType(unittest.TestCase): def tearDown(self): - # to not leak references, we must clean _pointer_type_cache - ctypes._reset_cache() + ctypes._pointer_type_cache_fallback.clear() def test_incomplete_example(self): - lpcell = POINTER("cell") + with self.assertWarns(DeprecationWarning): + lpcell = POINTER("cell") class cell(Structure): _fields_ = [("name", c_char_p), ("next", lpcell)] @@ -20,6 +25,8 @@ class cell(Structure): warnings.simplefilter('ignore', DeprecationWarning) ctypes.SetPointerType(lpcell, cell) + self.assertIs(POINTER(cell), lpcell) + c1 = cell() c1.name = b"foo" c2 = cell() @@ -37,7 +44,8 @@ class cell(Structure): self.assertEqual(result, [b"foo", b"bar"] * 4) def test_deprecation(self): - lpcell = POINTER("cell") + with self.assertWarns(DeprecationWarning): + lpcell = POINTER("cell") class cell(Structure): _fields_ = [("name", c_char_p), ("next", lpcell)] @@ -45,6 +53,7 @@ class cell(Structure): with self.assertWarns(DeprecationWarning): ctypes.SetPointerType(lpcell, cell) + self.assertIs(POINTER(cell), lpcell) if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_ctypes/test_keeprefs.py b/Lib/test/test_ctypes/test_keeprefs.py index 23b03b64b4a716..5602460d5ff8e0 100644 --- a/Lib/test/test_ctypes/test_keeprefs.py +++ b/Lib/test/test_ctypes/test_keeprefs.py @@ -1,6 +1,5 @@ import unittest -from ctypes import (Structure, POINTER, pointer, _pointer_type_cache, - c_char_p, c_int) +from ctypes import (Structure, POINTER, pointer, c_char_p, c_int) class SimpleTestCase(unittest.TestCase): @@ -115,10 +114,6 @@ class RECT(Structure): r.a[0].x = 42 r.a[0].y = 99 - # to avoid leaking when tests are run several times - # clean up the types left in the cache. - del _pointer_type_cache[POINT] - if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_ctypes/test_pointers.py b/Lib/test/test_ctypes/test_pointers.py index ed4541335dfca4..a8d243a45de0f4 100644 --- a/Lib/test/test_ctypes/test_pointers.py +++ b/Lib/test/test_ctypes/test_pointers.py @@ -1,15 +1,18 @@ import array import ctypes +import gc import sys import unittest from ctypes import (CDLL, CFUNCTYPE, Structure, - POINTER, pointer, _Pointer, _pointer_type_cache, + POINTER, pointer, _Pointer, byref, sizeof, c_void_p, c_char_p, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong, c_float, c_double) +from ctypes import _pointer_type_cache, _pointer_type_cache_fallback from test.support import import_helper +from weakref import WeakSet _ctypes_test = import_helper.import_module("_ctypes_test") from ._support import (_CData, PyCPointerType, Py_TPFLAGS_DISALLOW_INSTANTIATION, Py_TPFLAGS_IMMUTABLETYPE) @@ -22,6 +25,9 @@ class PointersTestCase(unittest.TestCase): + def tearDown(self): + _pointer_type_cache_fallback.clear() + def test_inheritance_hierarchy(self): self.assertEqual(_Pointer.mro(), [_Pointer, _CData, object]) @@ -127,6 +133,14 @@ def test_from_address(self): addr = a.buffer_info()[0] p = POINTER(POINTER(c_int)) + def test_pointer_from_pointer(self): + p1 = POINTER(c_int) + p2 = POINTER(p1) + + self.assertIsNot(p1, p2) + self.assertIs(p1.__pointer_type__, p2) + self.assertIs(p2._type_, p1) + def test_other(self): class Table(Structure): _fields_ = [("a", c_int), @@ -141,8 +155,6 @@ class Table(Structure): pt.contents.c = 33 - del _pointer_type_cache[Table] - def test_basic(self): p = pointer(c_int(42)) # Although a pointer can be indexed, it has no length @@ -175,6 +187,7 @@ def test_bug_1467852(self): q = pointer(y) pp[0] = q # <== self.assertEqual(p[0], 6) + def test_c_void_p(self): # http://sourceforge.net/tracker/?func=detail&aid=1518190&group_id=5470&atid=105470 if sizeof(c_void_p) == 4: @@ -193,6 +206,30 @@ def test_c_void_p(self): self.assertRaises(TypeError, c_void_p, 3.14) # make sure floats are NOT accepted self.assertRaises(TypeError, c_void_p, object()) # nor other objects + def test_read_null_pointer(self): + null_ptr = POINTER(c_int)() + with self.assertRaisesRegex(ValueError, "NULL pointer access"): + null_ptr[0] + + def test_write_null_pointer(self): + null_ptr = POINTER(c_int)() + with self.assertRaisesRegex(ValueError, "NULL pointer access"): + null_ptr[0] = 1 + + def test_set_pointer_to_null_and_read(self): + class Bar(Structure): + _fields_ = [("values", POINTER(c_int))] + + bar = Bar() + bar.values = (c_int * 3)(1, 2, 3) + + values = [bar.values[0], bar.values[1], bar.values[2]] + self.assertEqual(values, [1, 2, 3]) + + bar.values = None + with self.assertRaisesRegex(ValueError, "NULL pointer access"): + bar.values[0] + def test_pointers_bool(self): # NULL pointers have a boolean False value, non-NULL pointers True. self.assertEqual(bool(POINTER(c_int)()), False) @@ -210,20 +247,220 @@ def test_pointer_type_name(self): LargeNamedType = type('T' * 2 ** 25, (Structure,), {}) self.assertTrue(POINTER(LargeNamedType)) - # to not leak references, we must clean _pointer_type_cache - del _pointer_type_cache[LargeNamedType] - def test_pointer_type_str_name(self): large_string = 'T' * 2 ** 25 - P = POINTER(large_string) + with self.assertWarns(DeprecationWarning): + P = POINTER(large_string) self.assertTrue(P) - # to not leak references, we must clean _pointer_type_cache - del _pointer_type_cache[id(P)] - def test_abstract(self): self.assertRaises(TypeError, _Pointer.set_type, 42) + def test_pointer_types_equal(self): + t1 = POINTER(c_int) + t2 = POINTER(c_int) + + self.assertIs(t1, t2) + + p1 = t1(c_int(1)) + p2 = pointer(c_int(1)) + + self.assertIsInstance(p1, t1) + self.assertIsInstance(p2, t1) + + self.assertIs(type(p1), t1) + self.assertIs(type(p2), t1) + + def test_incomplete_pointer_types_still_equal(self): + with self.assertWarns(DeprecationWarning): + t1 = POINTER("LP_C") + with self.assertWarns(DeprecationWarning): + t2 = POINTER("LP_C") + + self.assertIs(t1, t2) + + def test_incomplete_pointer_types_cannot_instantiate(self): + with self.assertWarns(DeprecationWarning): + t1 = POINTER("LP_C") + with self.assertRaisesRegex(TypeError, "has no _type_"): + t1() + + def test_pointer_set_type_twice(self): + t1 = POINTER(c_int) + self.assertIs(c_int.__pointer_type__, t1) + self.assertIs(t1._type_, c_int) + + t1.set_type(c_int) + self.assertIs(c_int.__pointer_type__, t1) + self.assertIs(t1._type_, c_int) + + def test_pointer_set_wrong_type(self): + int_ptr = POINTER(c_int) + float_ptr = POINTER(c_float) + try: + class C(c_int): + pass + + t1 = POINTER(c_int) + t2 = POINTER(c_float) + t1.set_type(c_float) + self.assertEqual(t1(c_float(1.5))[0], 1.5) + self.assertIs(t1._type_, c_float) + self.assertIs(c_int.__pointer_type__, t1) + self.assertIs(c_float.__pointer_type__, float_ptr) + + t1.set_type(C) + self.assertEqual(t1(C(123))[0].value, 123) + self.assertIs(c_int.__pointer_type__, t1) + self.assertIs(c_float.__pointer_type__, float_ptr) + finally: + POINTER(c_int).set_type(c_int) + self.assertIs(POINTER(c_int), int_ptr) + self.assertIs(POINTER(c_int)._type_, c_int) + self.assertIs(c_int.__pointer_type__, int_ptr) + + def test_pointer_not_ctypes_type(self): + with self.assertRaisesRegex(TypeError, "must have storage info"): + POINTER(int) + + with self.assertRaisesRegex(TypeError, "must have storage info"): + pointer(int) + + with self.assertRaisesRegex(TypeError, "must have storage info"): + pointer(int(1)) + + def test_pointer_set_python_type(self): + p1 = POINTER(c_int) + with self.assertRaisesRegex(TypeError, "must have storage info"): + p1.set_type(int) + + def test_pointer_type_attribute_is_none(self): + class Cls(Structure): + _fields_ = ( + ('a', c_int), + ('b', c_float), + ) + + with self.assertRaisesRegex(AttributeError, ".Cls'> has no attribute '__pointer_type__'"): + Cls.__pointer_type__ + + p = POINTER(Cls) + self.assertIs(Cls.__pointer_type__, p) + + def test_arbitrary_pointer_type_attribute(self): + class Cls(Structure): + _fields_ = ( + ('a', c_int), + ('b', c_float), + ) + + garbage = 'garbage' + + P = POINTER(Cls) + self.assertIs(Cls.__pointer_type__, P) + Cls.__pointer_type__ = garbage + self.assertIs(Cls.__pointer_type__, garbage) + self.assertIs(POINTER(Cls), garbage) + self.assertIs(P._type_, Cls) + + instance = Cls(1, 2.0) + pointer = P(instance) + self.assertEqual(pointer[0].a, 1) + self.assertEqual(pointer[0].b, 2) + + del Cls.__pointer_type__ + + NewP = POINTER(Cls) + self.assertIsNot(NewP, P) + self.assertIs(Cls.__pointer_type__, NewP) + self.assertIs(P._type_, Cls) + + def test_pointer_types_factory(self): + """Shouldn't leak""" + def factory(): + class Cls(Structure): + _fields_ = ( + ('a', c_int), + ('b', c_float), + ) + + return Cls + + ws_typ = WeakSet() + ws_ptr = WeakSet() + for _ in range(10): + typ = factory() + ptr = POINTER(typ) + + ws_typ.add(typ) + ws_ptr.add(ptr) + + typ = None + ptr = None + + gc.collect() + + self.assertEqual(len(ws_typ), 0, ws_typ) + self.assertEqual(len(ws_ptr), 0, ws_ptr) + + +class PointerTypeCacheTestCase(unittest.TestCase): + # dummy tests to check warnings and base behavior + def tearDown(self): + _pointer_type_cache_fallback.clear() + + def test_deprecated_cache_with_not_ctypes_type(self): + class C: + pass + + with self.assertWarns(DeprecationWarning): + P = POINTER("C") + + with self.assertWarns(DeprecationWarning): + self.assertIs(_pointer_type_cache["C"], P) + + with self.assertWarns(DeprecationWarning): + _pointer_type_cache[C] = P + self.assertIs(C.__pointer_type__, P) + with self.assertWarns(DeprecationWarning): + self.assertIs(_pointer_type_cache[C], P) + + def test_deprecated_cache_with_ints(self): + with self.assertWarns(DeprecationWarning): + _pointer_type_cache[123] = 456 + + with self.assertWarns(DeprecationWarning): + self.assertEqual(_pointer_type_cache[123], 456) + + def test_deprecated_cache_with_ctypes_type(self): + class C(Structure): + _fields_ = [("a", c_int), + ("b", c_int), + ("c", c_int)] + + P1 = POINTER(C) + with self.assertWarns(DeprecationWarning): + P2 = POINTER("C") + + with self.assertWarns(DeprecationWarning): + _pointer_type_cache[C] = P2 + + self.assertIs(C.__pointer_type__, P2) + self.assertIsNot(C.__pointer_type__, P1) + + with self.assertWarns(DeprecationWarning): + self.assertIs(_pointer_type_cache[C], P2) + + with self.assertWarns(DeprecationWarning): + self.assertIs(_pointer_type_cache.get(C), P2) + + def test_get_not_registered(self): + with self.assertWarns(DeprecationWarning): + self.assertIsNone(_pointer_type_cache.get(str)) + + with self.assertWarns(DeprecationWarning): + self.assertIsNone(_pointer_type_cache.get(str, None)) + def test_repeated_set_type(self): # Regression test for gh-133290 class C(Structure): diff --git a/Lib/test/test_ctypes/test_structures.py b/Lib/test/test_ctypes/test_structures.py index bd7aba6376d167..221319642e8f3b 100644 --- a/Lib/test/test_ctypes/test_structures.py +++ b/Lib/test/test_ctypes/test_structures.py @@ -685,6 +685,30 @@ class Test8(Union): self.assertEqual(ctx.exception.args[0], 'item 1 in _argtypes_ passes ' 'a union by value, which is unsupported.') + def test_do_not_share_pointer_type_cache_via_stginfo_clone(self): + # This test case calls PyCStgInfo_clone() + # for the Mid and Vector class definitions + # and checks that pointer_type cache not shared + # between subclasses. + class Base(Structure): + _fields_ = [('y', c_double), + ('x', c_double)] + base_ptr = POINTER(Base) + + class Mid(Base): + pass + Mid._fields_ = [] + mid_ptr = POINTER(Mid) + + class Vector(Mid): + pass + + vector_ptr = POINTER(Vector) + + self.assertIsNot(base_ptr, mid_ptr) + self.assertIsNot(base_ptr, vector_ptr) + self.assertIsNot(mid_ptr, vector_ptr) + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_ctypes/test_values.py b/Lib/test/test_ctypes/test_values.py index 1e209797606275..8d1ee25ace5479 100644 --- a/Lib/test/test_ctypes/test_values.py +++ b/Lib/test/test_ctypes/test_values.py @@ -7,7 +7,6 @@ import sys import unittest from ctypes import (Structure, CDLL, POINTER, pythonapi, - _pointer_type_cache, c_ubyte, c_char_p, c_int) from test.support import import_helper, thread_unsafe @@ -98,8 +97,6 @@ class struct_frozen(Structure): "_PyImport_FrozenBootstrap example " "in Doc/library/ctypes.rst may be out of date") - del _pointer_type_cache[struct_frozen] - def test_undefined(self): self.assertRaises(ValueError, c_int.in_dll, pythonapi, "Undefined_Symbol") diff --git a/Lib/test/test_ctypes/test_win32.py b/Lib/test/test_ctypes/test_win32.py index 54b47dc28fbc73..7d5133221906bb 100644 --- a/Lib/test/test_ctypes/test_win32.py +++ b/Lib/test/test_ctypes/test_win32.py @@ -5,7 +5,6 @@ import sys import unittest from ctypes import (CDLL, Structure, POINTER, pointer, sizeof, byref, - _pointer_type_cache, c_void_p, c_char, c_int, c_long) from test import support from test.support import import_helper @@ -145,8 +144,8 @@ class RECT(Structure): self.assertEqual(ret.top, top.value) self.assertEqual(ret.bottom, bottom.value) - # to not leak references, we must clean _pointer_type_cache - del _pointer_type_cache[RECT] + self.assertIs(PointInRect.argtypes[0], ReturnRect.argtypes[2]) + self.assertIs(PointInRect.argtypes[0], ReturnRect.argtypes[5]) if __name__ == '__main__': diff --git a/Misc/NEWS.d/next/Library/2025-03-17-23-07-57.gh-issue-100926.B8gcbz.rst b/Misc/NEWS.d/next/Library/2025-03-17-23-07-57.gh-issue-100926.B8gcbz.rst new file mode 100644 index 00000000000000..6a71415fbd87c4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-17-23-07-57.gh-issue-100926.B8gcbz.rst @@ -0,0 +1,4 @@ +Move :func:`ctypes.POINTER` types cache from a global internal cache +(``_pointer_type_cache``) to the :attr:`ctypes._CData.__pointer_type__` +attribute of the corresponding :mod:`ctypes` types. +This will stop the cache from growing without limits in some situations. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 3605ca9007c9f8..1bb65e0a64920d 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -478,6 +478,7 @@ CType_Type_traverse(PyObject *self, visitproc visit, void *arg) Py_VISIT(info->converters); Py_VISIT(info->restype); Py_VISIT(info->checker); + Py_VISIT(info->pointer_type); Py_VISIT(info->module); } Py_VISIT(Py_TYPE(self)); @@ -493,7 +494,22 @@ ctype_clear_stginfo(StgInfo *info) Py_CLEAR(info->converters); Py_CLEAR(info->restype); Py_CLEAR(info->checker); - Py_CLEAR(info->module); + Py_CLEAR(info->pointer_type); + Py_CLEAR(info->module); // decref the module last +} + +void +ctype_free_stginfo_members(StgInfo *info) +{ + assert(info); + + PyMem_Free(info->ffi_type_pointer.elements); + info->ffi_type_pointer.elements = NULL; + PyMem_Free(info->format); + info->format = NULL; + PyMem_Free(info->shape); + info->shape = NULL; + ctype_clear_stginfo(info); } static int @@ -519,13 +535,7 @@ CType_Type_dealloc(PyObject *self) "deallocating ctypes %R", self); } if (info) { - PyMem_Free(info->ffi_type_pointer.elements); - info->ffi_type_pointer.elements = NULL; - PyMem_Free(info->format); - info->format = NULL; - PyMem_Free(info->shape); - info->shape = NULL; - ctype_clear_stginfo(info); + ctype_free_stginfo_members(info); } PyTypeObject *tp = Py_TYPE(self); @@ -566,6 +576,46 @@ _ctypes_CType_Type___sizeof___impl(PyObject *self, PyTypeObject *cls) return PyLong_FromSsize_t(size); } +static PyObject * +ctype_get_pointer_type(PyObject *self, void *Py_UNUSED(ignored)) +{ + ctypes_state *st = get_module_state_by_def(Py_TYPE(self)); + StgInfo *info; + if (PyStgInfo_FromType(st, self, &info) < 0) { + return NULL; + } + if (!info) { + PyErr_Format(PyExc_TypeError, "%R must have storage info", self); + return NULL; + } + + if (info->pointer_type) { + return Py_NewRef(info->pointer_type); + } + + PyErr_Format(PyExc_AttributeError, + "%R has no attribute '__pointer_type__'", + self); + return NULL; +} + +static int +ctype_set_pointer_type(PyObject *self, PyObject *tp, void *Py_UNUSED(ignored)) +{ + ctypes_state *st = get_module_state_by_def(Py_TYPE(self)); + StgInfo *info; + if (PyStgInfo_FromType(st, self, &info) < 0) { + return -1; + } + if (!info) { + PyErr_Format(PyExc_TypeError, "%R must have storage info", self); + return -1; + } + + Py_XSETREF(info->pointer_type, Py_XNewRef(tp)); + return 0; +} + static PyObject * CType_Type_repeat(PyObject *self, Py_ssize_t length); @@ -575,12 +625,19 @@ static PyMethodDef ctype_methods[] = { {0}, }; +static PyGetSetDef ctype_getsets[] = { + { "__pointer_type__", ctype_get_pointer_type, ctype_set_pointer_type, + "pointer type", NULL }, + { NULL, NULL } +}; + static PyType_Slot ctype_type_slots[] = { {Py_tp_token, Py_TP_USE_SPEC}, {Py_tp_traverse, CType_Type_traverse}, {Py_tp_clear, CType_Type_clear}, {Py_tp_dealloc, CType_Type_dealloc}, {Py_tp_methods, ctype_methods}, + {Py_tp_getset, ctype_getsets}, // Sequence protocol. {Py_sq_repeat, CType_Type_repeat}, {0, NULL}, @@ -1181,7 +1238,7 @@ class _ctypes.PyCPointerType "PyObject *" "clinic_state()->PyCPointerType_Type" static int -PyCPointerType_SetProto(ctypes_state *st, StgInfo *stginfo, PyObject *proto) +PyCPointerType_SetProto(ctypes_state *st, PyObject *self, StgInfo *stginfo, PyObject *proto) { if (!proto || !PyType_Check(proto)) { PyErr_SetString(PyExc_TypeError, @@ -1193,12 +1250,13 @@ PyCPointerType_SetProto(ctypes_state *st, StgInfo *stginfo, PyObject *proto) return -1; } if (!info) { - PyErr_SetString(PyExc_TypeError, - "_type_ must have storage info"); + PyErr_Format(PyExc_TypeError, "%R must have storage info", proto); return -1; } - Py_INCREF(proto); - Py_XSETREF(stginfo->proto, proto); + Py_XSETREF(stginfo->proto, Py_NewRef(proto)); + if (info->pointer_type == NULL) { + Py_XSETREF(info->pointer_type, Py_NewRef(self)); + } return 0; } @@ -1251,7 +1309,7 @@ PyCPointerType_init(PyObject *self, PyObject *args, PyObject *kwds) } if (proto) { const char *current_format; - if (PyCPointerType_SetProto(st, stginfo, proto) < 0) { + if (PyCPointerType_SetProto(st, self, stginfo, proto) < 0) { Py_DECREF(proto); return -1; } @@ -1309,10 +1367,9 @@ PyCPointerType_set_type_impl(PyTypeObject *self, PyTypeObject *cls, return NULL; } - if (PyCPointerType_SetProto(st, info, type) < 0) { + if (PyCPointerType_SetProto(st, (PyObject *)self, info, type) < 0) { return NULL; } - if (PyObject_SetAttr((PyObject *)self, &_Py_ID(_type_), type) < 0) { return NULL; } @@ -6206,7 +6263,6 @@ _ctypes_add_objects(PyObject *mod) } while (0) ctypes_state *st = get_module_state(mod); - MOD_ADD("_pointer_type_cache", Py_NewRef(st->_ctypes_ptrtype_cache)); #ifdef MS_WIN32 MOD_ADD("COMError", Py_NewRef(st->PyComError_Type)); @@ -6269,11 +6325,6 @@ _ctypes_mod_exec(PyObject *mod) return -1; } - st->_ctypes_ptrtype_cache = PyDict_New(); - if (st->_ctypes_ptrtype_cache == NULL) { - return -1; - } - st->PyExc_ArgError = PyErr_NewException("ctypes.ArgumentError", NULL, NULL); if (!st->PyExc_ArgError) { return -1; @@ -6312,7 +6363,6 @@ _ctypes_mod_exec(PyObject *mod) static int module_traverse(PyObject *module, visitproc visit, void *arg) { ctypes_state *st = get_module_state(module); - Py_VISIT(st->_ctypes_ptrtype_cache); Py_VISIT(st->_unpickle); Py_VISIT(st->array_cache); Py_VISIT(st->error_object_name); @@ -6347,7 +6397,6 @@ module_traverse(PyObject *module, visitproc visit, void *arg) { static int module_clear(PyObject *module) { ctypes_state *st = get_module_state(module); - Py_CLEAR(st->_ctypes_ptrtype_cache); Py_CLEAR(st->_unpickle); Py_CLEAR(st->array_cache); Py_CLEAR(st->error_object_name); diff --git a/Modules/_ctypes/callproc.c b/Modules/_ctypes/callproc.c index 83471aa3a42ad2..fc7265cb63ed9e 100644 --- a/Modules/_ctypes/callproc.c +++ b/Modules/_ctypes/callproc.c @@ -1972,105 +1972,6 @@ unpickle(PyObject *self, PyObject *args) return NULL; } -/*[clinic input] -_ctypes.POINTER as create_pointer_type - - type as cls: object - A ctypes type. - / - -Create and return a new ctypes pointer type. - -Pointer types are cached and reused internally, -so calling this function repeatedly is cheap. -[clinic start generated code]*/ - -static PyObject * -create_pointer_type(PyObject *module, PyObject *cls) -/*[clinic end generated code: output=98c3547ab6f4f40b input=3b81cff5ff9b9d5b]*/ -{ - PyObject *result; - PyTypeObject *typ; - PyObject *key; - - assert(module); - ctypes_state *st = get_module_state(module); - if (PyDict_GetItemRef(st->_ctypes_ptrtype_cache, cls, &result) != 0) { - // found or error - return result; - } - // not found - if (PyUnicode_CheckExact(cls)) { - PyObject *name = PyUnicode_FromFormat("LP_%U", cls); - result = PyObject_CallFunction((PyObject *)Py_TYPE(st->PyCPointer_Type), - "N(O){}", - name, - st->PyCPointer_Type); - if (result == NULL) - return result; - key = PyLong_FromVoidPtr(result); - if (key == NULL) { - Py_DECREF(result); - return NULL; - } - } else if (PyType_Check(cls)) { - typ = (PyTypeObject *)cls; - PyObject *name = PyUnicode_FromFormat("LP_%s", typ->tp_name); - result = PyObject_CallFunction((PyObject *)Py_TYPE(st->PyCPointer_Type), - "N(O){sO}", - name, - st->PyCPointer_Type, - "_type_", cls); - if (result == NULL) - return result; - key = Py_NewRef(cls); - } else { - PyErr_SetString(PyExc_TypeError, "must be a ctypes type"); - return NULL; - } - if (PyDict_SetItem(st->_ctypes_ptrtype_cache, key, result) < 0) { - Py_DECREF(result); - Py_DECREF(key); - return NULL; - } - Py_DECREF(key); - return result; -} - -/*[clinic input] -_ctypes.pointer as create_pointer_inst - - obj as arg: object - / - -Create a new pointer instance, pointing to 'obj'. - -The returned object is of the type POINTER(type(obj)). Note that if you -just want to pass a pointer to an object to a foreign function call, you -should use byref(obj) which is much faster. -[clinic start generated code]*/ - -static PyObject * -create_pointer_inst(PyObject *module, PyObject *arg) -/*[clinic end generated code: output=3b543bc9f0de2180 input=713685fdb4d9bc27]*/ -{ - PyObject *result; - PyObject *typ; - - ctypes_state *st = get_module_state(module); - if (PyDict_GetItemRef(st->_ctypes_ptrtype_cache, (PyObject *)Py_TYPE(arg), &typ) < 0) { - return NULL; - } - if (typ == NULL) { - typ = create_pointer_type(module, (PyObject *)Py_TYPE(arg)); - if (typ == NULL) - return NULL; - } - result = PyObject_CallOneArg(typ, arg); - Py_DECREF(typ); - return result; -} - static PyObject * buffer_info(PyObject *self, PyObject *arg) { @@ -2105,8 +2006,6 @@ buffer_info(PyObject *self, PyObject *arg) PyMethodDef _ctypes_module_methods[] = { {"get_errno", get_errno, METH_NOARGS}, {"set_errno", set_errno, METH_VARARGS}, - CREATE_POINTER_TYPE_METHODDEF - CREATE_POINTER_INST_METHODDEF {"_unpickle", unpickle, METH_VARARGS }, {"buffer_info", buffer_info, METH_O, "Return buffer interface information"}, _CTYPES_RESIZE_METHODDEF diff --git a/Modules/_ctypes/clinic/callproc.c.h b/Modules/_ctypes/clinic/callproc.c.h index 8a5c8f6427b7f0..e0cfcc6f38def7 100644 --- a/Modules/_ctypes/clinic/callproc.c.h +++ b/Modules/_ctypes/clinic/callproc.c.h @@ -142,32 +142,4 @@ _ctypes_resize(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } - -PyDoc_STRVAR(create_pointer_type__doc__, -"POINTER($module, type, /)\n" -"--\n" -"\n" -"Create and return a new ctypes pointer type.\n" -"\n" -" type\n" -" A ctypes type.\n" -"\n" -"Pointer types are cached and reused internally,\n" -"so calling this function repeatedly is cheap."); - -#define CREATE_POINTER_TYPE_METHODDEF \ - {"POINTER", (PyCFunction)create_pointer_type, METH_O, create_pointer_type__doc__}, - -PyDoc_STRVAR(create_pointer_inst__doc__, -"pointer($module, obj, /)\n" -"--\n" -"\n" -"Create a new pointer instance, pointing to \'obj\'.\n" -"\n" -"The returned object is of the type POINTER(type(obj)). Note that if you\n" -"just want to pass a pointer to an object to a foreign function call, you\n" -"should use byref(obj) which is much faster."); - -#define CREATE_POINTER_INST_METHODDEF \ - {"pointer", (PyCFunction)create_pointer_inst, METH_O, create_pointer_inst__doc__}, -/*[clinic end generated code: output=46a3841cbe5ddc96 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=23c74aced603977d input=a9049054013a1b77]*/ diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 3b6d390728a07f..9aceeceb88a49f 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -83,8 +83,6 @@ typedef struct { #ifdef MS_WIN32 PyTypeObject *PyComError_Type; #endif - /* This dict maps ctypes types to POINTER types */ - PyObject *_ctypes_ptrtype_cache; /* a callable object used for unpickling: strong reference to _ctypes._unpickle() function */ PyObject *_unpickle; @@ -390,6 +388,8 @@ typedef struct { PyObject *converters; /* tuple([t.from_param for t in argtypes]) */ PyObject *restype; /* CDataObject or NULL */ PyObject *checker; + PyObject *pointer_type; /* __pointer_type__ attribute; + arbitrary object or NULL */ PyObject *module; int flags; /* calling convention and such */ #ifdef Py_GIL_DISABLED @@ -452,6 +452,7 @@ stginfo_set_dict_final(StgInfo *info) extern int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info); extern void ctype_clear_stginfo(StgInfo *info); +extern void ctype_free_stginfo_members(StgInfo *info); typedef int(* PPROC)(void); @@ -640,6 +641,7 @@ PyStgInfo_Init(ctypes_state *state, PyTypeObject *type) if (!module) { return NULL; } + info->pointer_type = NULL; info->module = Py_NewRef(module); info->initialized = 1; diff --git a/Modules/_ctypes/stgdict.c b/Modules/_ctypes/stgdict.c index c4c4ec2cc5266a..f208d2956e429e 100644 --- a/Modules/_ctypes/stgdict.c +++ b/Modules/_ctypes/stgdict.c @@ -25,13 +25,7 @@ PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info) { Py_ssize_t size; - ctype_clear_stginfo(dst_info); - PyMem_Free(dst_info->ffi_type_pointer.elements); - PyMem_Free(dst_info->format); - dst_info->format = NULL; - PyMem_Free(dst_info->shape); - dst_info->shape = NULL; - dst_info->ffi_type_pointer.elements = NULL; + ctype_free_stginfo_members(dst_info); memcpy(dst_info, src_info, sizeof(StgInfo)); #ifdef Py_GIL_DISABLED @@ -45,6 +39,7 @@ PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info) Py_XINCREF(dst_info->restype); Py_XINCREF(dst_info->checker); Py_XINCREF(dst_info->module); + dst_info->pointer_type = NULL; // the cache cannot be shared if (src_info->format) { dst_info->format = PyMem_Malloc(strlen(src_info->format) + 1);