C Interoperability
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:
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.
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.