Skip to content

Commit eb229a9

Browse files
authored
gh-49960: Allow ctypes callbacks to return struct/union types (GH-157974)
1 parent 60e956e commit eb229a9

6 files changed

Lines changed: 429 additions & 59 deletions

File tree

‎Doc/library/ctypes.rst‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,31 @@ write::
13371337
:class:`threading.local` will *not* survive across different callbacks, even when
13381338
those calls are made from the same C thread.
13391339

1340+
Callback functions can return structures and unions by value, in addition to
1341+
the simple types:
1342+
1343+
.. code-block:: python
1344+
1345+
class Point(Structure):
1346+
_fields_ = [("x", c_int), ("y", c_int)]
1347+
1348+
@CFUNCTYPE(Point)
1349+
def get_origin():
1350+
return Point(0, 0)
1351+
1352+
.. note::
1353+
1354+
A structure or union returned from a callback is copied *by value*, and
1355+
:mod:`!ctypes` does not keep the returned object alive after the callback
1356+
returns. If the structure contains a pointer field (such as
1357+
:class:`c_char_p` or a :func:`POINTER` type), you must ensure that the
1358+
memory it refers to stays valid for as long as the C code uses it -- the
1359+
same requirement C itself imposes. Returning a pointer to memory owned by
1360+
a temporary Python object will leave the C caller with a dangling pointer.
1361+
1362+
.. versionchanged:: next
1363+
Callback functions can now return structures and unions.
1364+
13401365
.. _ctypes-accessing-values-exported-from-dlls:
13411366

13421367
Accessing values exported from dlls

‎Doc/whatsnew/3.16.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ ctypes
244244
implicitly converted when passed for a :func:`~ctypes.POINTER` argument.
245245
(Contributed by Peter Bierma in :gh:`71990`.)
246246

247+
* :ref:`Callback functions <ctypes-callback-functions>` can now return
248+
:class:`~ctypes.Structure` and :class:`~ctypes.Union` instances by value.
249+
(Contributed by Russell Keith-Magee in :gh:`49960`.)
250+
247251

248252
curses
249253
------

