> "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.
In This Chapter
- Overview
- Learning Paths
- 14.1 The iso_c_binding Module and C-Compatible Kinds
- 14.2 The bind(c) Attribute: Naming and Linkage
- 14.3 Calling C from Fortran
- 14.4 Calling Fortran from C
- 14.5 Passing Arrays, Strings, and Structs — and the Row/Column-Major Gotcha
- Project Checkpoint
- Summary
- Spaced Review
- What's Next
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_pointermachinery 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 withuse, 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_bindingas a phrasebook. Fortran and C both have a word for "8-byte floating-point number," but they spell it differently (real(dp)versusdouble). The phrasebook does not translate the number — the bytes are already identical — it translates the name, so that when you declarereal(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). Atype(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, andc_f_pointerlean directly on the pointer machinery from Chapter 11.c_loc(x)requiresxto have thetargetattribute 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 toc_locmust becontiguous(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 thanreal(dp), even when the two kinds are numerically identical on your machine? 2. What does it mean ifc_int128_thas a negative value on your platform? 3. Which twoiso_c_bindingprocedures convert between a Fortran object and a raw C address, and in which direction does each go?
Answers
(1)c_doubleis defined to match C'sdouble;dpmerely happens to. On an exotic platform where C'sdoublewere not IEEE-754 double,real(dp)could silently differ, breaking the interface — and in any casec_doubledocuments 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 ac_ptr(Fortran → C address);c_f_pointer(cp, fp[, shape])turns ac_ptrback 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 optionalname=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 exactlyfoo, case preserved, no underscore, no module decoration. This is what C sees.bind(c)with noname=→ the binding label defaults to the Fortran name in lower case (sosubroutine Foo() bind(c)binds asfoo).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 type —
type, 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 Cstruct. This is §14.5's centerpiece. - On a module variable —
integer(c_int), bind(c, name="verbosity") :: verbosity— gives a Fortran global variable C linkage, so it is the Cextern 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++ (throughextern "C"), Python (throughf2py,ctypes, orcffi), 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 be1or-1or0xFFFFFFFF, and that aCHARACTERargument secretly passed a hidden length. Codes carried per-compiler#ifdefthickets to paper over it. The 2003 standard replaced all of that withiso_c_bindingandbind(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 (
valueattribute). 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 abind(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 interfaceblock 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 writeimport :: c_double; both work.) bind(c, name="vec3_norm")says the linker symbol is exactlyvec3_norm— which is what the C compiler emitted.- Every argument is
real(c_double), value— the interoperable kind and by-value passing, matchingdouble xin 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
valueattribute:
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_normIt compiles cleanly and prints garbage — perhaps
vec3_norm(3,4,12) = 0.00, perhaps a huge number, perhaps a segmentation fault. Why? Withoutvalue, Fortran passes the addresses ofx,y,z. C'svec3_normreads those three addresses as if they were thedoublevalues, 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, checkvaluefirst.⚡ Performance Note: By-value passing copies. For three scalars that is free. But never put
valueon 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, thebind(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: double ↔ real(c_double) result, int ↔ integer(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'smainoccupies. You cannot link a Fortranprogramand a Cmaininto one executable — the linker will complain of a duplicatemain. So when C drives, the Fortran side must be a module or bare procedures with noprogramunit (as above). Conversely, in §14.3 the Fortranprogramwas the entry point and the C file supplied only functions, nomain. 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 doesnget thevalueattribute butvdoes not? 2. Why do we link the two object files withgfortranrather thangcc? 3. What goes wrong if the Fortran side is written as aprograminstead of amodulewhen C providesmain?
Answers
(1) C passes the scalarint nby value, so Fortran must receive a copy (value); C passes the array as a pointerdouble *v, which matches Fortran's default by-reference passing, so novalue. (2)gfortranautomatically links the Fortran runtimelibgfortranthat the Fortran code needs;gccwould require adding-lgfortranmanually. (3) A Fortranprogramand Cmainboth claim the executable's single entry point, causing a duplicate-mainlink 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 usesu(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
logicalis not interoperable with C's_Booleither. Fortran's default true/false bit pattern is compiler-defined and often not C's1/0. When a boolean crosses the boundary, declare itlogical(c_bool), never plainlogical— the same "say exactly what C means" discipline asreal(c_double)overreal(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 Cstruct. Its components must all be of interoperable type and kind; it may not haveallocatableorpointercomponents, type-bound procedures, or thesequenceattribute. 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, vyintoparticle_tanddouble vx, vy;into the C struct — in the same position in both — then haveparticle_reportadd 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).
-
In Chapter 3 you defined
dp = selected_real_kind(15, 307). How doesc_doublerelate todp, and why preferc_doublefor a variable that will be passed to C?
Answer
Both 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. -
(Chapter 3)
mean_ccomputessum(v) / real(n, c_double). Why is the explicitreal(n, c_double)conversion necessary rather than writingsum(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. -
(Chapter 6) The C interface for
vec3_normmarksx, y, zasvalue, but Chapter 6'sstepmarks its arguments withintent. Why can a dummy argument not be bothvalueandintent(out)in the way you might expect, and what doesvalueimply about who sees a change?
Answer
A `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. -
(Chapter 6) Why does the
bind(c)array argumentv(n)inmean_cuse an explicit-shape declaration rather than the assumed-shapev(:)that Chapter 6 recommended as the modern default?
Answer
Assumed-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.) -
(Chapters 3 & 6) A colleague declares a C-bound flag as
logical, value :: verboseand is surprised C seesverboseas neither true nor false it recognizes. What is wrong, and what is the fix?
Answer
Default `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.