Interfacing with Fortran from Python using 'iso_c_binding'

Hi there.
I am trying to learn how to interface with Fortran from Python using ‘iso_c_binding’. To test interoperability, I wrote a simple Fortran module to calculate the sum and mean of a 1D array, which is given below:

module module_calculate_stats
    use iso_fortran_env
    use iso_c_binding
    implicit none

    contains
        subroutine calculate_sum(array, array_sum) bind(C)
            real(kind=c_float), intent(in), dimension(:)::array
            real(kind=c_float), intent(out)::array_sum
            array_sum=sum(array)
        end subroutine calculate_sum

        subroutine calculate_mean(array, array_mean) bind(C)
            real(kind=c_float), intent(in), dimension(:)::array
            real(kind=c_float), intent(out)::array_mean
            array_mean=sum(array)/size(array)
        end subroutine calculate_mean

end module module_calculate_stats

I have successfully called the module subroutines from another Fortran program. The module was compiled into a shared library using the following command:

gfortran --shared module_calculate_stats.f90 -o module_calculate_stats.so -fPIC

Now, onto the Python program that calls the Fortran module:

import ctypes as ct

import numpy as np

if __name__=='__main__':

    fort_lib=ct.CDLL('./module_calculate_stats.so')
    fort_lib.calculate_sum.argtypes=[
            ct.POINTER(ct.c_float),]
    fort_lib.calculate_sum.restype=ct.c_float

    total=ct.c_float(-1)
    array=np.array(
            [1, 2, 3, 4, 5, 6], dtype=ct.c_float, order='F')
    array_ptr=array.ctypes.data_as(ct.POINTER(ct.c_float))

    fort_lib.calculate_sum(array_ptr, ct.byref(total))
    print(total)

When I run the Python program, I either get a segmentation fault (core dumped) or Internal Error: Invalid type in descriptor.
What am I doing wrong and how do I fix it?

Well you have to pass explicitly the size of the array, in Fortran:

        subroutine calculate_mean(array, array_mean, n) bind(C)
            real(kind=c_float), intent(in), dimension(n)::array
            real(kind=c_float), intent(out)::array_mean
            integer(c_int), value :: n
            array_mean=sum(array)/n
        end subroutine calculate_mean

And in Python I suggest to use:

from numpy.ctypeslib import ndpointer
...
fort_lib.calculate_sum.argtypes=[
            ndpointer(dtype=ct.c_float),
            ct.POINTER(ct.c_float),
            ct.c_int]
...

def my_mean(arr):
    arr = np.ascontiguousarray(arr, dtype=ct.c_float)
    mean = ct.c_float(0.0)
    n = arr.size
    fort_lib.calculate_sum(array, mean, n)
    return mean.value

I haven’t checked the code so there may be some small errors, but this is the idea.

This is a complete working example:

module module_calculate_stats
    use iso_fortran_env
    use iso_c_binding
    implicit none

    contains
        subroutine calculate_sum(array, array_sum, n) bind(C)
            integer(c_int), value :: n
            real(kind=c_float), intent(in), dimension(n)::array
            real(kind=c_float), intent(out)::array_sum
            array_sum=sum(array)
        end subroutine calculate_sum

        subroutine calculate_mean(array, array_mean, n) bind(C)
            integer(c_int), value :: n
            real(kind=c_float), intent(in), dimension(n)::array
            real(kind=c_float), intent(out)::array_mean
            array_mean=sum(array)/n
        end subroutine calculate_mean

end module module_calculate_stats

And the Python part:

import ctypes as ct
import numpy as np
from numpy.ctypeslib import ndpointer

fort_lib=ct.CDLL('./module_calculate_stats.so')

fort_lib.calculate_sum.argtypes=[
            ndpointer(dtype=ct.c_float),
            ct.POINTER(ct.c_float),
            ct.c_int]
fort_lib.calculate_sum.restype=None


def my_mean(arr):
    arr = np.ascontiguousarray(arr, dtype=ct.c_float)
    mean = ct.c_float(0.0)
    n = arr.size
    fort_lib.calculate_sum(arr, mean, n)
    return mean.value

print(my_mean([1,2,3,4,5]))

The actual call to the Fortran library should always be hided inside a python function, as you have to check that all the arguments are at least contiguous etc.

1 Like

Perhaps the examples provided here can help: fortran-in-python/src/example-ctypes at main · HugoMVale/fortran-in-python · GitHub

1 Like

Here another micro example that could help out https://github.com/scipy/scipy/issues/18118#issuecomment-1557557290

Thank you for your help. It did help me in fixing the issue.
Is there any reason you used the value argument for the n variable, couldn’t it have been intent(in) like the array variable?

Another question I have with your answer is, do I have to specify the array size to the Fortran module? As I have seen in many examples, you can just use : for dynamically allocated arrays.
And in my Fortran program that calls the subroutines, I used a dynamic array and did not pass the size to the subroutine.

Here is what Steve Lionel wrote on this topic python - Passing array to function with assumed-shape - Stack Overflow

