Search code examples
dllfortranpython-cffi

calling a fortran dll from python using cffi with multidimensional arrays


I use a dll that contains differential equation solvers among other useful mathematical tools. Unfortunately, this dll is written in Fortran. My program is written in python 3.7 and I use spyder as an IDE.

I successfully called easy functions from the dll. However, I can't seem to get functions to work that require multidimensional arrays.

This is the online documentation to the function I am trying to call: https://www.nag.co.uk/numeric/fl/nagdoc_fl26/html/f01/f01adf.html

The kernel dies without an error message if I execute the following code:

import numpy as np
import cffi as cf

ffi=cf.FFI()
lib=ffi.dlopen("C:\Windows\SysWOW64\DLL20DDS")
ffi.cdef("""void F01ADF (const int *n, double** a, const int *lda, int *ifail);""")

#Integer
nx = 4
n = ffi.new('const int*', nx)
lda = nx + 1
lda = ffi.new('const int*', lda)
ifail = 0
ifail = ffi.new('int*', ifail)

#matrix to be inversed

ax1 = np.array([5,7,6,5],dtype = float, order = 'F')
ax2 = np.array([7,10,8,7],dtype = float, order = 'F')
ax3 = np.array([6,8,10,9],dtype = float, order = 'F')
ax4 = np.array([5,7,9,10], dtype = float, order = 'F')
ax5 = np.array([0,0,0,0], dtype = float, order = 'F')

ax = (ax1,ax2,ax3,ax4,ax5)

#Array
zx = np.zeros(nx, dtype = float, order = 'F')

a = ffi.cast("double** ", zx.__array_interface__['data'][0])
for i in range(lda[0]):
    a[i] = ffi.cast("double* ", ax[i].__array_interface__['data'][0])

lib.F01ADF(n, a, lda, ifail)

Since function with 1D arrays work I assume that the multidimensional array is the issues.

Any kind of help is greatly appreciated, Thilo


Solution

  • Not having access to the dll you refer to complicates giving a definitive answer, however, the documentation of the dll and the provided Python script may be enough to diagnose the problem. There are at least two issues in your example:

    • The C header interface:

      Your documentation link clearly states what the function's C header interface should look like. I'm not very well versed in C, Python's cffi or cdef, but the parameter declaration for a in your function interface seems wrong. The double** a (pointer to pointer to double) in your function interface should most likely be double a[] or double* a (pointer to double) as stated in the documentation.

    • Defining a 2d Numpy array with Fortran ordering:

      1. Note that your Numpy arrays ax1..5 are one dimensional arrays, since the arrays only have one dimension order='F' and order='C' are equivalent in terms of memory layout and access. Thus, specifying order='F' here, probably does not have the intended effect (Fortran using column-major ordering for multi-dimensional arrays).
      2. The variable ax is a tuple of Numpy arrays, not a 2d Numpy array, and will therefore have a very different representation in memory (which is of utmost importance when passing data to the Fortran dll) than a 2d array.

    Towards a solution

    My first step would be to correct the C header interface. Next, I would declare ax as a proper Numpy array with two dimensions, using Fortran ordering, and then cast it to the appropriate data type, as in this example:

    #file: test.py
    import numpy as np
    import cffi as cf
    
    ffi=cf.FFI()
    lib=ffi.dlopen("./f01adf.dll")
    ffi.cdef("""void f01adf_ (const int *n, double a[], const int *lda, int *ifail);""")
    
    # integers
    nx = 4
    n = ffi.new('const int*', nx)
    lda = nx + 1
    lda = ffi.new('const int*', lda)
    ifail = 0
    ifail = ffi.new('int*', ifail)
    
    # matrix to be inversed
    ax = np.array([[5,  7,  6,  5],
                   [7, 10,  8,  7],
                   [6,  8, 10,  9],
                   [5,  7,  9, 10],
                   [0,  0,  0,  0]], dtype=float, order='F')
    
    #  operation on matrix using dll
    print("BEFORE:")
    print(ax.astype(int))
    
    a = ffi.cast("double* ", ax.__array_interface__['data'][0])
    lib.f01adf_(n, a, lda, ifail)
    
    print("\nAFTER:")
    print(ax.astype(int))
    

    For testing purposes, consider the following Fortran subroutine that has the same interface as your actual dll as a substitute for your dll. It will simply add 10**(i-1) to the i'th column of input array a. This will allow checking that the interface between Python and Fortran works as intended, and that the intended elements of array a are operated on:

    !file: f01adf.f90
    Subroutine f01adf(n, a, lda, ifail)
      Integer, Intent (In) :: n, lda
      Integer, Intent (Inout) :: ifail
      Real(Kind(1.d0)), Intent (Inout) :: a(lda,*)
    
      Integer :: i
    
      print *, "Fortran DLL says: Hello world!"
    
      If ((n < 1) .or. (lda < n+1)) Then
        ! Input variables not conforming to requirements
        ifail = 2
    
      Else
        ! Input variables acceptable
        ifail = 0
    
        ! add 10**(i-1) to the i'th column of 2d array 'a'
        Do i = 1, n
          a(:, i) = a(:, i) + 10**(i-1)
        End Do
    
      End If
    
    End Subroutine
    

    Compiling the Fortran code, and then running the suggested Python script, gives me the following output:

    > gfortran -O3 -shared -fPIC -fcheck=all -Wall -Wextra -std=f2008 -o f01adf.dll f01adf.f90
    > python test.py
      BEFORE:
      [[ 5  7  6  5]
      [ 7 10  8  7]
      [ 6  8 10  9]
      [ 5  7  9 10]
      [ 0  0  0  0]]
    
      Fortran DLL says: Hello world!
    
      AFTER:
      [[   6   17  106 1005]
      [   8   20  108 1007]
      [   7   18  110 1009]
      [   6   17  109 1010]
      [   1   10  100 1000]]