Skip to content

gh-61103: support double complex (_Complex) type in ctypes #120894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c9b872f
gh-61103: support double complex (_Complex) type in ctypes
skirpichev Jun 22, 2024
b402300
+ ifdef
skirpichev Jun 23, 2024
ad39e6d
+ fix rst formatting
skirpichev Jun 23, 2024
fd9135b
+ CMPLX
skirpichev Jun 23, 2024
c296fb3
Apply suggestions from code review
skirpichev Jun 23, 2024
7453ca5
+ support _Complex in c-analyzer
skirpichev Jun 23, 2024
e444c9c
+1
skirpichev Jun 23, 2024
2e90643
expand docs
skirpichev Jun 23, 2024
32ffacf
drop ctypes.__STDC_IEC_559_COMPLEX__
skirpichev Jun 23, 2024
9163950
And more tests, finally)
skirpichev Jun 23, 2024
bf81682
Apply suggestions from code review
skirpichev Jun 23, 2024
af2fdce
Apply suggestions from code review
skirpichev Jun 23, 2024
6d9d5ab
+ allow _Complex specifier before float/double/etc
skirpichev Jun 23, 2024
f9c709c
+ comment on clang workaround
skirpichev Jun 23, 2024
ccd418d
Address review:
skirpichev Jun 24, 2024
eb313dc
+ @requires_IEEE_754 for test_csqrt()
skirpichev Jun 24, 2024
693c04e
Revert "+ @requires_IEEE_754 for test_csqrt()"
skirpichev Jun 24, 2024
0ee7049
Adjust test_csqrt() to not use special numbers
skirpichev Jun 24, 2024
de91bbe
Revert "Adjust test_csqrt() to not use special numbers"
skirpichev Jun 24, 2024
b79200e
+1
skirpichev Jun 24, 2024
be6a685
+1
skirpichev Jun 24, 2024
04e89c2
revert test + fix typo
skirpichev Jun 24, 2024
0bc87da
Adjust test_csqrt() to not use special numbers
skirpichev Jun 24, 2024
6347412
+ make CMPLX static
skirpichev Jun 24, 2024
e757fb6
try bad CMPLX()
skirpichev Jun 24, 2024
1eb989c
more tests
skirpichev Jun 24, 2024
b44aae2
cexp test
skirpichev Jun 24, 2024
2153608
use my_csquare() in test
skirpichev Jun 24, 2024
af42aa8
revert CMPLX
skirpichev Jun 24, 2024
e57198e
Address review:
skirpichev Jun 24, 2024
36732bc
revert tests
skirpichev Jun 24, 2024
892b241
XXX skip test on MacOs arm64
skirpichev Jun 24, 2024
ee953c3
Revert "XXX skip test on MacOs arm64"
skirpichev Jun 24, 2024
875cbb8
XXX (to pass)
skirpichev Jun 24, 2024
f8e3a47
Address review: guard _complex.h with ifndef
skirpichev Jun 24, 2024
f447919
Apply suggestions from code review
skirpichev Jun 24, 2024
7b1c366
Merge branch 'master' into ctypes-c_complex-61103
skirpichev Jun 25, 2024
2ea028a
Add _Complex (sic!) to union result
skirpichev Jun 25, 2024
837add4
add missing ifdef + restore tests
skirpichev Jun 25, 2024
1b8f37e
Adjust test_csqrt() to not use special numbers
skirpichev Jun 24, 2024
0bd1ebe
Add _Complex also to tagPyCArgObject struct
skirpichev Jun 25, 2024
6eba445
cleanup: use "complex" instead of _Complex
skirpichev Jun 25, 2024
a6ac463
address review:
skirpichev Jun 26, 2024
615e8de
Update Modules/_ctypes/_ctypes.c
skirpichev Jun 26, 2024
f7d9973
address review: renamed macro (->PY_HAVE_C_COMPLEX)
skirpichev Jun 26, 2024
3d170bd
+ autoreconf
skirpichev Jun 26, 2024
4dd044c
+ test to ensure CMPLX preserves special components
skirpichev Jun 26, 2024
4cf7045
address review: rename macros to Py_HAVE_C_COMPLEX
skirpichev Jun 28, 2024
264fa7d
fix typos and adjust Makefile.pre.in
skirpichev Jun 28, 2024
f54b964
enable Py_HAVE_C_COMPLEX only if libff support complex numbers
skirpichev Jun 29, 2024
23db200
Revert "enable Py_HAVE_C_COMPLEX only if libff support complex numbers"
skirpichev Jun 29, 2024
5a7e366
ifdef Py_HAVE_C_COMPLEX -> if defined(Py_HAVE_C_COMPLEX) && defined(F…
skirpichev Jun 29, 2024
388e2ca
+ missing ffi.h imports
skirpichev Jun 29, 2024
e888565
+ add libffi's cflags/ldflags to _ctypes_test.c deps
skirpichev Jun 29, 2024
56b524c
Merge branch 'master' into ctypes-c_complex-61103
skirpichev Jun 29, 2024
e9cab4a
Adjust test in _ctypes.c to not use FFI_TYPE_COMPLEX define
skirpichev Jun 29, 2024
6d5bf66
sed: FFI_TYPE_COMPLEX -> FFI_TARGET_HAS_COMPLEX_TYPE
skirpichev Jun 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,16 @@ Fundamental data types
(1)
The constructor accepts any object with a truth value.

Additionally, if IEC 60559 compatible complex arithmetic (Annex G) is supported, the following
complex types are available:

+----------------------------------+---------------------------------+-----------------+
| ctypes type | C type | Python type |
+==================================+=================================+=================+
| :class:`c_double_complex` | :c:expr:`double complex` | complex |
+----------------------------------+---------------------------------+-----------------+


All these types can be created by calling them with an optional initializer of
the correct type and value::

Expand Down Expand Up @@ -2284,6 +2294,14 @@ These are the fundamental ctypes data types:
optional float initializer.


.. class:: c_double_complex

Represents the C :c:expr:`double complex` datatype, if available. The
constructor accepts an optional :class:`complex` initializer.

.. versionadded:: 3.14


.. class:: c_int

Represents the C :c:expr:`signed int` datatype. The constructor accepts an
Expand Down
6 changes: 6 additions & 0 deletions Lib/ctypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ class c_longdouble(_SimpleCData):
if sizeof(c_longdouble) == sizeof(c_double):
c_longdouble = c_double

try:
class c_double_complex(_SimpleCData):
_type_ = "C"
except AttributeError:
pass

if _calcsize("l") == _calcsize("q"):
# if long and long long have the same size, make c_longlong an alias for c_long
c_longlong = c_long
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_ctypes/test_libc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ctypes
import math
import unittest
from ctypes import (CDLL, CFUNCTYPE, POINTER, create_string_buffer, sizeof,
Expand All @@ -21,6 +22,17 @@ def test_sqrt(self):
self.assertEqual(lib.my_sqrt(4.0), 2.0)
self.assertEqual(lib.my_sqrt(2.0), math.sqrt(2.0))

@unittest.skipUnless(hasattr(ctypes, "c_double_complex"),
"requires C11 complex type")
def test_csqrt(self):
lib.my_csqrt.argtypes = ctypes.c_double_complex,
lib.my_csqrt.restype = ctypes.c_double_complex
self.assertEqual(lib.my_csqrt(4), 2+0j)
self.assertAlmostEqual(lib.my_csqrt(-1+0.01j),
0.004999937502734214+1.0000124996093955j)
self.assertAlmostEqual(lib.my_csqrt(-1-0.01j),
0.004999937502734214-1.0000124996093955j)

def test_qsort(self):
comparefunc = CFUNCTYPE(c_int, POINTER(c_char), POINTER(c_char))
lib.my_qsort.argtypes = c_void_p, c_size_t, c_size_t, comparefunc
Expand Down
85 changes: 73 additions & 12 deletions Lib/test/test_ctypes/test_numbers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import array
import ctypes
import struct
import sys
import unittest
from itertools import combinations
from math import copysign, isnan
from operator import truth
from ctypes import (byref, sizeof, alignment,
c_char, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint,
Expand Down Expand Up @@ -38,8 +41,55 @@ def valid_ranges(*types):
signed_ranges = valid_ranges(*signed_types)
bool_values = [True, False, 0, 1, -1, 5000, 'test', [], [1]]

class IntLike:
def __int__(self):
return 2

class IndexLike:
def __index__(self):
return 2

class FloatLike:
def __float__(self):
return 2.0

class ComplexLike:
def __complex__(self):
return 1+1j


INF = float("inf")
NAN = float("nan")


class NumberTestCase(unittest.TestCase):
# from Lib/test/test_complex.py
def assertFloatsAreIdentical(self, x, y):
"""assert that floats x and y are identical, in the sense that:
(1) both x and y are nans, or
(2) both x and y are infinities, with the same sign, or
(3) both x and y are zeros, with the same sign, or
(4) x and y are both finite and nonzero, and x == y

"""
msg = 'floats {!r} and {!r} are not identical'

if isnan(x) or isnan(y):
if isnan(x) and isnan(y):
return
elif x == y:
if x != 0.0:
return
# both zero; check that signs match
elif copysign(1.0, x) == copysign(1.0, y):
return
else:
msg += ': zeros have different signs'
self.fail(msg.format(x, y))

def assertComplexesAreIdentical(self, x, y):
self.assertFloatsAreIdentical(x.real, y.real)
self.assertFloatsAreIdentical(x.imag, y.imag)

def test_default_init(self):
# default values are set to zero
Expand Down Expand Up @@ -86,28 +136,39 @@ def test_byref(self):
def test_floats(self):
# c_float and c_double can be created from
# Python int and float
class FloatLike:
def __float__(self):
return 2.0
f = FloatLike()
for t in float_types:
self.assertEqual(t(2.0).value, 2.0)
self.assertEqual(t(2).value, 2.0)
self.assertEqual(t(2).value, 2.0)
self.assertEqual(t(f).value, 2.0)

@unittest.skipUnless(hasattr(ctypes, "c_double_complex"),
"requires C11 complex type")
def test_complex(self):
for t in [ctypes.c_double_complex]:
self.assertEqual(t(1).value, 1+0j)
self.assertEqual(t(1.0).value, 1+0j)
self.assertEqual(t(1+0.125j).value, 1+0.125j)
self.assertEqual(t(IndexLike()).value, 2+0j)
self.assertEqual(t(FloatLike()).value, 2+0j)
self.assertEqual(t(ComplexLike()).value, 1+1j)

@unittest.skipUnless(hasattr(ctypes, "c_double_complex"),
"requires C11 complex type")
def test_complex_round_trip(self):
# Ensure complexes transformed exactly. The CMPLX macro should
# preserve special components (like inf/nan or signed zero).
values = [complex(*_) for _ in combinations([1, -1, 0.0, -0.0, 2,
-3, INF, -INF, NAN], 2)]
for z in values:
with self.subTest(z=z):
z2 = ctypes.c_double_complex(z).value
self.assertComplexesAreIdentical(z, z2)

def test_integers(self):
class FloatLike:
def __float__(self):
return 2.0
f = FloatLike()
class IntLike:
def __int__(self):
return 2
d = IntLike()
class IndexLike:
def __index__(self):
return 2
i = IndexLike()
# integers cannot be constructed from floats,
# but from integer-like objects
Expand Down
2 changes: 1 addition & 1 deletion Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -3110,7 +3110,7 @@ MODULE_MATH_DEPS=$(srcdir)/Modules/_math.h
MODULE_PYEXPAT_DEPS=@LIBEXPAT_INTERNAL@
MODULE_UNICODEDATA_DEPS=$(srcdir)/Modules/unicodedata_db.h $(srcdir)/Modules/unicodename_db.h
MODULE__BLAKE2_DEPS=$(srcdir)/Modules/_blake2/impl/blake2-config.h $(srcdir)/Modules/_blake2/impl/blake2-impl.h $(srcdir)/Modules/_blake2/impl/blake2.h $(srcdir)/Modules/_blake2/impl/blake2b-load-sse2.h $(srcdir)/Modules/_blake2/impl/blake2b-load-sse41.h $(srcdir)/Modules/_blake2/impl/blake2b-ref.c $(srcdir)/Modules/_blake2/impl/blake2b-round.h $(srcdir)/Modules/_blake2/impl/blake2b.c $(srcdir)/Modules/_blake2/impl/blake2s-load-sse2.h $(srcdir)/Modules/_blake2/impl/blake2s-load-sse41.h $(srcdir)/Modules/_blake2/impl/blake2s-load-xop.h $(srcdir)/Modules/_blake2/impl/blake2s-ref.c $(srcdir)/Modules/_blake2/impl/blake2s-round.h $(srcdir)/Modules/_blake2/impl/blake2s.c $(srcdir)/Modules/_blake2/blake2module.h $(srcdir)/Modules/hashlib.h
MODULE__CTYPES_DEPS=$(srcdir)/Modules/_ctypes/ctypes.h
MODULE__CTYPES_DEPS=$(srcdir)/Modules/_ctypes/ctypes.h $(srcdir)/Modules/_complex.h
MODULE__CTYPES_TEST_DEPS=$(srcdir)/Modules/_ctypes/_ctypes_test_generated.c.h
MODULE__CTYPES_MALLOC_CLOSURE=@MODULE__CTYPES_MALLOC_CLOSURE@
MODULE__DECIMAL_DEPS=$(srcdir)/Modules/_decimal/docstrings.h @LIBMPDEC_INTERNAL@
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Support :c:expr:`double complex` C type in :mod:`ctypes` via
:class:`~ctypes.c_double_complex` if compiler has C11 complex
arithmetic. Patch by Sergey B Kirpichev.
34 changes: 34 additions & 0 deletions Modules/_complex.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* Workarounds for buggy complex number arithmetic implementations. */

#ifndef Py_HAVE_C_COMPLEX
# error "this header file should only be included if Py_HAVE_C_COMPLEX is defined"
#endif

#include <complex.h>

/* Other compilers (than clang), that claims to
implement C11 *and* define __STDC_IEC_559_COMPLEX__ - don't have
issue with CMPLX(). This is specific to glibc & clang combination:
https://sourceware.org/bugzilla/show_bug.cgi?id=26287

Here we fallback to using __builtin_complex(), available in clang
v12+. Else CMPLX implemented following C11 6.2.5p13: "Each complex type
has the same representation and alignment requirements as an array
type containing exactly two elements of the corresponding real type;
the first element is equal to the real part, and the second element
to the imaginary part, of the complex number.
*/
#if !defined(CMPLX)
# if defined(__clang__) && __has_builtin(__builtin_complex)
# define CMPLX(x, y) __builtin_complex ((double) (x), (double) (y))
# else
static inline double complex
CMPLX(double real, double imag)
{
double complex z;
((double *)(&z))[0] = real;
((double *)(&z))[1] = imag;
return z;
}
# endif
#endif
16 changes: 15 additions & 1 deletion Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1750,7 +1750,11 @@ class _ctypes.c_void_p "PyObject *" "clinic_state_sub()->PyCSimpleType_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=dd4d9646c56f43a9]*/

#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
static const char SIMPLE_TYPE_CHARS[] = "cbBhHiIlLdCfuzZqQPXOv?g";
#else
static const char SIMPLE_TYPE_CHARS[] = "cbBhHiIlLdfuzZqQPXOv?g";
#endif

/*[clinic input]
_ctypes.c_wchar_p.from_param as c_wchar_p_from_param
Expand Down Expand Up @@ -2226,7 +2230,17 @@ PyCSimpleType_init(PyObject *self, PyObject *args, PyObject *kwds)
goto error;
}

stginfo->ffi_type_pointer = *fmt->pffi_type;
if (!fmt->pffi_type->elements) {
stginfo->ffi_type_pointer = *fmt->pffi_type;
}
else {
stginfo->ffi_type_pointer.size = fmt->pffi_type->size;
stginfo->ffi_type_pointer.alignment = fmt->pffi_type->alignment;
stginfo->ffi_type_pointer.type = fmt->pffi_type->type;
stginfo->ffi_type_pointer.elements = PyMem_Malloc(2 * sizeof(ffi_type));
memcpy(stginfo->ffi_type_pointer.elements,
fmt->pffi_type->elements, 2 * sizeof(ffi_type));
}
stginfo->align = fmt->pffi_type->alignment;
stginfo->length = 0;
stginfo->size = fmt->pffi_type->size;
Expand Down
13 changes: 13 additions & 0 deletions Modules/_ctypes/_ctypes_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

#include <Python.h>

#include <ffi.h> // FFI_TARGET_HAS_COMPLEX_TYPE

#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
# include "../_complex.h" // csqrt()
# undef I // for _ctypes_test_generated.c.h
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# undef I // for _ctypes_test_generated.c.h
# undef I // _Complex_I

Copy link
Member Author

@skirpichev skirpichev Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would like to emphasize where new macro create a problem. I.e. name - matter, what it's - actually irrelevant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the relative include? They tend to produce problems further down the road.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about Include/internal/pycore_pycomplex.h?

There is Include/internal/pycore_pymath.h, that (IMHO) does part of work Modules/_math.h (<math.h> workarounds). There are no relative includes of _math.h (yet), but I think these files could be merged.

Copy link
Member

@picnixz picnixz Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that _math.h and _complex.h could be named as Include/internal/pylibc_{math,complex}.h because what they do is patching the C standard library itself rather than adding new functionalities (well, for _complex.h it's not really a patch but rather a way to harmonize the calls).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vstinner: I'd really like to avoid the relative include, but we can fix that in a follow-up PR.

#endif
#include <stdio.h> // printf()
#include <stdlib.h> // qsort()
#include <string.h> // memset()
Expand Down Expand Up @@ -443,6 +449,13 @@ EXPORT(double) my_sqrt(double a)
return sqrt(a);
}

#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
EXPORT(double complex) my_csqrt(double complex a)
{
return csqrt(a);
}
#endif

EXPORT(void) my_qsort(void *base, size_t num, size_t width, int(*compare)(const void*, const void*))
{
qsort(base, num, width, compare);
Expand Down
7 changes: 7 additions & 0 deletions Modules/_ctypes/callproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ module _ctypes
#include "pycore_global_objects.h"// _Py_ID()
#include "pycore_traceback.h" // _PyTraceback_Add()

#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
#include "../_complex.h" // complex
#endif

#include "clinic/callproc.c.h"

#define CTYPES_CAPSULE_NAME_PYMEM "_ctypes pymem"
Expand Down Expand Up @@ -651,6 +655,9 @@ union result {
double d;
float f;
void *p;
#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
double complex C;
#endif
};

struct argument {
Expand Down
33 changes: 33 additions & 0 deletions Modules/_ctypes/cfield.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
#include <ffi.h>
#include "ctypes.h"

#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
# include "../_complex.h" // complex
#endif

#define CTYPES_CFIELD_CAPSULE_NAME_PYMEM "_ctypes/cfield.c pymem"

Expand Down Expand Up @@ -1087,6 +1090,30 @@ d_get(void *ptr, Py_ssize_t size)
return PyFloat_FromDouble(val);
}

#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
static PyObject *
C_set(void *ptr, PyObject *value, Py_ssize_t size)
{
Py_complex c = PyComplex_AsCComplex(value);

if (c.real == -1 && PyErr_Occurred()) {
return NULL;
}
double complex x = CMPLX(c.real, c.imag);
memcpy(ptr, &x, sizeof(x));
_RET(value);
}

static PyObject *
C_get(void *ptr, Py_ssize_t size)
{
double complex x;

memcpy(&x, ptr, sizeof(x));
return PyComplex_FromDoubles(creal(x), cimag(x));
}
#endif

static PyObject *
d_set_sw(void *ptr, PyObject *value, Py_ssize_t size)
{
Expand Down Expand Up @@ -1592,6 +1619,9 @@ static struct fielddesc formattable[] = {
{ 'B', B_set, B_get, NULL},
{ 'c', c_set, c_get, NULL},
{ 'd', d_set, d_get, NULL, d_set_sw, d_get_sw},
#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
{ 'C', C_set, C_get, NULL},
#endif
{ 'g', g_set, g_get, NULL},
{ 'f', f_set, f_get, NULL, f_set_sw, f_get_sw},
{ 'h', h_set, h_get, NULL, h_set_sw, h_get_sw},
Expand Down Expand Up @@ -1642,6 +1672,9 @@ _ctypes_init_fielddesc(void)
case 'B': fd->pffi_type = &ffi_type_uchar; break;
case 'c': fd->pffi_type = &ffi_type_schar; break;
case 'd': fd->pffi_type = &ffi_type_double; break;
#if defined(Py_HAVE_C_COMPLEX) && defined(FFI_TARGET_HAS_COMPLEX_TYPE)
case 'C': fd->pffi_type = &ffi_type_complex_double; break;
#endif
case 'g': fd->pffi_type = &ffi_type_longdouble; break;
case 'f': fd->pffi_type = &ffi_type_float; break;
case 'h': fd->pffi_type = &ffi_type_sshort; break;
Expand Down
Loading
Loading