Skip to content

Commit 3f09229

Browse files
gh-78327: Use PyObject_HashNotImplemented for ctypes data types (GH-159736)
1 parent 630920e commit 3f09229

3 files changed

Lines changed: 64 additions & 31 deletions

File tree

‎Lib/test/test_ctypes/test_simplesubclasses.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
2-
from ctypes import Structure, CFUNCTYPE, c_int, _SimpleCData
2+
from collections.abc import Hashable
3+
from ctypes import Structure, CFUNCTYPE, c_int, _SimpleCData, POINTER
34
from ._support import (_CData, PyCSimpleType, Py_TPFLAGS_DISALLOW_INSTANTIATION,
45
Py_TPFLAGS_IMMUTABLETYPE)
56

@@ -26,6 +27,28 @@ def test_type_flags(self):
2627
self.assertTrue(_SimpleCData.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
2728
self.assertFalse(_SimpleCData.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
2829

30+
def test_unhashable(self):
31+
# gh-78327: CData objects are mutable and must not be hashable.
32+
class S(Structure):
33+
_fields_ = [("a", c_int)]
34+
35+
for cls in c_int, S, c_int * 2, POINTER(c_int), CFUNCTYPE(None):
36+
with self.subTest(cls=cls):
37+
self.assertIsNone(cls.__hash__)
38+
obj = cls()
39+
self.assertNotIsInstance(obj, Hashable)
40+
with self.assertRaisesRegex(TypeError,
41+
f"unhashable type: '{cls.__name__}'"):
42+
hash(obj)
43+
44+
# Subclasses can still opt in to hashing.
45+
class H(S):
46+
def __hash__(self):
47+
return 42
48+
49+
self.assertEqual(hash(H()), 42)
50+
self.assertIsInstance(H(), Hashable)
51+
2952
def test_metaclass_details(self):
3053
# Abstract classes (whose metaclass __init__ was not called) can't be
3154
# instantiated directly
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`ctypes` data types now set :meth:`~object.__hash__` to ``None``, so
2+
:class:`collections.abc.Hashable` checks report them correctly.

‎Modules/_ctypes/_ctypes.c‎

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,16 +3105,6 @@ PyCData_NewGetBuffer(PyObject *myself, Py_buffer *view, int flags)
31053105
return 0;
31063106
}
31073107

3108-
/*
3109-
* CData objects are mutable, so they cannot be hashable!
3110-
*/
3111-
static Py_hash_t
3112-
PyCData_nohash(PyObject *self)
3113-
{
3114-
PyErr_SetString(PyExc_TypeError, "unhashable type");
3115-
return -1;
3116-
}
3117-
31183108
/*[clinic input]
31193109
@critical_section
31203110
_ctypes.PyCData.__reduce__
@@ -3211,25 +3201,40 @@ static PyMethodDef PyCData_methods[] = {
32113201
{ NULL, NULL },
32123202
};
32133203

3214-
static PyType_Slot pycdata_slots[] = {
3215-
{Py_tp_dealloc, PyCData_dealloc},
3216-
{Py_tp_hash, PyCData_nohash},
3217-
{Py_tp_doc, PyDoc_STR("XXX to be provided")},
3218-
{Py_tp_traverse, PyCData_traverse},
3219-
{Py_tp_clear, PyCData_clear},
3220-
{Py_tp_methods, PyCData_methods},
3221-
{Py_tp_members, PyCData_members},
3222-
{Py_bf_getbuffer, PyCData_NewGetBuffer},
3223-
{0, NULL},
3224-
};
3225-
3226-
static PyType_Spec pycdata_spec = {
3227-
.name = "_ctypes._CData",
3228-
.basicsize = sizeof(CDataObject),
3229-
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
3230-
Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_DISALLOW_INSTANTIATION),
3231-
.slots = pycdata_slots,
3232-
};
3204+
/*
3205+
* CData objects are mutable, so they cannot be hashable. Setting tp_hash to
3206+
* PyObject_HashNotImplemented() makes type_ready() set __hash__ to None.
3207+
*
3208+
* Unfortunately, on Windows, PyObject_HashNotImplemented is imported from
3209+
* the Python DLL, and MSVC resolves the address of an imported function in
3210+
* a static initializer to a local thunk that can't be recognized. So, we
3211+
* initialize the slots at runtime to load the real address from the import table.
3212+
*/
3213+
static PyObject *
3214+
PyCData_create_type(PyObject *mod)
3215+
{
3216+
PyType_Slot slots[] = {
3217+
{Py_tp_dealloc, PyCData_dealloc},
3218+
{Py_tp_hash, PyObject_HashNotImplemented},
3219+
{Py_tp_doc, PyDoc_STR("The common base class of all ctypes "
3220+
"data types.")},
3221+
{Py_tp_traverse, PyCData_traverse},
3222+
{Py_tp_clear, PyCData_clear},
3223+
{Py_tp_methods, PyCData_methods},
3224+
{Py_tp_members, PyCData_members},
3225+
{Py_bf_getbuffer, PyCData_NewGetBuffer},
3226+
{0, NULL},
3227+
};
3228+
PyType_Spec spec = {
3229+
.name = "_ctypes._CData",
3230+
.basicsize = sizeof(CDataObject),
3231+
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
3232+
Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE |
3233+
Py_TPFLAGS_DISALLOW_INSTANTIATION),
3234+
.slots = slots,
3235+
};
3236+
return PyType_FromMetaclass(NULL, mod, &spec, NULL);
3237+
}
32333238

32343239
static int
32353240
PyCData_MallocBuffer(CDataObject *obj, StgInfo *info)
@@ -6249,7 +6254,10 @@ _ctypes_add_types(PyObject *mod)
62496254
*/
62506255
CREATE_TYPE(st->PyCArg_Type, &carg_spec, NULL, NULL);
62516256
CREATE_TYPE(st->PyCThunk_Type, &cthunk_spec, NULL, NULL);
6252-
CREATE_TYPE(st->PyCData_Type, &pycdata_spec, NULL, NULL);
6257+
st->PyCData_Type = (PyTypeObject *)PyCData_create_type(mod);
6258+
if (st->PyCData_Type == NULL) {
6259+
return -1;
6260+
}
62536261

62546262
// Common Metaclass
62556263
CREATE_TYPE(st->PyCType_Type, &pyctype_type_spec,

0 commit comments

Comments
 (0)