Search code examples
pythonctypescomplex-numbers

Complex number in ctypes


This might be a bit foolish but I'm trying to use ctypes to call a function that receives a complex vector as a paramter. But in ctypes there isn't a class c_complex. Does anybody know how to solve this?

edit: I'm refering to python's ctypes, in case there are others....


Solution

  • As noted by the OP in their comment on @Armin Rigo's answer, the correct way to do this is with wrapper functions. Also, as noted in the comments (by me) on the original question, this is also possible for C++ and Fortran. However, the method for getting this to work in all three languages is not necessarily obvious. Therefore, this answer presents a working example for each language.

    Let's say you have a C/C++/Fortran procedure that takes a scalar complex argument. Then you would need to write a wrapper procedure that takes two floats / doubles (the real and imaginary parts), combines them into a complex number and then calls the original procedure. Obviously, this can be extended to arrays of complex numbers but let's keep it simple with a single complex number for now.

    C

    For example, let's say you have a C function to print a formatted complex number. So we have my_complex.c:

    #include <stdio.h>
    #include <complex.h>
    
    void print_complex(double complex z)
    {
        printf("%.1f + %.1fi\n", creal(z), cimag(z));
    }
    

    Then we would have to add a wrapper function (at the end of the same file) like this:

    void print_complex_wrapper(double z_real, double z_imag)
    {
        double complex z = z_real + z_imag * I;
        print_complex(z);
    }
    

    Compile that into a library in the usual way:

    gcc -shared -fPIC -o my_complex_c.so my_complex.c
    

    Then call the wrapper function from Python:

    from ctypes import CDLL, c_double
    c = CDLL('./my_complex_c.so')
    c.print_complex_wrapper.argtypes = [c_double, c_double]
    z = complex(1.0 + 1j * 2.0)
    c.print_complex_wrapper(c_double(z.real), c_double(z.imag))
    

    C++

    The same thing in C++ is a bit more fiddly because an extern "C" interface needs to be defined to avoid name-mangling and we need to deal with the class in Python (both as per this SO Q&A).

    So, now we have my_complex.cpp (to which I have already added the wrapper function):

    #include <stdio.h>
    #include <complex>
    
    class ComplexPrinter
    {
        public:
            void printComplex(std::complex<double> z)
            {   
                printf("%.1f + %.1fi\n", real(z), imag(z));
            }
    
            void printComplexWrapper(double z_real, double z_imag)
            {   
                std::complex<double> z(z_real, z_imag);
                printComplex(z);
            }   
    };
    

    We also need to add an extern "C" interface (at the end of the same file) like this:

    extern "C" 
    {
        ComplexPrinter* ComplexPrinter_new()
        {   
            return new ComplexPrinter();
        }   
        void ComplexPrinter_printComplexWrapper(ComplexPrinter* printer, double z_real, double z_imag)
        {   
            printer->printComplexWrapper(z_real, z_imag);
        }   
    }
    

    Compile into a library in the usual way:

    g++ -shared -fPIC -o my_complex_cpp.so my_complex.cpp
    

    And call the wrapper from Python:

    from ctypes import CDLL, c_double
    cpp = CDLL('./my_complex_cpp.so')
    cpp.ComplexPrinter_printComplexWrapper.argtypes = [c_double, c_double]
    
    
    class ComplexPrinter:
    
        def __init__(self):
            self.obj = cpp.ComplexPrinter_new()
    
        def printComplex(self, z):
            cpp.ComplexPrinter_printComplexWrapper(c_double(z.real), c_double(z.imag))
    
    
    printer = ComplexPrinter()
    z = complex(1.0 + 1j * 2.0)
    printer.printComplex(z)
    

    Fortran

    And finally in Fortran we have the original subroutine in my_complex.f90:

    subroutine print_complex(z)
      use iso_c_binding, only: c_double_complex
      implicit none
      complex(c_double_complex), intent(in) :: z
      character(len=16) :: my_format = "(f4.1,a3,f4.1,a)"
      print my_format, real(z), " + ", aimag(z), "i" 
    end subroutine print_complex
    

    To which we add the wrapper function (at the end of the same file):

    subroutine print_complex_wrapper(z_real, z_imag) bind(c, name="print_complex_wrapper")
      use iso_c_binding, only: c_double, c_double_complex
      implicit none
      real(c_double), intent(in) :: z_real, z_imag
      complex(c_double_complex) :: z
      z = cmplx(z_real, z_imag)
      call print_complex(z)
    end subroutine print_complex_wrapper
    

    Then compile into a library in the usual way:

    gfortran -shared -fPIC -o my_complex_f90.so my_complex.f90
    

    And call from Python (note the use of POINTER):

    from ctypes import CDLL, c_double, POINTER
    f90 = CDLL('./my_complex_f90.so')
    f90.print_complex_wrapper.argtypes = [POINTER(c_double), POINTER(c_double)]
    z = complex(1.0 + 1j * 2.0)
    f90.print_complex_wrapper(c_double(z.real), c_double(z.imag))