31 min read

> "C is not a big language, and it is not well served by a big book."

Prerequisites

  • 3
  • 5
  • 6
  • 9
  • 11

Learning Objectives

  • Use the intrinsic iso_c_binding module and its C-interoperable kinds (c_int, c_double, c_char, c_ptr) to declare data that Fortran and C agree on bit for bit.
  • Apply the bind(c) attribute to give a procedure, a derived type, or a variable a predictable C linkage name, and explain how it defeats compiler name mangling.
  • Call a C routine from Fortran through an interface block, correctly choosing the value attribute for by-value scalar arguments.
  • Call a Fortran routine from a C driver, and issue the mixed compile-and-link command that binds the two object files into one program.
  • Pass arrays, null-terminated strings, and interoperable derived types across the boundary, and reconcile Fortran's column-major layout with C's row-major convention.

Chapter 14: C-Fortran Interoperability — ISO_C_BINDING and Calling Between Languages

"C is not a big language, and it is not well served by a big book." — Brian Kernighan and Dennis Ritchie, The C Programming Language

Overview

C is small, old, and everywhere. It is the language the operating system is written in, the language the system libraries expose, and — crucially — the language whose calling convention every other language has agreed to speak. When Python loads a NumPy routine, when Julia calls a system function, when R links a statistics library, the actual handshake happens through the C application binary interface: a low-level agreement about how arguments are placed in registers and on the stack, how names appear in the linker, and how a struct is laid out in memory. C is the lingua franca not because it is the best language but because it is the one everyone already translates to.

This chapter teaches Fortran to speak it. Since the 2003 standard, Fortran has had a standardized, portable mechanism for interoperating with C — the intrinsic module iso_c_binding and the bind(c) attribute — and it is one of the quiet triumphs of modern Fortran. Before 2003, calling between Fortran and C meant guessing your compiler's private name-mangling scheme, hoping its INTEGER matched C's int, and appending underscores by trial and error. After 2003, you write down the interface once, in a form the standard guarantees, and it compiles and links on every conforming compiler. This is the theme of the whole book made concrete: modern Fortran is a modern language, and interoperability is the feature that lets a seventy-year-old numerical powerhouse plug directly into the rest of the software world.

It is also the doorway to the next chapter. The way you call Fortran from Python — f2py, in Chapter 15 — is built on exactly the C interface you learn here; NumPy talks to your Fortran through the C ABI. Master this chapter and Chapter 15 becomes mostly bookkeeping.

In this chapter, you will learn to:

  • Declare Fortran variables whose kind, size, and representation match a specific C type exactly, using the named constants of iso_c_binding.
  • Give a Fortran procedure a stable, un-mangled name that a C linker can find, with bind(c).
  • Call a C function from Fortran — getting the by-value-versus-by-reference distinction right, which is where most first attempts fail.
  • Call a Fortran procedure from a C main, and write the compile-and-link command that joins the two.
  • Move the hard cases across the boundary: arrays (and the column-major/row-major trap), C strings (and their null terminator), and structs (as interoperable derived types).

Learning Paths

How to read this chapter by track. - 🔬 Scientist ("I want to call that C library / expose my kernel") — §14.1 and §14.3 are your core; read §14.5 for arrays and strings. This is the chapter that lets you reuse the C world. - 📖 Standard — read straight through; §14.2 is the standard's actual mechanism and worth close attention. - 🔧 Legacy ("I maintain a mixed C/Fortran code") — §14.2 (linkage, name mangling) explains the underscores you have been fighting; §14.5 covers the struct and array conventions old codes hard-coded. - ⚡ HPC ("I need to bind to MPI / CUDA / a system library") — the whole chapter; the c_ptr / c_loc / c_f_pointer machinery in §14.1 and §14.5 is how you hand a device or a communicator a raw pointer.


14.1 The iso_c_binding Module and C-Compatible Kinds

Two languages can only call each other if they agree on what a number is. When Fortran writes an integer into memory and C reads it back as an int, the bytes must line up: the same width, the same signedness, the same bit pattern. The trouble is that neither standard nails these down to a fixed size. A Fortran default integer is usually 32 bits and a C int is usually 32 bits, but "usually" is not a foundation you want to build a physics code on. What you need is a way to say, in Fortran, "give me the integer kind that is exactly C's int on this platform" — and let the compiler, which knows its companion C compiler, fill in the number.

That is precisely what the intrinsic module iso_c_binding provides.

Definition (iso_c_binding). An intrinsic module — built into every standard-conforming compiler, requested with use, intrinsic :: iso_c_binding — that supplies the named constants, derived types, and procedures needed to interoperate with C. Its most important exports are a set of kind parameters, one for each interoperable C type, each equal to the Fortran kind that matches that C type on the current platform. Introduced in Fortran 2003; extended in 2008 and 2018.

Because it is intrinsic, you do not install or link anything; you simply use it. The intrinsic keyword is optional but good practice — it tells the compiler (and the reader) that you mean the iso_c_binding, not a module of your own that happens to share the name.