For consistency, n should preferably have intent(in).
The attribute value is a matter of choice, but requires consistent handling because fortran passes arguments by reference and C by value:

  • if you add value, you can pass the argument “normally” in python.
  • if you do not add value, then you need to pass by reference in python.

Two examples below.

fortran-in-python/example_ctypes.py at main · HugoMVale/fortran-in-python · GitHub

from ctypes import CDLL, c_int, c_double, byref, POINTER
import numpy as np
import os


# %% Import dll
here = os.path.dirname(os.path.realpath(__file__))
mathtools = CDLL(os.path.join(here, './fortran/mathtools.dll'))

# %% intproduct (args passed by reference)

a = 3
b = 4
result = mathtools.intproduct(byref(c_int(a)), byref(c_int(b)))
print("intproduct: ", result)

# %% intproduct (args passed by value)

a = 3
b = 4
result = mathtools.intproduct_byvalue(c_int(a), c_int(b))
print("intproduct_byvalue: ", result)

fortran-in-python/mathtools.f90 at main · HugoMVale/fortran-in-python · GitHub

 integer(c_int) function intproduct(a, b) result(res) bind(c)
    !! Product of two integers directly in C types
    integer(c_int), intent(in) :: a, b
    res = a*b
 end function

 integer(c_int) function intproduct_byvalue(a, b) result(res) bind(c)
    !! Product of two integers directly in C types
    integer(c_int), value, intent(in) :: a, b
    res = a*b
 end function

Yes it could. No real reason, just a stylistic preference.
Strictly speaking when you pass a scalar variable with the value attribute its value may be passed directly on a register of the CPU, while if you use an intent(in) you pass the address of the variable on a register of the CPU, so you have to dereference that address. But the difference is negligible, so it is just a stylistic preference.
As I’m using procedure that should appear as a C procedure I stick to the C way of writing and way of passing arguments.
If I were calling Fortran from Fortran I would have used a variable with intent(in) as this is the way one writes in Fortran.

Of course there is a difference in Fortran between a variable with the value attribute and one with the intent(in) attribute.

hi @egio ,

I tried your example but I got the following error: AttributeError: function ‘calculate_sum’ not found.

Do you have any idea of the origin of the problem ?

Thank you .

I need to see your actual python and Fortran code, and how you compile the source code.
Remember that you should create a shared library, compiling typically the source with the option -shared and -fpic look at:
Managing libraries (static and dynamic libraries) — Fortran Programming Language

Fortran code:
Calc_Stats.f90 (750 Bytes)

Python code:

Summary

import ctypes as ct
import numpy as np
from numpy.ctypeslib import ndpointer

fort_lib=ct.CDLL(‘./Calc_stats.dll’)

fort_lib.calculate_sum.argtypes=[
ndpointer(dtype=ct.c_float),
ct.POINTER(ct.c_float),
ct.c_int]
fort_lib.calculate_sum.restype=None

fort_lib.calculate_mean.argtypes=[
ndpointer(dtype=ct.c_float),
ct.POINTER(ct.c_float),
ct.c_int]
fort_lib.calculate_mean.restype=None

def my_sum(arr):
arr = np.ascontiguousarray(arr, dtype=ct.c_float)
som = ct.c_float(0.0)
n = arr.size
fort_lib.calculate_sum(arr, mean, n)
return som.value

def my_mean(arr):
arr = np.ascontiguousarray(arr, dtype=ct.c_float)
mean = ct.c_float(0.0)
n = arr.size
fort_lib.calculate_mean(arr, mean, n)
return mean.value

print(my_sum([1,2,3,4,5]))
print(my_mean([1,2,3,4,5]))

I use IFORT to create my DLL :

  • $ ifort -c Calc_Stats.f90

  • $ ifort -dll -exe:Calc_Stats.dll Calc_Stats.obj

In windows you have to change your source like:

module module_calculate_stats
    use iso_fortran_env
    use iso_c_binding
    implicit none

    contains
        subroutine calculate_sum(array, array_sum, n) bind(C)
        !DEC$ ATTRIBUTES DLLEXPORT :: calculate_sum
            integer(c_int), value :: n
            real(kind=c_float), intent(in), dimension(n)::array
            real(kind=c_float), intent(out)::array_sum
            array_sum=sum(array)
        end subroutine calculate_sum

        subroutine calculate_mean(array, array_mean, n) bind(C)
        !DEC$ ATTRIBUTES DLLEXPORT :: calculate_mean
            integer(c_int), value :: n
            real(kind=c_float), intent(in), dimension(n)::array
            real(kind=c_float), intent(out)::array_mean
            array_mean=sum(array)/n
        end subroutine calculate_mean

end module module_calculate_stats

Note the !DEC$ ATTRIBUTES DLLEXPORT :: calculate_....
See: https://community.intel.com/t5/Intel-Fortran-Compiler/Creating-dll-from-fortran-file/m-p/1182026

@egio thank you.

If you use gfortran on windows (msys2) you can pass the option --export-all-symbol to the linker.

gfortran -o Calc_Stats.dll -Wl,--out-implib= Calc_Stats.dll.a,--export-all-symbols,--enable-auto-import, -Wl Calc_Stats.o

1 Like