‎Lib/test/test_ctypes/test_callbacks.py‎

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import math
55
import sys
66
import unittest
7+
import warnings
78
from _ctypes import CTYPES_MAX_ARGCOUNT
8-
from ctypes import (CDLL, cdll, Structure, CFUNCTYPE,
9+
from ctypes import (CDLL, cdll, Structure, Union, CFUNCTYPE,
910
ArgumentError, POINTER, sizeof,
10-
c_byte, c_ubyte, c_char,
11+
c_byte, c_ubyte, c_char, c_char_p,
1112
c_short, c_ushort, c_int, c_uint,
1213
c_long, c_longlong, c_ulonglong, c_ulong,
1314
c_float, c_double, c_longdouble, py_object)
14-
from ctypes.util import find_library, wrap_dll_function
15+
from ctypes.util import find_library, struct, wrap_dll_function
1516
from test import support
1617
from test.support import import_helper
1718
_ctypes_test = import_helper.import_module("_ctypes_test")
@@ -293,6 +294,222 @@ def callback(check, s):
293294
self.assertEqual(s.second, check.second)
294295
self.assertEqual(s.third, check.third)
295296

297+
# gh-49960: callbacks may return structures and unions by value.
298+
299+
def test_callback_return_small_struct(self):
300+
@struct
301+
class SmallRet:
302+
a: c_int
303+
b: c_int
304+
305+
CALLBACK = CFUNCTYPE(SmallRet)
306+
dll = CDLL(_ctypes_test.__file__)
307+
func = dll._testfunc_cbk_ret_small_struct
308+
func.argtypes = (CALLBACK,)
309+
func.restype = SmallRet
310+
311+
result = func(CALLBACK(lambda: SmallRet(17, 42)))
312+
self.assertEqual((result.a, result.b), (17, 42))
313+
314+
def test_callback_return_struct_reaches_c(self):
315+
# The C helper sums the fields itself, proving the bytes actually
316+
# reached the C caller rather than only round-tripping in Python.
317+
@struct
318+
class SmallRet:
319+
a: c_int
320+
b: c_int
321+
322+
CALLBACK = CFUNCTYPE(SmallRet)
323+
dll = CDLL(_ctypes_test.__file__)
324+
func = dll._testfunc_cbk_ret_small_struct_sum
325+
func.argtypes = (CALLBACK,)
326+
func.restype = c_long
327+
328+
self.assertEqual(func(CALLBACK(lambda: SmallRet(300, 45))), 345)
329+
330+
def test_callback_return_large_struct(self):
331+
# Mirrors `Test` in Modules/_ctypes/_ctypes_test.c: >8 bytes, so it
332+
# is returned via a hidden pointer rather than in registers.
333+
@struct
334+
class X:
335+
first: c_ulong
336+
second: c_ulong
337+
third: c_ulong
338+
339+
CALLBACK = CFUNCTYPE(X)
340+
dll = CDLL(_ctypes_test.__file__)
341+
func = dll._testfunc_cbk_ret_large_struct
342+
func.argtypes = (CALLBACK,)
343+
func.restype = X
344+
345+
result = func(CALLBACK(lambda: X(0xdeadbeef, 0xcafebabe, 0x0bad1dea)))
346+
self.assertEqual(result.first, 0xdeadbeef)
347+
self.assertEqual(result.second, 0xcafebabe)
348+
self.assertEqual(result.third, 0x0bad1dea)
349+
350+
def test_callback_return_float_struct(self):
351+
# All-float struct: SSE class on x86-64, HFA on AArch64.
352+
@struct
353+
class FloatRet:
354+
x: c_double
355+
y: c_double
356+
357+
CALLBACK = CFUNCTYPE(FloatRet)
358+
dll = CDLL(_ctypes_test.__file__)
359+
func = dll._testfunc_cbk_ret_float_struct
360+
func.argtypes = (CALLBACK,)
361+
func.restype = FloatRet
362+
363+
result = func(CALLBACK(lambda: FloatRet(1.5, -2.25)))
364+
self.assertEqual((result.x, result.y), (1.5, -2.25))
365+
366+
def test_callback_return_union(self):
367+
class UnionRet(Union):
368+
_fields_ = [("i", c_int), ("f", c_float)]
369+
370+
CALLBACK = CFUNCTYPE(UnionRet)
371+
dll = CDLL(_ctypes_test.__file__)
372+
func = dll._testfunc_cbk_ret_union
373+
func.argtypes = (CALLBACK,)
374+
func.restype = UnionRet
375+
376+
result = func(CALLBACK(lambda: UnionRet(i=0x41424344)))
377+
self.assertEqual(result.i, 0x41424344)
378+
379+
def test_callback_return_struct_repeatedly(self):
380+
# Guards against per-call leaks and against regressing into the
381+
# "memory leak in callback function" RuntimeWarning path.
382+
@struct
383+
class SmallRet:
384+
a: c_int
385+
b: c_int
386+
387+
CALLBACK = CFUNCTYPE(SmallRet)
388+
dll = CDLL(_ctypes_test.__file__)
389+
func = dll._testfunc_cbk_ret_small_struct_sum
390+
func.argtypes = (CALLBACK,)
391+
func.restype = c_long
392+
393+
cb = CALLBACK(lambda: SmallRet(1, 2))
394+
# A RuntimeWarning raised inside the callback is reported as an
395+
# unraisable exception rather than propagated, so check for those.
396+
with warnings.catch_warnings():
397+
warnings.simplefilter("error", RuntimeWarning)
398+
with support.catch_unraisable_exception() as cm:
399+
for _ in range(100):
400+
self.assertEqual(func(cb), 3)
401+
self.assertIsNone(cm.unraisable)
402+
403+
def test_callback_return_struct_subclass(self):
404+
# isinstance semantics: a subclass instance is acceptable.
405+
@struct
406+
class SmallRet:
407+
a: c_int
408+
b: c_int
409+
410+
class SubRet(SmallRet):
411+
pass
412+
413+
CALLBACK = CFUNCTYPE(SmallRet)
414+
dll = CDLL(_ctypes_test.__file__)
415+
func = dll._testfunc_cbk_ret_small_struct_sum
416+
func.argtypes = (CALLBACK,)
417+
func.restype = c_long
418+
419+
self.assertEqual(func(CALLBACK(lambda: SubRet(4, 5))), 9)
420+
421+
def test_callback_return_struct_wrong_type(self):
422+
@struct
423+
class SmallRet:
424+
a: c_int
425+
b: c_int
426+
427+
@struct
428+
class Other:
429+
q: c_int
430+
431+
CALLBACK = CFUNCTYPE(SmallRet)
432+
dll = CDLL(_ctypes_test.__file__)
433+
func = dll._testfunc_cbk_ret_small_struct_sum
434+
func.argtypes = (CALLBACK,)
435+
func.restype = c_long
436+
437+
for bad in (None, 42, Other(1)):
438+
with self.subTest(bad=bad):
439+
def cb(bad=bad):
440+
return bad
441+
with support.catch_unraisable_exception() as cm:
442+
# As with other result types, the value returned to C
443+
# after a conversion failure is undefined; don't test it.
444+
func(CALLBACK(cb))
445+
self.assertIsInstance(cm.unraisable.exc_value, TypeError)
446+
self.assertEqual(
447+
cm.unraisable.err_msg,
448+
f"Exception ignored while converting result "
449+
f"of ctypes callback function {cb!r}")
450+
451+
def test_callback_return_struct_with_pointer(self):
452+
# gh-49960 / bpo-5710 discussion: the struct is copied by value, so
453+
# any pointer it contains must reference memory the caller keeps
454+
# alive. Here `keepalive` does exactly that.
455+
@struct
456+
class WithPtr:
457+
s: c_char_p
458+
459+
keepalive = b"hello"
460+
461+
CALLBACK = CFUNCTYPE(WithPtr)
462+
dll = CDLL(_ctypes_test.__file__)
463+
func = dll._testfunc_cbk_ret_ptr_struct
464+
func.argtypes = (CALLBACK,)
465+
func.restype = WithPtr
466+
467+
result = func(CALLBACK(lambda: WithPtr(keepalive)))
468+
self.assertEqual(result.s, b"hello")
469+
470+
def test_callback_return_struct_ignores_instancecheck(self):
471+
# The result type is checked without running Python code, so a
472+
# metaclass __instancecheck__ cannot cause a smaller structure to be
473+
# accepted (which would read past the end of its buffer).
474+
class Meta(type(Structure)):
475+
def __instancecheck__(cls, inst):
476+
return True
477+
478+
# Not @struct: it would rebuild the class and drop the metaclass.
479+
class SmallRet(Structure, metaclass=Meta):
480+
_fields_ = [("a", c_int), ("b", c_int)]
481+
482+
@struct
483+
class Tiny:
484+
x: c_byte
485+
486+
CALLBACK = CFUNCTYPE(SmallRet)
487+
dll = CDLL(_ctypes_test.__file__)
488+
func = dll._testfunc_cbk_ret_small_struct_sum
489+
func.argtypes = (CALLBACK,)
490+
func.restype = c_long
491+
492+
def cb():
493+
return Tiny(5)
494+
with support.catch_unraisable_exception() as cm:
495+
func(CALLBACK(cb))
496+
self.assertIsInstance(cm.unraisable.exc_value, TypeError)
497+
498+
def test_callback_return_struct_fields_final(self):
499+
# Once a structure is used as a callback's result type, its layout
500+
# is fixed: the callback's libffi signature depends on it.
501+
@struct
502+
class Base:
503+
a: c_int
504+
b: c_int
505+
506+
class Sub(Base):
507+
pass
508+
509+
CFUNCTYPE(Sub)(lambda: Sub(1, 2))
510+
with self.assertRaisesRegex(AttributeError, "_fields_ is final"):
511+
Sub._fields_ = [("c", c_double)]
512+
296513
def test_callback_too_many_args(self):
297514
def func(*args):
298515
return len(args)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`ctypes` callback functions can now return structures and unions by
2+
value. Previously only "simple" types could be returned, and any other result
3+
type raised :exc:`TypeError`.

‎Modules/_ctypes/_ctypes_test.c‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,79 @@ _testfunc_cbk_large_struct(Test in, void (*func)(Test))
7272
func(in);
7373
}
7474

