C Interoperability

Since Fortran 2003, a standardized bridge to C has been part of the language: iso_c_binding maps types, bind(c) fixes calling conventions and names, and every serious scientific stack — LAPACK callers in Python, GPU runtimes, netCDF — relies on it. This lesson crosses the border in both directions.

The ABI bridge model

Calling another language well means agreeing on three things: how names appear to the linker, how arguments are passed, and which data formats are exchanged. bind(c) takes over all three — the routine gets an exact C symbol name, scalars and arrays follow the C ABI, and iso_c_binding provides the kind constants that match C types on your platform:

C and Fortran share one ABI through iso_c_binding

Fig. 1 — One interface, verified by both compilers, no glue code.

module c_api
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_char
  implicit none
  interface
    ! Declare the C function so Fortran can call it.
    integer(c_int) function c_add(a, b) bind(c, name="c_add")
      import :: c_int
      integer(c_int), value :: a, b    ! value = pass-by-value (C default)
    end function c_add
  end interface
contains
  ! Export a Fortran subroutine to C with a fixed, unmangled name.
  subroutine scale_array(n, arr, factor) bind(c, name="scale_array")
    integer(c_int), value :: n
    real(c_double), intent(inout) :: arr(n)
    real(c_double), value :: factor
    arr = arr * factor
  end subroutine scale_array
end module c_api

The value attribute is mandatory for C scalars (C passes by value; Fortran defaults to reference). The bind(c) symbols are ordinary link names — the C side declares matching headers and links the same object files:

/* call_demo.c — the C side of the same contract */
#include <stdio.h>
int  c_add(int a, int b);
void scale_array(int n, double *arr, double factor);

int main(void) {
    int sum = c_add(2, 3);            /* Fortran function, C name */
    double v[3] = {1.0, 2.0, 3.0};
    scale_array(3, v, 10.0);          /* Fortran subroutine */
    printf("sum=%d  v[1]=%.1f\n", sum, v[1]);
    return 0;
}
gfortran -c c_api.f90
gcc     -c call_demo.c
gfortran -o demo c_api.o call_demo.o     # one link, two languages
./demo                                   # sum=5  v[1]=20.0

Pointers and memory: C_LOC and C_F_POINTER

Bridging allocations is where the model needs care. c_loc(x) produces a type(c_ptr) — a raw address Fortran can hand to C. c_f_pointer(ptr, fptr, shape) converts a C address back into a typed Fortran pointer with a shape. The pattern below asks a C allocator for memory, then views it as a Fortran array — the standard way to receive buffers from C libraries:

program buffer_view
  use, intrinsic :: iso_c_binding
  implicit none
  interface
    function make_buffer(n) bind(c, name="make_buffer") result(p)
      import :: c_int, c_ptr
      integer(c_int), value :: n
      type(c_ptr) :: p
    end function make_buffer
  end interface
  type(c_ptr) :: raw
  real(c_double), pointer :: view(:)
  integer :: n = 3
  raw = make_buffer(n)
  call c_f_pointer(raw, view, [n])    ! interpret as a rank-1 array
  view = 1.0
  print *, view, associated(view)
end program buffer_view

When to use which strategy

Three practical routes exist. Direct bind(c) in one executable — the fastest, what this lesson demonstrates — suits Fortran kernels called from C drivers and vice versa. A shared library exposing a C ABI (gfortran -shared -fPIC) lets Python, Rust, R and Java all load the same Fortran math with ctypes/FFI — the python-interop lesson builds exactly this. Compile-time #include or preprocessor is legacy and fragile; avoid it. In every route, the type mapping table (integer(c_int), real(c_double), character(kind=c_char), type(c_ptr)) is the single source of truth both sides must respect.

Classic failure: forgetting value on a C scalar turns an int argument into a pointer to int, and the C side reads memory garbage. Rule: scalar C arguments always carry value; arrays are never value and are passed by reference as in Fortran.