Skip to content

Commit 96b4087

Browse files
authored
bpo-37140: Fix StructUnionType_paramfunc() (GH-15612)
Fix a ctypes regression of Python 3.8. When a ctypes.Structure is passed by copy to a function, ctypes internals created a temporary object which had the side effect of calling the structure finalizer (__del__) twice. The Python semantics requires a finalizer to be called exactly once. Fix ctypes internals to no longer call the finalizer twice. Create a new internal StructParam_Type which is only used by _ctypes_callproc() to call PyMem_Free(ptr) on Py_DECREF(argument). StructUnionType_paramfunc() creates such object.
1 parent 6a650aa commit 96b4087

File tree

3 files changed

+109
-20
lines changed

3 files changed

+109
-20
lines changed

Lib/ctypes/test/test_structures.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ctypes.test import need_symbol
44
from struct import calcsize
55
import _ctypes_test
6-
import test.support
6+
from test import support
77

88
class SubclassesTest(unittest.TestCase):
99
def test_subclass(self):
@@ -202,7 +202,7 @@ class X(Structure):
202202
"_pack_": -1}
203203
self.assertRaises(ValueError, type(Structure), "X", (Structure,), d)
204204

205-
@test.support.cpython_only
205+
@support.cpython_only
206206
def test_packed_c_limits(self):
207207
# Issue 15989
208208
import _testcapi
@@ -396,27 +396,66 @@ class Z(Y):
396396
self.assertRaises(TypeError, lambda: Z(1, 2, 3, 4, 5, 6, 7))
397397

398398
def test_pass_by_value(self):
399-
# This should mirror the structure in Modules/_ctypes/_ctypes_test.c
400-
class X(Structure):
399+
# This should mirror the Test structure
400+
# in Modules/_ctypes/_ctypes_test.c
401+
class Test(Structure):
401402
_fields_ = [
402403
('first', c_ulong),
403404
('second', c_ulong),
404405
('third', c_ulong),
405406
]
406407

407-
s = X()
408+
s = Test()
408409
s.first = 0xdeadbeef
409410
s.second = 0xcafebabe
410411
s.third = 0x0bad1dea
411412
dll = CDLL(_ctypes_test.__file__)
412413
func = dll._testfunc_large_struct_update_value
413-
func.argtypes = (X,)
414+
func.argtypes = (Test,)
414415
func.restype = None
415416
func(s)
416417
self.assertEqual(s.first, 0xdeadbeef)
417418
self.assertEqual(s.second, 0xcafebabe)
418419
self.assertEqual(s.third, 0x0bad1dea)
419420

421+
def test_pass_by_value_finalizer(self):
422+
# bpo-37140: Similar to test_pass_by_value(), but the Python structure
423+
# has a finalizer (__del__() method): the finalizer must only be called
424+
# once.
425+
426+
finalizer_calls = []
427+
428+
class Test(Structure):
429+
_fields_ = [
430+
('first', c_ulong),
431+
('second', c_ulong),
432+
('third', c_ulong),
433+
]
434+
def __del__(self):
435+
finalizer_calls.append("called")
436+
437+
s = Test(1, 2, 3)
438+
# Test the StructUnionType_paramfunc() code path which copies the
439+
# structure: if the stucture is larger than sizeof(void*).
440+
self.assertGreater(sizeof(s), sizeof(c_void_p))
441+
442+
dll = CDLL(_ctypes_test.__file__)
443+
func = dll._testfunc_large_struct_update_value
444+
func.argtypes = (Test,)
445+
func.restype = None
446+
func(s)
447+
# bpo-37140: Passing the structure by refrence must not call
448+
# its finalizer!
449+
self.assertEqual(finalizer_calls, [])
450+
self.assertEqual(s.first, 1)
451+
self.assertEqual(s.second, 2)
452+
self.assertEqual(s.third, 3)
453+
454+
# The finalizer must be called exactly once
455+
s = None
456+
support.gc_collect()
457+
self.assertEqual(finalizer_calls, ["called"])
458+
420459
def test_pass_by_value_in_register(self):
421460
class X(Structure):
422461
_fields_ = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a ctypes regression of Python 3.8. When a ctypes.Structure is passed by
2+
copy to a function, ctypes internals created a temporary object which had
3+
the side effect of calling the structure finalizer (__del__) twice. The
4+
Python semantics requires a finalizer to be called exactly once. Fix ctypes
5+
internals to no longer call the finalizer twice.

