Skip to content

Commit 45ce0db

Browse files
bpo-40795: ctypes calls unraisablehook with an exception (GH-20452)
If ctypes fails to convert the result of a callback or if a ctypes callback function raises an exception, sys.unraisablehook is now called with an exception set. Previously, the error was logged into stderr by PyErr_Print(). (cherry picked from commit 10228ba) Co-authored-by: Victor Stinner <[email protected]>
1 parent b5ecbf0 commit 45ce0db

File tree

6 files changed

+79
-41
lines changed

6 files changed

+79
-41
lines changed

Lib/ctypes/test/test_callbacks.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import functools
22
import unittest
3+
from test import support
4+
35
from ctypes import *
46
from ctypes.test import need_symbol
57
import _ctypes_test
@@ -301,8 +303,22 @@ def func(*args):
301303
with self.assertRaises(ArgumentError):
302304
cb(*args2)
303305

306+
def test_convert_result_error(self):
307+
def func():
308+
return ("tuple",)
309+
310+
proto = CFUNCTYPE(c_int)
311+
ctypes_func = proto(func)
312+
with support.catch_unraisable_exception() as cm:
313+
# don't test the result since it is an uninitialized value
314+
result = ctypes_func()
315+
316+
self.assertIsInstance(cm.unraisable.exc_value, TypeError)
317+
self.assertEqual(cm.unraisable.err_msg,
318+
"Exception ignored on converting result "
319+
"of ctypes callback function")
320+
self.assertIs(cm.unraisable.object, func)
304321

305-
################################################################
306322

307323
if __name__ == '__main__':
308324
unittest.main()

Lib/ctypes/test/test_random_things.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from ctypes import *
2-
import unittest, sys
2+
import contextlib
3+
from test import support
4+
import unittest
5+
import sys
6+
37

48
def callback_func(arg):
59
42 / arg
@@ -34,41 +38,40 @@ class CallbackTracbackTestCase(unittest.TestCase):
3438
# created, then a full traceback printed. When SystemExit is
3539
# raised in a callback function, the interpreter exits.
3640

37-
def capture_stderr(self, func, *args, **kw):
38-
# helper - call function 'func', and return the captured stderr
39-
import io
40-
old_stderr = sys.stderr
41-
logger = sys.stderr = io.StringIO()
42-
try:
43-
func(*args, **kw)
44-
finally:
45-
sys.stderr = old_stderr
46-
return logger.getvalue()
41+
@contextlib.contextmanager
42+
def expect_unraisable(self, exc_type, exc_msg=None):
43+
with support.catch_unraisable_exception() as cm:
44+
yield
45+
46+
self.assertIsInstance(cm.unraisable.exc_value, exc_type)
47+
if exc_msg is not None:
48+
self.assertEqual(str(cm.unraisable.exc_value), exc_msg)
49+
self.assertEqual(cm.unraisable.err_msg,
50+
"Exception ignored on calling ctypes "
51+
"callback function")
52+
self.assertIs(cm.unraisable.object, callback_func)
4753

4854
def test_ValueError(self):
4955
cb = CFUNCTYPE(c_int, c_int)(callback_func)
50-
out = self.capture_stderr(cb, 42)
51-
self.assertEqual(out.splitlines()[-1],
52-
"ValueError: 42")
56+
with self.expect_unraisable(ValueError, '42'):
57+
cb(42)
5358

5459
def test_IntegerDivisionError(self):
5560
cb = CFUNCTYPE(c_int, c_int)(callback_func)
56-
out = self.capture_stderr(cb, 0)
57-
self.assertEqual(out.splitlines()[-1][:19],
58-
"ZeroDivisionError: ")
61+
with self.expect_unraisable(ZeroDivisionError):
62+
cb(0)
5963

6064
def test_FloatDivisionError(self):
6165
cb = CFUNCTYPE(c_int, c_double)(callback_func)
62-
out = self.capture_stderr(cb, 0.0)
63-
self.assertEqual(out.splitlines()[-1][:19],
64-
"ZeroDivisionError: ")
66+
with self.expect_unraisable(ZeroDivisionError):
67+
cb(0.0)
6568

6669
def test_TypeErrorDivisionError(self):
6770
cb = CFUNCTYPE(c_int, c_char_p)(callback_func)
68-
out = self.capture_stderr(cb, b"spam")
69-
self.assertEqual(out.splitlines()[-1],
70-
"TypeError: "
71-
"unsupported operand type(s) for /: 'int' and 'bytes'")
71+
err_msg = "unsupported operand type(s) for /: 'int' and 'bytes'"
72+
with self.expect_unraisable(TypeError, err_msg):
73+
cb(b"spam")
74+
7275

7376
if __name__ == '__main__':
7477
unittest.main()

