3

Most of my library is written with Cython in the "normal" C mode. Up to now I rarely needed any C++ functionality, but always assumed (and sometimes did!) I could just switch to C++-mode for one module if I wanted to.

So I have like 10+ modules in C-mode and 1 module in C++-mode.

The problem is now how Cython seems to handle complex numbers definitions. In C-mode it assumes I think C complex numbers, and in C++-mode it assumes I think C++ complex numbers. I've read they might be even the same by now, but in any case Cython complains that they are not:

openChargeState/utility/cheb.cpp:2895:35: error: cannot convert ‘__pyx_t_double_complex {aka std::complex<double>}’ to ‘__complex__ double’ for argument ‘1’ to ‘double cabs(__complex__ double)’
 __pyx_t_5 = ((cabs(__pyx_v_num) == INFINITY) != 0);

In that case I'm trying to use cabs defined in a C-mode module, and calling it from the C++-mode module.

I know there are some obvious workarounds (right now I'm just not using C++-mode; I'd like to use vectors and instead use the slower Python lists for now).

Is there maybe a way to tell my C++-mode module to use C complex numbers, or tell it that they are the same? If there is I couldn't find a working way to ctypedef C complex numbers in my C++-mode module... Or are there any other solutions?


EDIT: Comments of DavidW and ead suggested some reasonable things. First the minimum working example.

setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
from Cython.Build import cythonize

extra_compile_args=['-O3']
compdir = {'language_level' : '3'}

extensions = cythonize([
                        Extension("cmod", ["cmod.pyx"]),
                        Extension("cppmod", ["cppmod.pyx"], language='c++')
                       ], 
                       compiler_directives = compdir
                      )

setup(cmdclass = {'build_ext': build_ext},
      ext_modules = extensions
     )

import cppmod

cmod.pyx

cdef double complex c_complex_fun(double complex xx):
    return xx**2    

cmod.pxd

cdef double complex c_complex_fun(double complex xx)

cdef extern from "complex.h":
    double cabs(double complex zz) nogil

cppmod.pyx

cimport cmod

cdef double complex cpp_complex_fun(double complex xx):
    return cmod.c_complex_fun(xx)*abs(xx)   # cmod.cabs(xx) doesn't work here

print(cpp_complex_fun(5.5))

Then just compile with python3 setup.py build_ext --inplace.

Now the interesting part is that (as written in the code) only "indirectly" imported c functions have a problem, in my case cabs. So the suggestion to just use abs actually does help, but I still don't understand the underlying logic. I hope I don't encounter this in another problem. I'm leaving the question up for now. Maybe somebody knows what's happening.

oli
  • 659
  • 1
  • 6
  • 18

1 Answers1

3

Your problem has nothing to do with the fact, that one module is compiled as a C-extension and the other as a C++-extension - one can easily reproduce the issue in a C++-extension alone:

%%cython -+ 
cdef extern from "complex.h": 
    double cabs(double complex zz) nogil

def cpp_complex_fun(double complex xx):
    return cabs(xx)

results in your error-message:

error: cannot convert __pyx_t_double_complex {aka std::complex<double>} to __complex__ double for argument 1 to double cabs(__complex__ double)

The problem is that the complex numbers are ... well, complex. Cython's strategy (can be looked up here and here) to handle complex numbers is to use an available implementation from C/CPP and if none is found a hand-written fallback is used:

#if !defined(CYTHON_CCOMPLEX)
  #if defined(__cplusplus)
    #define CYTHON_CCOMPLEX 1
  #elif defined(_Complex_I)
    #define CYTHON_CCOMPLEX 1
  #else
    #define CYTHON_CCOMPLEX 0
  #endif
#endif
....
#if CYTHON_CCOMPLEX
  #ifdef __cplusplus
    typedef ::std::complex< double > __pyx_t_double_complex;
  #else
    typedef double _Complex __pyx_t_double_complex;
  #endif
#else
    typedef struct { double real, imag; } __pyx_t_double_complex;
#endif

In case of a C++-extension, Cython's double complex is translated to std::complex<double> and thus cannot be called with cabs( double complex z ) because std::complex<double> isn't double complex.

So actually, it is your "fault": you lied to Cython and told him, that cabs has the signature double cabs(std::complex<double> z), but it was not enough to fool the c++-compiler.

That means, in c++-module std::abs(std::complex<double>) could be used, or just Cython's/Python's abs, which is automatically translated to the right function (this is however not possible for all standard-function).


In case of the C-extension, because you have included complex.h as an so called "early include" with cdef extern from "complex.h", thus for the above defines _Complex_I becomes defined and Cython's complex becomes an alias for double complex and thus cabs can be used.


Probably the right thing for Cython would be to always use the fallback per default and that the user should be able to choose the desired implementation (double complex or std::complex<double>) explicitly.

ead
  • 32,758
  • 6
  • 90
  • 153
  • I accepted your answer a while ago, because I don't disagree with what you're saying. I do understand that there are ways around my issues (as I have stated in my edits before your post as well), but I don't think how Cython handles c++/c modules in regards of complex numbers is ideal. But then again, I mixing 3 languages basically, so it's probably not that easy to find a better solution. – oli Jun 17 '20 at 08:02
  • @oli Complex numbers in Cython are a can of worms. It gets even more complicated when coding on different platforms (i.e. Linux vs Windows) I have bugged the Cython-devs some time ago: https://github.com/cython/cython/issues/2513#issuecomment-407552578. – ead Jun 17 '20 at 08:09