|
4 | 4 | import math |
5 | 5 | import sys |
6 | 6 | import unittest |
| 7 | +import warnings |
7 | 8 | from _ctypes import CTYPES_MAX_ARGCOUNT |
8 | | -from ctypes import (CDLL, cdll, Structure, CFUNCTYPE, |
| 9 | +from ctypes import (CDLL, cdll, Structure, Union, CFUNCTYPE, |
9 | 10 | ArgumentError, POINTER, sizeof, |
10 | | - c_byte, c_ubyte, c_char, |
| 11 | + c_byte, c_ubyte, c_char, c_char_p, |
11 | 12 | c_short, c_ushort, c_int, c_uint, |
12 | 13 | c_long, c_longlong, c_ulonglong, c_ulong, |
13 | 14 | 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 |
15 | 16 | from test import support |
16 | 17 | from test.support import import_helper |
17 | 18 | _ctypes_test = import_helper.import_module("_ctypes_test") |
@@ -293,6 +294,222 @@ def callback(check, s): |
293 | 294 | self.assertEqual(s.second, check.second) |
294 | 295 | self.assertEqual(s.third, check.third) |
295 | 296 |
|
| 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 | + |
296 | 513 | def test_callback_too_many_args(self): |
297 | 514 | def func(*args): |
298 | 515 | return len(args) |
|
0 commit comments