Here are the kind constants you will reach for most often, beside the C types they match:

  Fortran declaration            matches C type        typical size
  ---------------------------    ------------------    ------------
  integer(c_int)                 int                   4 bytes
  integer(c_long)                long int              8 bytes (LP64)
  integer(c_size_t)              size_t                8 bytes (LP64)
  integer(c_int64_t)             int64_t               8 bytes
  real(c_float)                  float                 4 bytes
  real(c_double)                 double                8 bytes
  complex(c_double_complex)      double _Complex       16 bytes
  logical(c_bool)                _Bool                 1 byte
  character(kind=c_char)         char                  1 byte

Definition (C-interoperable kind). A Fortran kind parameter, exported by iso_c_binding, whose values a Fortran object shares — bit for bit — with the corresponding C type. A value of an interoperable kind can be passed to and from C with no conversion. If a platform has no Fortran kind matching some C type, the corresponding constant is negative, which is your (compile-time) signal that the type is unavailable there.

Notice the relationship to the precision kinds you already know. Back in Chapter 3 you defined the book's working precision as dp = selected_real_kind(15, 307), the kind that guarantees at least fifteen significant digits. On every platform this book targets, dp and c_double name the same underlying kind — IEEE 754 double precision — because C's double is also IEEE 754 double. So the two are interchangeable in practice. But they mean different things: dp means "enough precision for my science," and c_double means "exactly what C calls double." When a variable exists to cross the boundary, declare it real(c_double) and say what you mean; when it is purely internal, real(dp) keeps the numerical intent front and center. They will be the same integer value, and your code documents itself.

💡 Intuition: Think of iso_c_binding as a phrasebook. Fortran and C both have a word for "8-byte floating-point number," but they spell it differently (real(dp) versus double). The phrasebook does not translate the number — the bytes are already identical — it translates the name, so that when you declare real(c_double) you are guaranteed to have written down the very type the C side is expecting.

Two exports of iso_c_binding are not kinds at all but derived types, and they are how Fortran holds a raw C pointer:

  • c_ptr — an opaque derived type interoperable with any C object pointer (void *, double *, and so on). A type(c_ptr) variable carries an address and nothing else.
  • c_funptr — the same idea for a C function pointer, used for callbacks.

