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.
This commit is contained in:
Victor Stinner 2019-08-30 14:30:33 +02:00 committed by GitHub
parent 6a650aaf77
commit 96b4087ce7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 109 additions and 20 deletions

View File

@ -3,7 +3,7 @@ from ctypes import *
from ctypes.test import need_symbol from ctypes.test import need_symbol
from struct import calcsize from struct import calcsize
import _ctypes_test import _ctypes_test
import test.support from test import support
class SubclassesTest(unittest.TestCase): class SubclassesTest(unittest.TestCase):
def test_subclass(self): def test_subclass(self):
@ -202,7 +202,7 @@ class StructureTestCase(unittest.TestCase):
"_pack_": -1} "_pack_": -1}
self.assertRaises(ValueError, type(Structure), "X", (Structure,), d) self.assertRaises(ValueError, type(Structure), "X", (Structure,), d)
@test.support.cpython_only @support.cpython_only
def test_packed_c_limits(self): def test_packed_c_limits(self):
# Issue 15989 # Issue 15989
import _testcapi import _testcapi
@ -396,27 +396,66 @@ class StructureTestCase(unittest.TestCase):
self.assertRaises(TypeError, lambda: Z(1, 2, 3, 4, 5, 6, 7)) self.assertRaises(TypeError, lambda: Z(1, 2, 3, 4, 5, 6, 7))
def test_pass_by_value(self): def test_pass_by_value(self):
# This should mirror the structure in Modules/_ctypes/_ctypes_test.c # This should mirror the Test structure
class X(Structure): # in Modules/_ctypes/_ctypes_test.c
class Test(Structure):
_fields_ = [ _fields_ = [
('first', c_ulong), ('first', c_ulong),
('second', c_ulong), ('second', c_ulong),
('third', c_ulong), ('third', c_ulong),
] ]
s = X() s = Test()
s.first = 0xdeadbeef s.first = 0xdeadbeef
s.second = 0xcafebabe s.second = 0xcafebabe
s.third = 0x0bad1dea s.third = 0x0bad1dea
dll = CDLL(_ctypes_test.__file__) dll = CDLL(_ctypes_test.__file__)
func = dll._testfunc_large_struct_update_value func = dll._testfunc_large_struct_update_value
func.argtypes = (X,) func.argtypes = (Test,)
func.restype = None func.restype = None
func(s) func(s)
self.assertEqual(s.first, 0xdeadbeef) self.assertEqual(s.first, 0xdeadbeef)
self.assertEqual(s.second, 0xcafebabe) self.assertEqual(s.second, 0xcafebabe)
self.assertEqual(s.third, 0x0bad1dea) self.assertEqual(s.third, 0x0bad1dea)
def test_pass_by_value_finalizer(self):
# bpo-37140: Similar to test_pass_by_value(), but the Python structure
# has a finalizer (__del__() method): the finalizer must only be called
# once.
finalizer_calls = []
class Test(Structure):
_fields_ = [
('first', c_ulong),
('second', c_ulong),
('third', c_ulong),
]
def __del__(self):
finalizer_calls.append("called")
s = Test(1, 2, 3)
# Test the StructUnionType_paramfunc() code path which copies the
# structure: if the stucture is larger than sizeof(void*).
self.assertGreater(sizeof(s), sizeof(c_void_p))
dll = CDLL(_ctypes_test.__file__)
func = dll._testfunc_large_struct_update_value
func.argtypes = (Test,)
func.restype = None
func(s)
# bpo-37140: Passing the structure by refrence must not call
# its finalizer!
self.assertEqual(finalizer_calls, [])
self.assertEqual(s.first, 1)
self.assertEqual(s.second, 2)
self.assertEqual(s.third, 3)
# The finalizer must be called exactly once
s = None
support.gc_collect()
self.assertEqual(finalizer_calls, ["called"])
def test_pass_by_value_in_register(self): def test_pass_by_value_in_register(self):
class X(Structure): class X(Structure):
_fields_ = [ _fields_ = [

View File

@ -0,0 +1,5 @@
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.

View File

@ -392,6 +392,35 @@ _ctypes_alloc_format_string_with_shape(int ndim, const Py_ssize_t *shape,
return result; return result;
} }
/* StructParamObject and StructParam_Type are used in _ctypes_callproc()
for argument.keep to call PyMem_Free(ptr) on Py_DECREF(argument).
StructUnionType_paramfunc() creates such object when a ctypes Structure is
passed by copy to a C function. */
typedef struct {
PyObject_HEAD
void *ptr;
} StructParamObject;
static void
StructParam_dealloc(PyObject *myself)
{
StructParamObject *self = (StructParamObject *)myself;
PyMem_Free(self->ptr);
Py_TYPE(self)->tp_free(myself);
}
static PyTypeObject StructParam_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_ctypes.StructParam_Type",
.tp_basicsize = sizeof(StructParamObject),
.tp_dealloc = StructParam_dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
};
/* /*
PyCStructType_Type - a meta type/class. Creating a new class using this one as PyCStructType_Type - a meta type/class. Creating a new class using this one as
__metaclass__ will call the constructor StructUnionType_new. It replaces the __metaclass__ will call the constructor StructUnionType_new. It replaces the
@ -403,35 +432,47 @@ static PyCArgObject *
StructUnionType_paramfunc(CDataObject *self) StructUnionType_paramfunc(CDataObject *self)
{ {
PyCArgObject *parg; PyCArgObject *parg;
CDataObject *copied_self; PyObject *obj;
StgDictObject *stgdict; StgDictObject *stgdict;
void *ptr;
if ((size_t)self->b_size > sizeof(void*)) { if ((size_t)self->b_size > sizeof(void*)) {
void *new_ptr = PyMem_Malloc(self->b_size); ptr = PyMem_Malloc(self->b_size);
if (new_ptr == NULL) if (ptr == NULL) {
return NULL; return NULL;
memcpy(new_ptr, self->b_ptr, self->b_size); }
copied_self = (CDataObject *)PyCData_AtAddress( memcpy(ptr, self->b_ptr, self->b_size);
(PyObject *)Py_TYPE(self), new_ptr);
copied_self->b_needsfree = 1; /* Create a Python object which calls PyMem_Free(ptr) in
its deallocator. The object will be destroyed
at _ctypes_callproc() cleanup. */
obj = (&StructParam_Type)->tp_alloc(&StructParam_Type, 0);
if (obj == NULL) {
PyMem_Free(ptr);
return NULL;
}
StructParamObject *struct_param = (StructParamObject *)obj;
struct_param->ptr = ptr;
} else { } else {
copied_self = self; ptr = self->b_ptr;
Py_INCREF(copied_self); obj = (PyObject *)self;
Py_INCREF(obj);
} }
parg = PyCArgObject_new(); parg = PyCArgObject_new();
if (parg == NULL) { if (parg == NULL) {
Py_DECREF(copied_self); Py_DECREF(obj);
return NULL; return NULL;
} }
parg->tag = 'V'; parg->tag = 'V';
stgdict = PyObject_stgdict((PyObject *)copied_self); stgdict = PyObject_stgdict((PyObject *)self);
assert(stgdict); /* Cannot be NULL for structure/union instances */ assert(stgdict); /* Cannot be NULL for structure/union instances */
parg->pffi_type = &stgdict->ffi_type_pointer; parg->pffi_type = &stgdict->ffi_type_pointer;
parg->value.p = copied_self->b_ptr; parg->value.p = ptr;
parg->size = copied_self->b_size; parg->size = self->b_size;
parg->obj = (PyObject *)copied_self; parg->obj = obj;
return parg; return parg;
} }
@ -5700,6 +5741,10 @@ PyInit__ctypes(void)
if (PyType_Ready(&DictRemover_Type) < 0) if (PyType_Ready(&DictRemover_Type) < 0)
return NULL; return NULL;
if (PyType_Ready(&StructParam_Type) < 0) {
return NULL;
}
#ifdef MS_WIN32 #ifdef MS_WIN32
if (create_comerror() < 0) if (create_comerror() < 0)
return NULL; return NULL;