8000 Add workaround for bpo-36880 to ctypes_patch · dgelessus/rubicon-objc@831e17b · GitHub
[go: up one dir, main page]

Skip to content

Commit 831e17b

Browse files
committed
Add workaround for bpo-36880 to ctypes_patch
Previously, our ctypes_patch would trigger a bug in ctypes (see https://bugs.python.org/issue36880 and python/cpython#13364), which would eventually crash Python if done too often. This workaround changes our ctypes_patch implementation so that it no longer triggers the ctypes bug. This fixes the crash reported in beeware/toga#549.
1 parent d615b71 commit 831e17b

File tree

2 files changed

+45
-2
lines changed

2 files changed

+45
-2
lines changed

rubicon/objc/ctypes_patch.py

Lines changed: 19 additions & 2 deletions
8000
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ class ffi_type(ctypes.Structure):
9999

100100
# The GETFUNC and SETFUNC typedefs from "Modules/_ctypes/ctypes.h".
101101
GETFUNC = ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p, ctypes.c_ssize_t)
102-
SETFUNC = ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p, ctypes.py_object, ctypes.c_ssize_t)
102+
# The return type of SETFUNC is declared here as a c_void_p instead of py_object to work around ctypes bug
103+
# bpo-36880 (https://bugs.python.org/issue36880). See the comment in make_callback_returnable's setfunc for details.
104+
SETFUNC = ctypes.PYFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.py_object, ctypes.c_ssize_t)
103105

104106

105107
# The StgDictObject structure from "Modules/_ctypes/ctypes.h".
@@ -119,6 +121,10 @@ class StgDictObject(ctypes.Structure):
119121
]
120122

121123

124+
ctypes.pythonapi.Py_IncRef.restype = None
125+
ctypes.pythonapi.Py_IncRef.argtypes = [ctypes.POINTER(PyObject)]
126+
127+
122128
def unwrap_mappingproxy(proxy):
123129
"""Return the mapping contained in a mapping proxy object."""
124130

@@ -208,7 +214,18 @@ def setfunc(ptr, value, size):
208214
)
209215

210216
ctypes.memmove(ptr, ctypes.addressof(value), actual_size)
211-
return None
217+
# Because of ctypes bug bpo-36880 (https://bugs.python.org/issue36880), returning None from a callback with
218+
# restype py_object causes a reference counting error that can crash Python.
219+
# To work around this bug, the restype of SETFUNC is declared as c_void_p instead.
220+
# This way ctypes performs no automatic reference counting for the returned object, which avoids the bug.
221+
# However, this way we have to manually convert the Python object to a pointer and adjust its reference count.
222+
none_ptr = ctypes.cast(id(None), ctypes.POINTER(PyObject))
223+
# The return value of a SETFUNC is expected to have an extra reference
224+
# (which will be owned by the caller of the SETFUNC).
225+
ctypes.pythonapi.Py_IncRef(none_ptr)
226+
# The returned pointer must be returned as a plain int, not as a c_void_p,
227+
# otherwise ctypes won't recognize it and will raise a TypeError.
228+
return ctypes.cast(none_ptr, ctypes.c_void_p).value
212229

213230
# Store the getfunc and setfunc as attributes on the ctype, so they don't get garbage-collected.
214231
ctype._rubicon_objc_ctypes_patch_getfunc = getfunc

tests/test_ctypes_patch.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,29 @@ def get_struct():
9494
struct = get_struct()
9595
self.assertEqual(struct.spam, 123)
9696
self.assertEqual(struct.ham, 123)
97+
98+
def test_patched_type_returned_often(self):
99+
"""Returning a patched type very often works properly without crashing anything.
100+
101+
This checks that bpo-36880 is either fixed or worked around.
102+
"""
103+
104+
class TestStruct(ctypes.Structure):
105+
_fields_ = [
106+
("spam", ctypes.c_int),
107+
("ham", ctypes.c_double),
108+
]
109+
functype = ctypes.CFUNCTYPE(TestStruct)
110+
111+
ctypes_patch.make_callback_returnable(TestStruct)
112+
113+
# After patching, the structure can be returned from a callback.
114+
@functype
115+
def get_struct():
116+
return TestStruct(123, 123)
117+
118+
for _ in range(10000):
119+
# After being returned from the callback, the structure's data is intact.
120+
struct = get_struct()
121+
self.assertEqual(struct.spam, 123)
122+
self.assertEqual(struct.ham, 123)

0 commit comments

Comments
 (0)
0