75+
/*
76+
* gh-49960: callbacks returning structs and unions by value.
77+
*
78+
* Each of these exercises a different ABI return class, since struct
79+
* return conventions are highly platform-specific:
80+
* - SmallRet: small integer struct, returned in registers
81+
* - Test: >8 bytes (reused from above), returned via hidden pointer
82+
* - FloatRet: all-float struct, SSE class on x86-64 / HFA on AArch64
83+
* - UnionRet: union
84+
* - PtrRet: struct containing a pointer
85+
*/
86+
87+
typedef struct {
88+
int a;
89+
int b;
90+
} SmallRet;
91+
92+
EXPORT(SmallRet)
93+
_testfunc_cbk_ret_small_struct(SmallRet (*func)(void))
94+
{
95+
return func();
96+
}
97+
98+
/* Returns a scalar derived from the struct, so a test can prove the bytes
99+
actually reached the C caller rather than only round-tripping in Python. */
100+
EXPORT(long)
101+
_testfunc_cbk_ret_small_struct_sum(SmallRet (*func)(void))
102+
{
103+
SmallRet s = func();
104+
return (long)s.a + (long)s.b;
105+
}
106+
107+
EXPORT(Test)
108+
_testfunc_cbk_ret_large_struct(Test (*func)(void))
109+
{
110+
return func();
111+
}
112+
113+
typedef struct {
114+
double x;
115+
double y;
116+
} FloatRet;
117+
118+
EXPORT(FloatRet)
119+
_testfunc_cbk_ret_float_struct(FloatRet (*func)(void))
120+
{
121+
return func();
122+
}
123+
124+
typedef union {
125+
int i;
126+
float f;
127+
} UnionRet;
128+
129+
EXPORT(UnionRet)
130+
_testfunc_cbk_ret_union(UnionRet (*func)(void))
131+
{
132+
return func();
133+
}
134+
135+
/* Struct containing a pointer, for the documented pointer-lifetime contract:
136+
the struct is copied by value, so the pointed-to memory must outlive the
137+
callback. */
138+
typedef struct {
139+
const char *s;
140+
} PtrRet;
141+
142+
EXPORT(PtrRet)
143+
_testfunc_cbk_ret_ptr_struct(PtrRet (*func)(void))
144+
{
145+
return func();
146+
}
147+
75148
/*
76149
* See issue 29565. Update a structure passed by value;
77150
* the caller should not see any change.

0 commit comments

Comments
 (0)