Lib/ctypes/test/test_unaligned_structures.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,13 @@ class Y(SwappedStructure):
2727
class TestStructures(unittest.TestCase):
2828
def test_native(self):
2929
for typ in structures:
30-
## print typ.value
3130
self.assertEqual(typ.value.offset, 1)
3231
o = typ()
3332
o.value = 4
3433
self.assertEqual(o.value, 4)
3534

3635
def test_swapped(self):
3736
for typ in byteswapped_structures:
38-
## print >> sys.stderr, typ.value
3937
self.assertEqual(typ.value.offset, 1)
4038
o = typ()
4139
o.value = 4
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`ctypes` module: If ctypes fails to convert the result of a callback or
2+
if a ctypes callback function raises an exception, sys.unraisablehook is now
3+
called with an exception set. Previously, the error was logged into stderr
4+
by :c:func:`PyErr_Print`.

Modules/_ctypes/callbacks.c

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,6 @@ static void _CallPythonObject(void *mem,
213213
pArgs++;
214214
}
215215

216-
#define CHECK(what, x) \
217-
if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyErr_Print()
218-
219216
if (flags & (FUNCFLAG_USE_ERRNO | FUNCFLAG_USE_LASTERROR)) {
220217
error_object = _ctypes_get_errobj(&space);
221218
if (error_object == NULL)
@@ -235,7 +232,10 @@ if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyEr
235232
}
236233

237234
result = PyObject_CallObject(callable, arglist);
238-
CHECK("'calling callback function'", result);
235+
if (result == NULL) {
236+
_PyErr_WriteUnraisableMsg("on calling ctypes callback function",
237+
callable);
238+
}
239239

240240
#ifdef MS_WIN32
241241
if (flags & FUNCFLAG_USE_LASTERROR) {
@@ -251,16 +251,17 @@ if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyEr
251251
}
252252
Py_XDECREF(error_object);
253253

254-
if ((restype != &ffi_type_void) && result) {
255-
PyObject *keep;
254+
if (restype != &ffi_type_void && result) {
256255
assert(setfunc);
256+
257257
#ifdef WORDS_BIGENDIAN
258-
/* See the corresponding code in callproc.c, around line 961 */
259-
if (restype->type != FFI_TYPE_FLOAT && restype->size < sizeof(ffi_arg))
258+
/* See the corresponding code in _ctypes_callproc():
259+
in callproc.c, around line 1219. */
260+
if (restype->type != FFI_TYPE_FLOAT && restype->size < sizeof(ffi_arg)) {
260261
mem = (char *)mem + sizeof(ffi_arg) - restype->size;
262+
}
261263
#endif
262-
keep = setfunc(mem, result, 0);
263-
CHECK("'converting callback result'", keep);
264+
264265
/* keep is an object we have to keep alive so that the result
265266
stays valid. If there is no such object, the setfunc will
266267
have returned Py_None.
@@ -270,18 +271,32 @@ if (x == NULL) _PyTraceback_Add(what, "_ctypes/callbacks.c", __LINE__ - 1), PyEr
270271
be the result. EXCEPT when restype is py_object - Python
271272
itself knows how to manage the refcount of these objects.
272273
*/
273-
if (keep == NULL) /* Could not convert callback result. */
274-
PyErr_WriteUnraisable(callable);
275-
else if (keep == Py_None) /* Nothing to keep */
274+
PyObject *keep = setfunc(mem, result, 0);
275+
276+
if (keep == NULL) {
277+
/* Could not convert callback result. */
278+
_PyErr_WriteUnraisableMsg("on converting result "
279+
"of ctypes callback function",
280+
callable);
281+
}
282+
else if (keep == Py_None) {
283+
/* Nothing to keep */
276284
Py_DECREF(keep);
285+
}
277286
else if (setfunc != _ctypes_get_fielddesc("O")->setfunc) {
278287
if (-1 == PyErr_WarnEx(PyExc_RuntimeWarning,
279288
"memory leak in callback function.",
280289
1))
281-
PyErr_WriteUnraisable(callable);
290+
{
291+
_PyErr_WriteUnraisableMsg("on converting result "
292+
"of ctypes callback function",
293+
callable);
294+
}
282295
}
283296
}
297+
284298
Py_XDECREF(result);
299+
285300
Done:
286301
Py_XDECREF(arglist);
287302
PyGILState_Release(state);

Modules/_ctypes/callproc.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,9 @@ PyObject *_ctypes_callproc(PPROC pProc,
12321232
if (rtype->type != FFI_TYPE_FLOAT
12331233
&& rtype->type != FFI_TYPE_STRUCT
12341234
&& rtype->size < sizeof(ffi_arg))
1235+
{
12351236
resbuf = (char *)resbuf + sizeof(ffi_arg) - rtype->size;
1237+
}
12361238
#endif
12371239

12381240
#ifdef MS_WIN32

0 commit comments

Comments
 (0)