gh-100926: Move ctype's pointers cache from _pointer_type_cache to StgInfo (GH-131282)

Deprecate _pointer_type_cache and calling POINTER on a string.

Co-authored-by: neonene <53406459+neonene@users.noreply.github.com>
Co-authored-by: Jun Komoda <45822440+junkmd@users.noreply.github.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
Sergey Miryanov 2025-05-02 10:06:37 -07:00 committed by GitHub
parent 7e7e49be78
commit a0bc0c462f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 697 additions and 213 deletions

View file

@ -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_

View file

@ -807,11 +807,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
--------
@ -1679,6 +1684,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.

View file

@ -266,7 +266,72 @@ _check_size(c_void_p)
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

View file

@ -232,7 +232,6 @@ class Test(unittest.TestCase, StructCheckMixin):
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 Test(unittest.TestCase, StructCheckMixin):
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)

View file

@ -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
def set_non_ctypes_pointer_type(cls, pointer_type):
cls.__pointer_type__ = pointer_type
class PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
def tearDown(self):
# to not leak references, we must clean _pointer_type_cache
ctypes._reset_cache()
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 @@ class PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 @@ class PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 @@ class PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 @@ class PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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 PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
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))

View file

@ -3,15 +3,20 @@ import unittest
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 TestSetPointerType(unittest.TestCase):
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 TestSetPointerType(unittest.TestCase):
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 TestSetPointerType(unittest.TestCase):
with self.assertWarns(DeprecationWarning):
ctypes.SetPointerType(lpcell, cell)
self.assertIs(POINTER(cell), lpcell)
if __name__ == '__main__':
unittest.main()

View file

@ -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 PointerToStructure(unittest.TestCase):
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()

View file

@ -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 @@ python_types = [int, int, int, int, int, int,
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 @@ class PointersTestCase(unittest.TestCase):
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 PointersTestCase(unittest.TestCase):
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 @@ class PointersTestCase(unittest.TestCase):
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 @@ class PointersTestCase(unittest.TestCase):
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 @@ class PointersTestCase(unittest.TestCase):
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):

View file

@ -685,6 +685,30 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin):
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()

View file

@ -7,7 +7,6 @@ import importlib.util
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 PythonValuesTestCase(unittest.TestCase):
"_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")

View file

@ -5,7 +5,6 @@ import errno
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 Structures(unittest.TestCase):
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__':

View file

@ -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.

View file

@ -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);

View file

@ -1972,105 +1972,6 @@ error:
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

View file

@ -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]*/

View file

@ -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;

View file

@ -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);