Along with them come the null constants c_null_ptr and c_null_funptr, the character constant c_null_char (C's '\0' string terminator, which §14.5 needs), and a small set of procedures:

  c_loc(x)               → returns a c_ptr holding the address of x
                           (x must have target or pointer, and be contiguous)
  c_f_pointer(cp, fp)    → makes Fortran pointer fp alias the C address in cp
  c_f_pointer(cp, fp, shape) → same, giving the array its shape
  c_associated(cp)       → .true. if cp is non-null (like C's  p != NULL)
  c_sizeof(x)            → size of x in bytes (like C's sizeof), Fortran 2008

🔗 Connection: c_ptr, c_loc, and c_f_pointer lean directly on the pointer machinery from Chapter 11. c_loc(x) requires x to have the target attribute for exactly the reason a Fortran pointer needs a target: you cannot take a stable address of something the compiler is free to keep in a register. And the array you hand to c_loc must be contiguous (Chapter 11 again), because a C pointer describes an unbroken block of memory — it has no notion of Fortran's array strides.

You will not use c_ptr in the first, simple examples — most interoperable calls pass numbers and arrays directly — but when a C API traffics in void * (memory allocators, opaque handles, MPI communicators, GPU device pointers), c_ptr with c_loc and c_f_pointer is the bridge. We return to it in §14.5.

🔄 Check Your Understanding. 1. Why is declaring a boundary-crossing real as real(c_double) safer than real(dp), even when the two kinds are numerically identical on your machine? 2. What does it mean if c_int128_t has a negative value on your platform? 3. Which two iso_c_binding procedures convert between a Fortran object and a raw C address, and in which direction does each go?

Answers (1) c_double is defined to match C's double; dp merely happens to. On an exotic platform where C's double were not IEEE-754 double, real(dp) could silently differ, breaking the interface — and in any case c_double documents that the variable exists to talk to C. (2) There is no Fortran kind on that platform interoperable with C's __int128/int128_t, so you cannot declare a variable of it; the negative value lets you detect this at compile time. (3) c_loc(x) turns a Fortran object into a c_ptr (Fortran → C address); c_f_pointer(cp, fp[, shape]) turns a c_ptr back into a usable Fortran pointer (C address → Fortran).


14.2 The bind(c) Attribute: Naming and Linkage

Agreeing on types is half the battle. The other half is names. When you compile a Fortran procedure called step, the compiler does not emit a symbol literally named step into the object file. It mangles the name — decorates it according to a private scheme — so that, among other things, a module procedure step in module heat_solver never collides with a step somewhere else.

Definition (name mangling). The compiler's transformation of a source-level procedure or variable name into the decorated symbol name that actually appears in the object file and the linker. Fortran compilers mangle differently from one another and differently from C, which is why, without help, a C program cannot find a Fortran routine by its source name.

The scheme is compiler-specific, and that is the whole problem. With gfortran, a plain external subroutine daxpy becomes the symbol daxpy_ — lowercased, with a trailing underscore. A module procedure step in module heat_solver becomes something like __heat_solver_MOD_step. A different compiler decorates it differently. A C programmer trying to call your Fortran has to reverse-engineer this, and any code that does so is nailed to one compiler.

The bind(c) attribute makes the problem disappear.

Definition (bind(c)). An attribute that gives a Fortran entity C linkage: a predictable, un-mangled binding label that a C compiler and linker will recognize, together with the guarantee that the entity obeys C's interoperability rules. Written after a procedure heading — subroutine f(...) bind(c) — or on a derived-type or variable declaration. An optional name= clause sets the exact linker name.

Attach bind(c) to a procedure and the compiler emits its symbol under a name you control:

subroutine step(...) bind(c, name="step_c")   ! linker symbol: exactly  step_c

The rules for the binding label are simple and worth memorizing:

  • bind(c, name="foo") → the linker symbol is exactly foo, case preserved, no underscore, no module decoration. This is what C sees.
  • bind(c) with no name= → the binding label defaults to the Fortran name in lower case (so subroutine Foo() bind(c) binds as foo).
  • bind(c, name="") (empty) → no binding label; a special case used when you want the interoperability rules but not a global symbol.

Because you choose the name, the module a bind(c) procedure lives in is invisible to C: whether step_c sits in module heat_solver or floats free, C calls step_c and the linker finds it. bind(c) cuts the symbol loose from the mangling.

You can see the difference. Compile a module with one ordinary procedure and one bind(c) procedure, then list the symbols in the object file with nm:

module demo
  implicit none
contains
  subroutine plain()                        ! ordinary module procedure
  end subroutine plain
  subroutine tidy() bind(c, name="tidy")     ! same, but with C linkage
  end subroutine tidy
end module demo
$ gfortran -std=f2018 -Wall -c demo.f90
$ nm demo.o | grep -i -E 'plain|tidy'
0000000000000000 T __demo_MOD_plain      <- mangled: module name baked in
0000000000000010 T tidy                  <- bind(c): exactly the name we asked for

The mangled __demo_MOD_plain is unfindable from C without knowing gfortran's private scheme; tidy is the clean symbol any linker resolves. (The symbol names are what matter; the exact hex offsets vary.) That contrast is the entire practical reason bind(c) exists.

bind(c) is not only for procedures. Two other uses matter:

  • On a derived typetype, bind(c) :: t — makes the type interoperable: it is laid out in memory with exactly the same field order, padding, and alignment as the equivalent C struct. This is §14.5's centerpiece.
  • On a module variableinteger(c_int), bind(c, name="verbosity") :: verbosity — gives a Fortran global variable C linkage, so it is the C extern int verbosity;. The two languages then share one storage location.

There are constraints, and they are the price of the guarantee. Every dummy argument of a bind(c) procedure must itself be interoperable (an interoperable kind, an interoperable type, or a c_ptr); the result of a bind(c) function must be an interoperable scalar or void (a subroutine). The basic subset we use in this chapter — interoperable scalars passed by value or by reference, and explicit-shape arrays — works identically on every compiler. (Fortran 2018 added the ability to pass allocatable, pointer, and assumed-shape arrays through bind(c) procedures using a C-side array descriptor, declared in the header ISO_Fortran_binding.h; it is powerful, newer, and beyond our scope here. We stick to the portable core.)

🚪 Threshold Concept. bind(c) is not really "the Fortran-to-C feature." It is the Fortran-to- everything feature. Because the C ABI is the universal calling convention, the moment a routine has a clean C linkage and interoperable arguments, it can be called by C, C++ (through extern "C"), Python (through f2py, ctypes, or cffi), Julia, Rust, and any other language that can dial a C function. You are not learning to talk to one language; you are learning to answer the phone for all of them.

📜 From History: Before Fortran 2003, mixed-language programming was folklore. You learned that your compiler appended one underscore (or two, or none), that its LOGICAL .true. might be 1 or -1 or 0xFFFFFFFF, and that a CHARACTER argument secretly passed a hidden length. Codes carried per-compiler #ifdef thickets to paper over it. The 2003 standard replaced all of that with iso_c_binding and bind(c) — one portable mechanism — and it is a large part of why modern Fortran integrates so cleanly into today's polyglot scientific stacks.


14.3 Calling C from Fortran

Now we make a call. Suppose a colleague hands you a small, fast, well-tested C routine and you want it in your Fortran program. Here it is — the Euclidean norm of a 3-vector:

/* cnorm.c -- a C routine we will call from Fortran. */
#include <math.h>

double vec3_norm(double x, double y, double z) {
    return sqrt(x*x + y*y + z*z);
}

To call it, Fortran needs to know its interface: its name, its argument types, its result type — and, the detail that trips everyone, how the arguments are passed. C passes scalars by value: the function receives a private copy of x, y, and z. Fortran, by default, passes by reference: it hands the callee the address of the argument. If you describe vec3_norm to Fortran without correction, Fortran will pass three addresses, C will interpret those addresses as the bit patterns of doubles, and you will get nonsense (or a crash). The fix is the value attribute, which tells Fortran to pass a copy — to match C.

Definition (value attribute). A dummy-argument attribute declaring that the argument is passed by value: the procedure receives a copy, and changes to it are not seen by the caller. It is how a Fortran interface matches C's default scalar-passing convention. It requires an explicit interface (which a bind(c) interface block always provides).

You describe the C function to Fortran with an interface block whose body carries bind(c):

! example-01-call-c.f90 -- Fortran calling the C routine vec3_norm.
program call_c
  use, intrinsic :: iso_c_binding, only: c_double
  implicit none

  interface
    function vec3_norm(x, y, z) bind(c, name="vec3_norm") result(r)
      use, intrinsic :: iso_c_binding, only: c_double
      real(c_double), value :: x, y, z
      real(c_double)        :: r
    end function vec3_norm
  end interface

  real(c_double) :: d
  d = vec3_norm(3.0_c_double, 4.0_c_double, 12.0_c_double)
  print '(a, f6.2)', 'vec3_norm(3,4,12) = ', d
end program call_c

! Expected output:
! vec3_norm(3,4,12) =  13.00

Read the interface carefully, because it is the template for every "call C from Fortran" you will ever write:

  • The interface … end interface block describes a procedure that lives elsewhere (in the C object file). It is a promise to the compiler about what to generate at the call site.
  • The body needs its own use, intrinsic :: iso_c_binding — an interface body is a separate scope and does not inherit the program's imports. (You could instead write import :: c_double; both work.)
  • bind(c, name="vec3_norm") says the linker symbol is exactly vec3_norm — which is what the C compiler emitted.
  • Every argument is real(c_double), value — the interoperable kind and by-value passing, matching double x in C.

Now compile them together. Two languages means two front-ends, then one link:

$ gcc -c cnorm.c
$ gfortran -std=f2018 -Wall example-01-call-c.f90 cnorm.o -o demo1 -lm
$ ./demo1
vec3_norm(3,4,12) =  13.00

The first line compiles the C source to an object file with the C compiler. The second compiles the Fortran and links it against cnorm.o, producing the executable. We drive the link with gfortran (not gcc) on purpose: the Fortran main program needs the Fortran runtime library, libgfortran, and gfortran links it automatically. We add -lm because the C routine calls sqrt from the math library. The output, $\sqrt{3^2 + 4^2 + 12^2} = \sqrt{169} = 13$, we computed by hand.

🐛 Find the Bug. A student writes the interface without the value attribute:

fortran function vec3_norm(x, y, z) bind(c, name="vec3_norm") result(r) use, intrinsic :: iso_c_binding, only: c_double real(c_double) :: x, y, z ! <-- no value real(c_double) :: r end function vec3_norm

It compiles cleanly and prints garbage — perhaps vec3_norm(3,4,12) = 0.00, perhaps a huge number, perhaps a segmentation fault. Why? Without value, Fortran passes the addresses of x, y, z. C's vec3_norm reads those three addresses as if they were the double values, squares three pointers, and takes a square root of nonsense. The compiler cannot catch it: each side is internally consistent; only the contract between them is broken. This is the single most common C-interop mistake — when a call gives absurd results, check value first.

⚡ Performance Note: By-value passing copies. For three scalars that is free. But never put value on a large array or struct argument — you would copy the whole thing on every call. Arrays and big structs cross the boundary by reference (an address), which is C's convention for them anyway, so no copy happens. Passing a million-element array by reference costs one pointer; passing it by value would cost a million doubles. §14.5 relies on this.

🐍 Python Comparison: This is the mechanism, not a curiosity. When you eventually call this same Fortran-wrapped C from Python, or call a Fortran routine from NumPy via f2py, the interface you are looking at is what the tooling generates for you under the hood — the interoperable kinds, the bind(c) symbol, the by-value/by-reference decision. Understanding it here means that when Chapter 15 automates it, you will know what the automation is doing and how to fix it when it misbehaves.


14.4 Calling Fortran from C

The traffic runs both ways, and this direction is the one that matters most for a Fortran programmer: your fast numerical kernel, called from a program — or a language — written in something else. Here the roles swap. Fortran provides the routine and gives it bind(c) so it has a clean symbol; C declares a matching prototype and calls it.

Let us expose a small Fortran routine — the mean of an array — to a C driver. Because it is a module procedure, no interface block is needed on the Fortran side; bind(c) alone makes it callable:

! example-02-fmean.f90 -- a Fortran routine, exposed to C via bind(c).
module fstats
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  implicit none
contains

  function mean_c(n, v) bind(c, name="mean_c") result(m)
    integer(c_int), value :: n         ! C passes the count by value
    real(c_double), intent(in) :: v(n) ! C passes the array by reference (a pointer)
    real(c_double) :: m
    m = sum(v) / real(n, c_double)
  end function mean_c

end module fstats

! Expected output (via the C driver below):
! mean = 6.00

Two decisions define the interface, and they are the mirror image of §14.3. The count n is value, because C will pass an int by value. The array v is not value: it is an explicit-shape dummy v(n), received by reference, which is exactly what a C double * is — the address of the first element. This is the natural, no-copy way arrays cross the boundary. Note also that n carries value while v carries intent(in); a value argument may not also be declared intent, since it is a private copy the routine could freely modify.

Now the C side. C declares what it is calling — the prototype — and calls it like any C function:

/* example-02-driver.c -- a C main() that calls the Fortran routine mean_c. */
#include <stdio.h>

/* The Fortran function, as C sees it: same name, matching types. */
double mean_c(int n, const double *v);

int main(void) {
    double v[5] = {2.0, 4.0, 6.0, 8.0, 10.0};
    double m = mean_c(5, v);
    printf("mean = %.2f\n", m);
    return 0;
}
/* Expected output:
 * mean = 6.00
 */

The C prototype double mean_c(int n, const double *v) matches the Fortran bind(c) interface term for term: doublereal(c_double) result, intinteger(c_int), value, double * ↔ the by-reference real(c_double) :: v(n). The name mean_c is the binding label we chose. C passes the array v as a pointer to its first element — precisely what the Fortran expects.

There is one subtlety in the build. Now the main is in C, but the program still contains Fortran, so it still needs libgfortran. Compile each source to an object file, then link with gfortran so the Fortran runtime comes along:

$ gfortran -std=f2018 -Wall -c example-02-fmean.f90
$ gcc -c example-02-driver.c
$ gfortran example-02-fmean.o example-02-driver.o -o demo2
$ ./demo2
mean = 6.00

If you instead linked with gcc, you would have to add -lgfortran (and possibly -lm) by hand; letting gfortran drive the link is the simpler, more portable choice. The mean of $\{2,4,6,8,10\}$ is $30/5 = 6$, hand-computed as always.

One reassurance about the runtime. You might worry that a Fortran routine reached from a C main needs some explicit "start the Fortran runtime" call before it is safe to use. With gfortran it does not: as long as libgfortran is linked (which driving the link with gfortran ensures), the runtime initializes itself on demand. Our mean_c does pure arithmetic and no I/O, which is the cleanest case, but even a called routine that opens a file or prints a diagnostic works — the library sets up its I/O machinery lazily the first time it is used. The rule of thumb is simply: if any Fortran ends up in the executable, let gfortran perform the final link.

⚠️ Common Pitfall — two mains, or none. A Fortran main program (program …) compiles to the program entry point, the same slot C's main occupies. You cannot link a Fortran program and a C main into one executable — the linker will complain of a duplicate main. So when C drives, the Fortran side must be a module or bare procedures with no program unit (as above). Conversely, in §14.3 the Fortran program was the entry point and the C file supplied only functions, no main. Exactly one entry point, and you decide which language owns it.

🔗 Connection: This is the pattern behind the anchor promise of Part III. A national-scale code often has a C or Python driver orchestrating Fortran numerical kernels; bind(c) is the seam. And it is how, in Chapter 21, the reverse also holds: Fortran calls down into LAPACK — itself Fortran, but reached on many systems through a C-compatible interface — putting the fastest linear algebra on the planet one clean call away.

🔄 Check Your Understanding. 1. In mean_c, why does n get the value attribute but v does not? 2. Why do we link the two object files with gfortran rather than gcc? 3. What goes wrong if the Fortran side is written as a program instead of a module when C provides main?

Answers (1) C passes the scalar int n by value, so Fortran must receive a copy (value); C passes the array as a pointer double *v, which matches Fortran's default by-reference passing, so no value. (2) gfortran automatically links the Fortran runtime libgfortran that the Fortran code needs; gcc would require adding -lgfortran manually. (3) A Fortran program and C main both claim the executable's single entry point, causing a duplicate-main link error; the Fortran side must be a module with no program unit.


14.5 Passing Arrays, Strings, and Structs — and the Row/Column-Major Gotcha

Scalars are easy. The interesting boundary crossings are the aggregates: arrays, strings, and structs. Each has one thing you must get right.

Arrays: the memory-order trap

A one-dimensional array crosses cleanly. C's double *v and Fortran's by-reference v(n) describe the same block of memory, in the same order, so the §14.4 array passed perfectly. The trouble begins in two dimensions, and it is one of the sharpest edges in all of mixed-language programming.

Recall from Chapter 5 that Fortran stores arrays in column-major order: in a(i,j), the first index varies fastest through memory. C stores in row-major order: in a[i][j], the last index varies fastest. The same rectangle of numbers, handed from one language to the other with no copy, is therefore transposed in the eyes of the other language.

  A 2x3 array of values 1..6, stored in memory as: 1 2 3 4 5 6

  Fortran reads it column-major as A(i,j), i=1..2, j=1..3:
        j=1  j=2  j=3
   i=1   1    3    5           (first index fastest: 1,2 then 3,4 then 5,6)
   i=2   2    4    6

  C reads the SAME bytes row-major as A[i][j], i=0..1, j=0..2:
        j=0  j=1  j=2
   i=0   1    2    3           (last index fastest: 1,2,3 then 4,5,6)
   i=1   4    5    6

  => Fortran's A(i,j) is C's A[j-1][i-1]. The axes are swapped.

🚪 Threshold Concept. No bytes are wrong and nothing is corrupted — the two languages simply index the same memory by different conventions. There is no copy that "fixes" this for free; a genuine transpose costs a full array copy. The professional habit is therefore to agree on a layout contract at the interface and honor it on both sides, rather than transpose. The usual contract: keep Fortran's column-major view (it is the one that makes the numerics fast), and have the C side index so that the fast axis is contiguous — i.e., C uses u[j*nx + i] where Fortran uses u(i+1, j+1). Same walk through memory, no copy, no surprise.

⚠️ Common Pitfall: The bug this creates is vicious because the program runs. A C driver that fills a grid row-major and a Fortran kernel that reads it column-major will each behave sensibly on their own; the result is just silently transposed — a heat source that should be on the left edge appears on the top edge. There is no crash and no compiler warning. Whenever a mixed-language 2-D result looks "rotated" or "flipped," suspect the layout contract before anything else.

For a plain one-dimensional array Fortran → C, the mechanics are exactly §14.4 in reverse:

interface
  function dsum(n, v) bind(c, name="dsum") result(s)
    use, intrinsic :: iso_c_binding, only: c_int, c_double
    integer(c_int), value :: n
    real(c_double), intent(in) :: v(n)   ! by reference: C sees  const double *
    real(c_double) :: s
  end function dsum
end interface

When the C side takes a raw void * by value instead of a typed pointer — as memory allocators and opaque-handle APIs do — you build the c_ptr yourself with c_loc, and (if C hands one back) recover a usable Fortran array with c_f_pointer:

real(c_double), allocatable, target :: a(:)   ! target: so c_loc can address it
type(c_ptr) :: p
real(c_double), pointer :: view(:)

allocate(a(100))
p = c_loc(a)                 ! a  void*  to a(1), to hand to a C API
! ... C fills the block through p ...
call c_f_pointer(p, view, [100])   ! view(1:100) now aliases the same memory

This is the c_ptr/c_loc/c_f_pointer triad from §14.1 doing real work; note the target attribute on a, without which c_loc is not allowed (Chapter 11).

Strings: mind the null terminator

C has no string type. A C "string" is a char * pointing at a run of bytes ending in a null character, '\0'. Fortran's character variables carry their length separately and are not null-terminated. Bridging them means adding the terminator yourself, using c_null_char from iso_c_binding. To call C's puts (which prints a null-terminated string), declare the argument as a c_char assumed-size array and append the terminator to the actual string:

interface
  subroutine puts(s) bind(c, name="puts")
    use, intrinsic :: iso_c_binding, only: c_char
    character(kind=c_char), intent(in) :: s(*)   ! C sees  const char *
  end subroutine puts
end interface
...
call puts(c_char_"Hello from Fortran, via C's puts" // c_null_char)

The c_char_"…" prefix makes the literal explicitly of C-character kind (on mainstream systems the default character kind already equals c_char, but saying so is portable), and // c_null_char concatenates the terminating null so C knows where the string ends. Forget the c_null_char and C reads past the end of your string until it stumbles on a stray zero byte — printing garbage, or crashing.

⚠️ Common Pitfall: A Fortran logical is not interoperable with C's _Bool either. Fortran's default true/false bit pattern is compiler-defined and often not C's 1/0. When a boolean crosses the boundary, declare it logical(c_bool), never plain logical — the same "say exactly what C means" discipline as real(c_double) over real(dp).

Structs: the interoperable derived type

Finally, aggregates of mixed fields — C's struct. Fortran matches a C struct with a derived type marked bind(c).

Definition (interoperable derived type). A derived type declared with the bind(c) attribute (type, bind(c) :: t), which the compiler lays out in memory with the same field order, padding, and alignment as the equivalent C struct. Its components must all be of interoperable type and kind; it may not have allocatable or pointer components, type-bound procedures, or the sequence attribute. A value of an interoperable derived type may be passed to and from C directly.

Here a particle struct is shared by both languages. Fortran defines the type and a routine over it; C defines the matching struct and calls the routine:

! example-03-struct.f90 -- an interoperable derived type shared with C.
module particles
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  implicit none

  type, bind(c) :: particle_t
    integer(c_int) :: id
    real(c_double) :: x, y, mass
  end type particle_t

contains

  function particle_report(p) bind(c, name="particle_report") result(r)
    type(particle_t), intent(in) :: p    ! by reference: C passes  const particle_t *
    real(c_double) :: r
    r = p%mass * (p%x*p%x + p%y*p%y)
  end function particle_report

end module particles

! Expected output (via the C driver below):
! particle 7: report = 50.00
/* example-03-driver.c -- C sharing a struct with Fortran. */
#include <stdio.h>

struct particle_t {           /* same field order and types as the Fortran type */
    int    id;
    double x, y, mass;
};

double particle_report(const struct particle_t *p);

int main(void) {
    struct particle_t p = { 7, 3.0, 4.0, 2.0 };
    double r = particle_report(&p);
    printf("particle %d: report = %.2f\n", p.id, r);
    return 0;
}
/* Expected output:
 * particle 7: report = 50.00
 */
$ gfortran -std=f2018 -Wall -c example-03-struct.f90
$ gcc -c example-03-driver.c
$ gfortran example-03-struct.o example-03-driver.o -o demo3
$ ./demo3
particle 7: report = 50.00

The result is $\text{mass}\,(x^2 + y^2) = 2\,(3^2 + 4^2) = 2 \times 25 = 50$. Two points make this work, and both are the bind(c)-on-a-type guarantee earning its keep. First, the field order and types match exactly: Fortran's id, x, y, mass (one c_int, three c_double) mirror C's int id; double x, y, mass;. Second — and this is the part you would otherwise get wrong by hand — the padding matches automatically. On a typical platform, C inserts four bytes of padding after id so that the 8-byte double x starts on an 8-byte boundary, making the struct 32 bytes rather than 28. A hand-rolled Fortran type with no notion of C's alignment rules would misplace every field after the first. The bind(c) attribute tells the Fortran compiler to use its companion C compiler's layout rules, so the padding lines up without your intervention. (You can confirm sizes agree with c_sizeof(p) in Fortran and sizeof in C — both report 32.)

🧩 Try It Yourself: Add a field. Put real(c_double) :: vx, vy into particle_t and double vx, vy; into the C struct — in the same position in both — then have particle_report add the kinetic term. Predict the new number before you compile. Now delete the field from the C struct only, leaving the Fortran type unchanged, and predict what happens (the two sides disagree on the struct's size and layout; C reads the wrong bytes for every field after the mismatch — a silent, corrupting bug that no compiler catches, because neither side is individually wrong). Keeping the two definitions in lockstep is the eternal discipline of shared structs.


Project Checkpoint

This checkpoint is optional and advanced; the solver stays pure Fortran. But it is a genuinely useful capability: exposing the solver's time step to C so that a C — or, later, a Python — driver could run the simulation. We add a thin bind(c) shim over the same computation, leaving the canonical step(field, alpha, dt) from Chapter 6 untouched.

There is a reason we cannot simply put bind(c) on the existing routine: it takes a field_t (Chapter 9), and field_t has an allocatable component u(:,:) — which an interoperable derived type may not have (§14.5). So the C-facing shim takes the field unpacked into interoperable arguments: the dimensions, the raw array, and the physical parameters. It performs one explicit forward-Euler update of the interior with the five-point Laplacian (the numerics belong to Chapter 24; here we only expose them):

$$ u_{i,j}^{\text{new}} = u_{i,j} + \alpha\,\Delta t\left(\frac{u_{i+1,j} - 2u_{i,j} + u_{i-1,j}}{\Delta x^2} + \frac{u_{i,j+1} - 2u_{i,j} + u_{i,j-1}}{\Delta y^2}\right) $$

subroutine step_c(nx, ny, u, dx, dy, alpha, dt) bind(c, name="step_c")
  integer(c_int), value :: nx, ny                 ! by value, like C's  int
  real(c_double), intent(inout) :: u(nx, ny)      ! by reference: C's  double *
  real(c_double), value :: dx, dy, alpha, dt
  real(c_double) :: unew(nx, ny)
  integer :: i, j
  unew = u
  do j = 2, ny-1                 ! j outer, i inner: the column-major-friendly order
    do i = 2, nx-1
      unew(i,j) = u(i,j) + alpha*dt * (                                   &
           (u(i+1,j) - 2.0_c_double*u(i,j) + u(i-1,j)) / (dx*dx) +        &
           (u(i,j+1) - 2.0_c_double*u(i,j) + u(i,j-1)) / (dy*dy) )
    end do
  end do
  u = unew
end subroutine step_c

The array u is passed by reference (no value) — a copy of a whole grid on every step would be absurd (§14.5's performance note) — while the scalars are value. A C driver treats u as a flat block indexed column-major, honoring the layout contract from §14.5 so no transpose is needed:

/* A C front-end for the Fortran heat step (compile the module WITHOUT its test program). */
void step_c(int nx, int ny, double *u, double dx, double dy,
            double alpha, double dt);
/* index Fortran u(i+1, j+1) as u[j*nx + i] — the fast axis (i) contiguous */

The full code/project-checkpoint.f90 wraps step_c in a module with a short Fortran test program that sets a 3×3 grid to zero with a hot center u(2,2) = 100, takes one step with $\alpha = \Delta x = \Delta y = 1$, $\Delta t = 0.1$, and prints the result. By hand, the Laplacian at the single interior point is $(0 - 200 + 0) + (0 - 200 + 0) = -400$, so $u(2,2) \to 100 + (1)(0.1)(-400) = 60$. The expected output is u(2,2) after one step = 60.00. How it feeds the capstone: the same shim, once the solver is parallel and validated, is what lets a non-Fortran driver script a parameter sweep over your kernel — the interoperability seam that makes the finished code a library, not just a program.


Summary

This chapter connected Fortran to the C ABI — and through it, to essentially every other language — with two standard tools, iso_c_binding and bind(c).

Idea The short version
iso_c_binding Intrinsic module of C-interoperable kinds (c_int, c_double, c_char, …), the c_ptr/c_funptr types, and c_loc/c_f_pointer. use, intrinsic :: iso_c_binding.
C-interoperable kind A kind whose Fortran object matches a C type bit for bit. Declare boundary data real(c_double), integer(c_int) — say what C means.
bind(c) Gives a procedure, type, or variable a clean C linkage name, defeating name mangling. bind(c, name="foo") sets the exact symbol.
value Passes a scalar by value to match C; omit it (default by reference) for arrays and structs, which C passes as pointers.
Calling C from Fortran An interface block with a bind(c) body; value on by-value scalars; link the C object with gfortran.
Calling Fortran from C bind(c) on a module procedure (no program); a matching C prototype; link with gfortran for libgfortran.
Interoperable derived type type, bind(c); same fields, order, and (automatically) padding as the C struct; no allocatable/pointer components.
The 2-D gotcha Fortran column-major vs C row-major transposes a shared 2-D array. Agree a layout contract (u(i+1,j+1)u[j*nx+i]); never rely on a free transpose.
Strings C strings are null-terminated; append c_null_char, declare character(kind=c_char).

The two things to memorize: first, the by-value/by-reference rule — scalars from C need value, arrays and structs do not, and getting it wrong gives silent garbage, not a compiler error. Second, the column-major/row-major transpose — the same 2-D memory is indexed oppositely by the two languages, so fix a contract at the interface rather than hoping for a copy that never comes.

Mixed-language compile pattern to keep: gcc -c foo.c then gfortran bar.f90 foo.o -o prog — compile C with the C compiler, and drive the final link with gfortran so the Fortran runtime is included.

Spaced Review

Revisiting Chapter 3 (kinds and types) and Chapter 6 (procedures and intent).

  1. In Chapter 3 you defined dp = selected_real_kind(15, 307). How does c_double relate to dp, and why prefer c_double for a variable that will be passed to C?

    AnswerBoth name the double-precision real kind, and on every mainstream platform they are the *same* integer kind value (IEEE 754 double). But `dp` is defined by a *precision request* ("at least 15 digits") while `c_double` is defined as "exactly C's `double`." For a boundary-crossing variable, `c_double` both guarantees the match and documents the intent.

  2. (Chapter 3) mean_c computes sum(v) / real(n, c_double). Why is the explicit real(n, c_double) conversion necessary rather than writing sum(v) / n?

    Answer`n` is an integer. Dividing a real `sum(v)` by an integer `n` is mixed-mode arithmetic and *would* promote `n` to real here — so `sum(v)/n` is actually fine numerically. The explicit `real(n, c_double)` is written for clarity and to guarantee the division happens in `c_double`; the real trap Chapter 3 warned about is *integer/integer* division (e.g. `sum_int/n` truncating), which this avoids by construction.

  3. (Chapter 6) The C interface for vec3_norm marks x, y, z as value, but Chapter 6's step marks its arguments with intent. Why can a dummy argument not be both value and intent(out) in the way you might expect, and what does value imply about who sees a change?

    AnswerA `value` argument is a private *copy*; the callee may modify it, but the caller never sees the change (there is nothing to write back to). It may carry `intent(in)` redundantly but pairing it with `intent(out)`/`intent(inout)` is contradictory — those intents promise the caller receives a result, which by-value passing cannot deliver. Use `value` for inputs C passes by value; use by-reference `intent(out)`/`intent(inout)` when the caller must see the update.

  4. (Chapter 6) Why does the bind(c) array argument v(n) in mean_c use an explicit-shape declaration rather than the assumed-shape v(:) that Chapter 6 recommended as the modern default?

    AnswerAssumed-shape `v(:)` is passed with a Fortran *array descriptor* (a hidden structure carrying bounds and strides), which plain C does not understand. Explicit-shape `v(n)` is passed as a bare address — exactly a C `double *` — so it is the portable interoperable form. (F2018 *can* pass assumed-shape through `bind(c)` using a C-side descriptor header, but that is the advanced path, not the simple one.)

  5. (Chapters 3 & 6) A colleague declares a C-bound flag as logical, value :: verbose and is surprised C sees verbose as neither true nor false it recognizes. What is wrong, and what is the fix?

    AnswerDefault `logical` is not interoperable — its bit pattern for true/false is compiler-defined and need not be C's `1`/`0`. Declare it `logical(c_bool)` (the kind that matches C's `_Bool`). Same lesson as `c_double` over `dp`: use the interoperable kind for anything that crosses the boundary.

What's Next

You have taught Fortran to speak C, which means you have taught it to speak to almost everything. The most valuable thing to do with that skill is to reach the world's most popular language for scientific orchestration: Python. Chapter 15 does exactly that with f2py, a tool that reads your Fortran and generates — automatically — the very interoperable interface you have been writing by hand, wrapping your kernel as a Python module. You will call Fortran from a NumPy program, meet the column-major issue again (this time as NumPy's order='F'), and finally measure the ten-to-a-hundred-fold speedup that is the whole reason to pair the two languages. Everything you just learned about kinds, bind(c), by-value passing, and array layout is what makes that automation comprehensible — and fixable when it breaks. Let's wrap a Fortran kernel for Python.