diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 6c7c2e5270736e4..c0171596a17a33e 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -11,7 +11,7 @@ c_short, c_ushort, c_int, c_uint, c_long, c_longlong, c_ulonglong, c_ulong, c_float, c_double, c_longdouble, py_object) -from ctypes.util import find_library +from ctypes.util import find_library, wrap_dll_function from test import support from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") @@ -328,6 +328,20 @@ def func(): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) + def test_narrow_int_return_widened(self): + # gh-156933: Narrow integers were not widened on s390x + CALLBACK = CFUNCTYPE(c_int) + + @wrap_dll_function(CDLL(_ctypes_test.__file__)) + def _testfunc_callback_int_to_longlong(func: CALLBACK) -> c_longlong: + pass + + @CALLBACK + def cb(): + return -1 + + self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst b/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst new file mode 100644 index 000000000000000..afe1dffa96b403d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst @@ -0,0 +1,2 @@ +Fix incorrect integer return values from :mod:`ctypes` callbacks on some +platforms, such as s390x. diff --git a/Modules/_ctypes/_ctypes_test.c b/Modules/_ctypes/_ctypes_test.c index 991ff0d675c2f1c..6a5b20143f55272 100644 --- a/Modules/_ctypes/_ctypes_test.c +++ b/Modules/_ctypes/_ctypes_test.c @@ -638,6 +638,11 @@ EXPORT(long long) _testfunc_callback_q_qf(long long value, return sum; } +EXPORT(long long) _testfunc_callback_int_to_longlong(int (*func)(void)) +{ + return func(); +} + typedef struct { char *name; char *value; diff --git a/Modules/_ctypes/callbacks.c b/Modules/_ctypes/callbacks.c index fd508ae61f2e040..e67817e8f6f4d96 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -101,6 +101,22 @@ TryAddRef(PyObject *cnv, CDataObject *obj) } #endif +static int +is_narrow_int_ffi_type(int type) +{ + switch (type) { + case FFI_TYPE_SINT8: + case FFI_TYPE_UINT8: + case FFI_TYPE_SINT16: + case FFI_TYPE_UINT16: + case FFI_TYPE_SINT32: + case FFI_TYPE_UINT32: + return 1; + default: + return 0; + } +} + /****************************************************************************** * * Call the python object with all arguments @@ -222,13 +238,21 @@ static void _CallPythonObject(ctypes_state *st, if (restype != &ffi_type_void && result) { assert(setfunc); -#ifdef WORDS_BIGENDIAN - /* See the corresponding code in _ctypes_callproc(): - in callproc.c, around line 1219. */ - if (restype->type != FFI_TYPE_FLOAT && restype->size < sizeof(ffi_arg)) { - mem = (char *)mem + sizeof(ffi_arg) - restype->size; - } -#endif + /* libffi's closure contract requires integral results narrower + than ffi_arg to fill a whole register, sign-extended if signed; + setfunc() only writes restype->size bytes. */ + union { + ffi_arg arg; + int8_t s8; + uint8_t u8; + int16_t s16; + uint16_t u16; + int32_t s32; + uint32_t u32; + } narrow_res = {0}; + int narrow = restype->size < sizeof(ffi_arg) && + is_narrow_int_ffi_type(restype->type); + void *resmem = narrow ? (void *)&narrow_res : mem; /* keep is an object we have to keep alive so that the result stays valid. If there is no such object, the setfunc will @@ -239,7 +263,34 @@ static void _CallPythonObject(ctypes_state *st, be the result. EXCEPT when restype is py_object - Python itself knows how to manage the refcount of these objects. */ - PyObject *keep = setfunc(mem, result, restype->size); + PyObject *keep = setfunc(resmem, result, restype->size); + + if (narrow && keep != NULL) { + ffi_arg widened; + switch (restype->type) { + case FFI_TYPE_SINT8: + widened = (ffi_arg)(ffi_sarg)narrow_res.s8; + break; + case FFI_TYPE_SINT16: + widened = (ffi_arg)(ffi_sarg)narrow_res.s16; + break; + case FFI_TYPE_SINT32: + widened = (ffi_arg)(ffi_sarg)narrow_res.s32; + break; + case FFI_TYPE_UINT8: + widened = narrow_res.u8; + break; + case FFI_TYPE_UINT16: + widened = narrow_res.u16; + break; + case FFI_TYPE_UINT32: + widened = narrow_res.u32; + break; + default: + Py_UNREACHABLE(); + } + memcpy(mem, &widened, sizeof(ffi_arg)); + } if (keep == NULL) { /* Could not convert callback result. */