Modules/_ctypes/_ctypes.c

+59-14
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,35 @@ _ctypes_alloc_format_string_with_shape(int ndim, const Py_ssize_t *shape,
392392
return result;
393393
}
394394

395+
/* StructParamObject and StructParam_Type are used in _ctypes_callproc()
396+
for argument.keep to call PyMem_Free(ptr) on Py_DECREF(argument).
397+
398+
StructUnionType_paramfunc() creates such object when a ctypes Structure is
399+
passed by copy to a C function. */
400+
typedef struct {
401+
PyObject_HEAD
402+
void *ptr;
403+
} StructParamObject;
404+
405+
406+
static void
407+
StructParam_dealloc(PyObject *myself)
408+
{
409+
StructParamObject *self = (StructParamObject *)myself;
410+
PyMem_Free(self->ptr);
411+
Py_TYPE(self)->tp_free(myself);
412+
}
413+
414+
415+
static PyTypeObject StructParam_Type = {
416+
PyVarObject_HEAD_INIT(NULL, 0)
417+
.tp_name = "_ctypes.StructParam_Type",
418+
.tp_basicsize = sizeof(StructParamObject),
419+
.tp_dealloc = StructParam_dealloc,
420+
.tp_flags = Py_TPFLAGS_DEFAULT,
421+
};
422+
423+
395424
/*
396425
PyCStructType_Type - a meta type/class. Creating a new class using this one as
397426
__metaclass__ will call the constructor StructUnionType_new. It replaces the
@@ -403,35 +432,47 @@ static PyCArgObject *
403432
StructUnionType_paramfunc(CDataObject *self)
404433
{
405434
PyCArgObject *parg;
406-
CDataObject *copied_self;
435+
PyObject *obj;
407436
StgDictObject *stgdict;
437+
void *ptr;
408438

409439
if ((size_t)self->b_size > sizeof(void*)) {
410-
void *new_ptr = PyMem_Malloc(self->b_size);
411-
if (new_ptr == NULL)
440+
ptr = PyMem_Malloc(self->b_size);
441+
if (ptr == NULL) {
412442
return NULL;
413-
memcpy(new_ptr, self->b_ptr, self->b_size);
414-
copied_self = (CDataObject *)PyCData_AtAddress(
415-
(PyObject *)Py_TYPE(self), new_ptr);
416-
copied_self->b_needsfree = 1;
443+
}
444+
memcpy(ptr, self->b_ptr, self->b_size);
445+
446+
/* Create a Python object which calls PyMem_Free(ptr) in
447+
its deallocator. The object will be destroyed
448+
at _ctypes_callproc() cleanup. */
449+
obj = (&StructParam_Type)->tp_alloc(&StructParam_Type, 0);
450+
if (obj == NULL) {
451+
PyMem_Free(ptr);
452+
return NULL;
453+
}
454+
455+
StructParamObject *struct_param = (StructParamObject *)obj;
456+
struct_param->ptr = ptr;
417457
} else {
418-
copied_self = self;
419-
Py_INCREF(copied_self);
458+
ptr = self->b_ptr;
459+
obj = (PyObject *)self;
460+
Py_INCREF(obj);
420461
}
421462

422463
parg = PyCArgObject_new();
423464
if (parg == NULL) {
424-
Py_DECREF(copied_self);
465+
Py_DECREF(obj);
425466
return NULL;
426467
}
427468

428469
parg->tag = 'V';
429-
stgdict = PyObject_stgdict((PyObject *)copied_self);
470+
stgdict = PyObject_stgdict((PyObject *)self);
430471
assert(stgdict); /* Cannot be NULL for structure/union instances */
431472
parg->pffi_type = &stgdict->ffi_type_pointer;
432-
parg->value.p = copied_self->b_ptr;
433-
parg->size = copied_self->b_size;
434-
parg->obj = (PyObject *)copied_self;
473+
parg->value.p = ptr;
474+
parg->size = self->b_size;
475+
parg->obj = obj;
435476
return parg;
436477
}
437478

@@ -5700,6 +5741,10 @@ PyInit__ctypes(void)
57005741
if (PyType_Ready(&DictRemover_Type) < 0)
57015742
return NULL;
57025743

5744+
if (PyType_Ready(&StructParam_Type) < 0) {
5745+
return NULL;
5746+
}
5747+
57035748
#ifdef MS_WIN32
57045749
if (create_comerror() < 0)
57055750
return NULL;

0 commit comments

Comments
 (0)