Interoperability with C

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. Every Fortran example was compiled warning-free with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, each C file was compiled with gcc -std=c17 -Wall -Wextra -pedantic (GCC 16.2), and the examples were also built and run with LLVM Flang 23 (23.1.4), in a Debian container on an AArch64 (64-bit Arm) machine. The output shown is real output; where the two compilers differ, or reject a feature, the page says so and shows the real diagnostics. Sizes and kind values are the ones of that platform (Linux, LP64 data model). Interoperability with C is clause 18 of the standard: 18.2 is the iso_c_binding module, 18.3 says which Fortran entities interoperate with which C entities, 18.9 covers global variables and 18.10 procedures. The bind attribute is in 8.5.5 and in 15.6.2.1 (proc-language-binding-spec), value in 8.5.19.

Fortran has had a standard way to call C, and to be called from C, since Fortran 2003: the iso_c_binding module supplies the kinds, pointer types and conversion procedures, and the bind(c) attribute gives a procedure, variable or derived type the C view of itself. Both directions need nothing but the compilers' normal object files. Fortran 2018 absorbed the technical specification ISO/IEC TS 29113 and relaxed most of the restrictions of the first design (assumed-type, optional, allocatable and assumed-shape dummy arguments; the latter two through C descriptors, the subject of C Descriptors and ISO_Fortran_binding), and Fortran 2023 added f_c_string, c_f_strpointer and the lower argument of c_f_pointer. This page covers the data and procedures that cross the boundary as plain C values and pointers, using the running example heatflow, whose Jacobi and FTCS kernels are exposed to C through the module heatflow_capi and the header heatflow.h. How to build that library for C, C++ and Python callers is in Calling Fortran from C, C++ and Python, and the same ideas for a MATLAB gateway in MATLAB: MEX, Engine and MAT-File APIs. The C language side (pointers, structures, linkage) is described in C Pointers, Structures, Unions and Type Aliases and Storage Duration, Scope and Linkage; the same boundary seen from Rust is in FFI & C Interop and from Go in cgo and C Interoperability.

The model: four pieces and a companion processor

The standard speaks of a companion processor: the C compiler whose types and calling conventions the Fortran processor agrees to follow. Interoperability is then a statement about entities, not about languages. A Fortran entity is interoperable if an equivalent C entity could be written (18.1), whether or not one exists. A call across the boundary involves four pieces (18.3.7, Note 4): the reference in the caller, the interface or prototype the caller sees, the interface or prototype the callee is described by, and the function itself. The compiler can check only the pieces it sees, so a wrong interface body is a silent error, not a diagnostic: nothing compares a Fortran interface body with a C definition.

Direction What you write

Fortran calls C

an interface block describing the C function with bind(c) (and name= when the names differ), and the C object file or library at link time

C calls Fortran

a Fortran procedure with bind(c), a C prototype that matches it (usually in a header), and the Fortran object file or library, plus the Fortran runtime library at link time

C data seen from Fortran

a module variable with bind(c), a bind(c) derived type for a struct, c_ptr for void *, c_funptr for function pointers

Fortran data seen from C

the same declarations read the other way; arrays are passed as the address of the first element

The rules for the arguments of an interoperable procedure (15.6.2.1, C1561-C1568) are short. A function result must be an interoperable scalar. Each dummy argument must be an interoperable variable or procedure, or, since Fortran 2018, assumed-shape, assumed-rank, assumed-type, of assumed character length, allocatable or a pointer; with the latter group the C function receives a pointer to a descriptor, not to the data (18.3.7). A dummy argument cannot be a coarray, and an array dummy argument cannot have value. An interoperable procedure cannot be elemental, optional and value cannot both be given to a dummy argument of the same procedure (C869), and a C function with a variable argument list (...) cannot be described (18.3.7, item 7); Fortran has no portable way to call printf.

The iso_c_binding kinds and constants

Fortran’s default integer is not guaranteed to be a C int, and real need not be a C float. The kinds that mean a specific C type live in iso_c_binding (18.2.2, Table 18.2). Each C type has a named constant for the kind of the interoperating Fortran type, and a program uses the constant rather than a number:

C type Fortran declaration Notes

int, short, long, long long

integer(c_int), integer(c_short), integer(c_long), integer(c_long_long)

the sizes are the platform’s

signed char, unsigned char

integer(c_signed_char)

there are no unsigned kinds, so unsigned char uses the signed kind

size_t, ptrdiff_t, intptr_t, intmax_t

integer(c_size_t), integer(c_ptrdiff_t), integer(c_intptr_t), integer(c_intmax_t)

c_ptrdiff_t came with the Fortran 2018 content of TS 29113

int8_t …​ int64_t, int_leastN_t, int_fastN_t

integer(c_int8_t) …​ integer(c_int64_t), c_int_least16_t, c_int_fast32_t

exact-width kinds are the portable choice for data formats

float, double, long double

real(c_float), real(c_double), real(c_long_double)

a negative value means that no Fortran kind matches

float _Complex, double _Complex

complex(c_float_complex), complex(c_double_complex)

same values as the real kinds

_Bool

logical(c_bool)

not the default logical

char

character(kind=c_char)

length must be 1 for a scalar variable; arrays of character(kind=c_char) model C strings

void *, other object pointers

type(c_ptr)

opaque; see C pointers and function pointers

function pointers

type(c_funptr)

opaque

If the companion processor has a type without a matching Fortran kind, the constant is negative. For the real kinds, -1 means that no Fortran real kind has the precision of the C type, -2 that none has its range, -3 that none has either and -4 that there is no interoperating kind for another reason; for the integer kinds, -1 means that the C type exists and has no Fortran kind and -2 that the C type does not exist (18.2.2). Test with if (c_long_double > 0) before relying on a constant that might be negative. The program below prints what the two compilers report on this machine:

program c_kinds
  use, intrinsic :: iso_c_binding
  implicit none
  print '(a, i0)', 'c_int           = ', c_int
  print '(a, i0)', 'c_short         = ', c_short
  print '(a, i0)', 'c_long          = ', c_long
  print '(a, i0)', 'c_long_long     = ', c_long_long
  print '(a, i0)', 'c_signed_char   = ', c_signed_char
  print '(a, i0)', 'c_size_t        = ', c_size_t
  print '(a, i0)', 'c_int8_t        = ', c_int8_t
  print '(a, i0)', 'c_int64_t       = ', c_int64_t
  print '(a, i0)', 'c_intptr_t      = ', c_intptr_t
  print '(a, i0)', 'c_float         = ', c_float
  print '(a, i0)', 'c_double        = ', c_double
  print '(a, i0)', 'c_long_double   = ', c_long_double
  print '(a, i0)', 'c_float_complex = ', c_float_complex
  print '(a, i0)', 'c_bool          = ', c_bool
  print '(a, i0)', 'c_char          = ', c_char
  print '(a, l1, a, l1)', 'c_bool true/false stored as logical: ', logical(.true., c_bool), ' ', logical(.false., c_bool)
  print '(a, i0, a, i0, a, i0)', 'c_sizeof(1_c_int)=', c_sizeof(1_c_int), ' c_sizeof(1.0_c_double)=', c_sizeof(1.0_c_double), &
    ' c_sizeof(1.0_c_long_double)=', c_sizeof(1.0_c_long_double)
  print '(a, 8(i0, 1x))', 'codes null alert backspace formfeed newline cr htab vtab: ', &
    iachar(c_null_char), iachar(c_alert), iachar(c_backspace), iachar(c_form_feed), iachar(c_new_line), &
    iachar(c_carriage_return), iachar(c_horizontal_tab), iachar(c_vertical_tab)
  print '(a, l1, a, l1)', 'c_associated(c_null_ptr)=', c_associated(c_null_ptr), '  c_associated(c_null_funptr)=', c_associated(c_null_funptr)
end program c_kinds

GNU Fortran 16.2 and LLVM Flang 23 printed the same lines (the first run is the GNU Fortran one):

c_int           = 4
c_short         = 2
c_long          = 8
c_long_long     = 8
c_signed_char   = 1
c_size_t        = 8
c_int8_t        = 1
c_int64_t       = 8
c_intptr_t      = 8
c_float         = 4
c_double        = 8
c_long_double   = 16
c_float_complex = 4
c_bool          = 1
c_char          = 1
c_bool true/false stored as logical: T F
c_sizeof(1_c_int)=4 c_sizeof(1.0_c_double)=8 c_sizeof(1.0_c_long_double)=16
codes null alert backspace formfeed newline cr htab vtab: 0 7 8 12 10 13 9 11
c_associated(c_null_ptr)=F  c_associated(c_null_funptr)=F

On this platform c_long_double is 16 because AArch64 Linux has a 128-bit long double that matches Fortran’s quad precision; on another platform long double can be a different format and the constant, or its sign, will differ, which is the reason to print or test it instead of assuming. c_bool is a one-byte logical whose true value has the C value 1 (18.2.2): a logical(c_bool) argument can be passed to a C function that takes _Bool without conversion, and a default logical cannot (see What the compiler checks). The c_null_char constant and its companions are the characters of Table 18.1, here printed as their character codes; c_new_line equals new_line('a').

The exact-width constants are the ones to use when a binary file or a protocol fixes the size. The constants c_float128 and c_float16 are extensions, not standard: Flang documents c_float128 and c_float16 in its extensions list, and GNU Fortran 16.2 accepts c_float128 without -std but rejects it under -std=f2023.

Calling C from Fortran

A C function is described to Fortran by an interface body with bind(c). The body has the same shape as for any external procedure (see Interfaces, Generics and Operator Overloading), with three differences: the dummy arguments have interoperable types; a scalar argument that C receives by value has the value attribute; and a scalar that C receives as a pointer does not. With the value attribute the dummy argument is passed as a copy, as in C, and the actual argument may be an expression; without it, the compiler passes the address. The intent of a value dummy argument may be only in or omitted (C868). The import statement brings the kind names into the interface body, which does not see its host’s names automatically.

The C side of the first program below has three small functions, one with an unusual case in its name:

/* C side of the first calls from Fortran. */
int hf_add_value(int a, int b) { return a + b; }

void hf_add_ref(const int *a, const int *b, int *sum) { *sum = *a + *b; }

double HF_Mixed_Case(double x) { return 2.0 * x; }
program c_calls
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  implicit none

  interface
    ! Fortran name and C name differ: name= gives the binding label, whose case is significant.
    function add_by_value(a, b) bind(c, name='hf_add_value') result(s)
      import :: c_int
      integer(c_int), value, intent(in) :: a, b      ! passed like a C int
      integer(c_int) :: s
    end function add_by_value

    subroutine add_by_reference(a, b, s) bind(c, name='hf_add_ref')
      import :: c_int
      integer(c_int), intent(in) :: a, b             ! no value: C receives int *
      integer(c_int), intent(out) :: s
    end subroutine add_by_reference

    function twice(x) bind(c, name='HF_Mixed_Case') result(y)
      import :: c_double
      real(c_double), value, intent(in) :: x
      real(c_double) :: y
    end function twice

    ! A function of the C library, no C source of our own needed.
    function c_hypot(x, y) bind(c, name='hypot') result(h)
      import :: c_double
      real(c_double), value, intent(in) :: x, y
      real(c_double) :: h
    end function c_hypot
  end interface

  integer(c_int) :: total

  print '(a, i0)', 'by value:     3 + 4 = ', add_by_value(3_c_int, 4_c_int)
  call add_by_reference(3_c_int, 4_c_int, total)
  print '(a, i0)', 'by reference: 3 + 4 = ', total
  print '(a, f4.1)', 'HF_Mixed_Case(1.5) = ', twice(1.5_c_double)
  print '(a, f4.1)', 'hypot(3, 4)        = ', c_hypot(3.0_c_double, 4.0_c_double)
end program c_calls

Built with gcc -fPIC -c 03_ops.c and gfortran 03_calls.f90 03_ops.o (strict flags as above), the program printed the following; Flang 23 gave identical output:

by value:     3 + 4 = 7
by reference: 3 + 4 = 7
HF_Mixed_Case(1.5) =  3.0
hypot(3, 4)        =  5.0

Three things happen here. First, name= sets the binding label (18.10.2): the name by which the procedure is known to the C linker. Without name= the label is the Fortran name in lower case, so bind(c) on a procedure Add_Values would be called add_values in C. The case of a label given with name= is significant, which is why HF_Mixed_Case can be reached from a Fortran interface called twice. Second, hf_add_value has value arguments because the C prototype takes int, while hf_add_ref takes int * and is described without value. Third, hypot shows that the interface body does not care who wrote the function: any symbol the linker can find, including those of the C library, can be described this way, and the interface can use a Fortran name of its own. A C function that returns void is a subroutine; any other return type is a function with an interoperable scalar result.

A mistake in the value attribute does not produce a diagnostic. Writing the interface of hf_add_value without value (integer(c_int), intent(in) for both a and b) compiled warning-free with the strict flags, and the program printed 3 + 4 = 8394212 on three runs in a row: the compiler passed the addresses of the two integers where C expects the integers themselves, and C added two addresses, truncated to an int. The opposite mistake, value on the third argument of hf_add_ref, which C takes as int *, also compiled without a message; the call made C store through the address 0 and the program ended with Program received signal SIGSEGV: Segmentation fault - invalid memory reference. (the two interface bodies were the ones of the first program with value removed from a and b, and with value added to s). Check every interface body against the C prototype, argument by argument, as 18.3.7 lists.

What the compiler checks

The compiler does check that the dummy arguments and the result are interoperable types (C1562-C1567), and so it can reject an interface that no C prototype could match. This deliberately non-conforming module contains a dummy argument of default logical, one of a derived type that lacks bind(c) and one of default real:

! Deliberately non-conforming: arguments that cannot be interoperable.
module bad_binding
  use, intrinsic :: iso_c_binding, only: c_int
  implicit none

  type :: plain_t                        ! no bind(c): not interoperable
    integer(c_int) :: id
  end type plain_t

contains

  subroutine set_flag(flag) bind(c)
    logical, value, intent(in) :: flag   ! default logical has no C counterpart; c_bool is required
  end subroutine set_flag

  subroutine take_plain(p) bind(c)
    type(plain_t), intent(in) :: p       ! derived type without bind(c)
  end subroutine take_plain

  subroutine take_default_real(x) bind(c)
    real, value, intent(in) :: x         ! default real is c_float only by coincidence of the kind value
  end subroutine take_default_real
end module bad_binding

GNU Fortran 16.2 with -std=f2023 reports a warning for the kind it cannot prove to be a C type and errors for the others:

11_bad_interface.f90:12:23:

   12 |   subroutine set_flag(flag) bind(c)
      |                       1~~~
Warning: Variable 'flag' at (1) is a dummy argument of the BIND(C) procedure 'set_flag' but may not be C interoperable [-Wc-binding-type]
11_bad_interface.f90:20:32:

   20 |   subroutine take_default_real(x) bind(c)
      |                                1
Warning: Variable 'x' at (1) is a dummy argument of the BIND(C) procedure 'take_default_real' but may not be C interoperable [-Wc-binding-type]
11_bad_interface.f90:16:25:

   16 |   subroutine take_plain(p) bind(c)
      |                         1
Error: Variable 'p' at (1) is a dummy argument to the BIND(C) procedure 'take_plain' but is not C interoperable because derived type 'plain_t' is not C interoperable
11_bad_interface.f90:12:23:

   12 |   subroutine set_flag(flag) bind(c)
      |                       1~~~
Error: GNU Extension: LOGICAL dummy argument 'flag' at (1) with non-C_Bool kind in BIND(C) procedure 'set_flag'

LLVM Flang 23 compiled the same file without a message and exit code 0 (the line flang rc=0 was added by the test script), so the diagnostic depends on the compiler, and the module that passed one compiler’s checks is not proven interoperable. Treat the -Wc-binding-type warning of GNU Fortran as an error in a project that exposes a C API (-Werror=c-binding-type). The warning for default real is a fair reminder: the kind of default real happens to be that of c_float on most platforms, but only real(c_float) says so. The default logical is a hard error under -std=f2023, because a four-byte .true. is not the C _Bool.

Binding labels are global identifiers: two entities cannot have the same one, and a binding label must also differ from the global identifier of any other global entity, such as an external procedure without a label, ignoring differences in case (19.2). This module is deliberately wrong:

! Deliberately non-conforming: two procedures with the same binding label.
module dup_label
  implicit none
contains
  subroutine first() bind(c, name='hf_step')
  end subroutine first
  subroutine second() bind(c, name='hf_step')
  end subroutine second
end module dup_label
11_dup.f90:5:3:

    5 |   subroutine first() bind(c, name='hf_step')
      |   1~~~~~~~~~~~~~~~
    6 |   end subroutine first
    7 |   subroutine second() bind(c, name='hf_step')
      |   2~~~~~~~~~~~~~~~~
Error: Procedure 'first' with binding label 'hf_step' at (1) uses the same global identifier as entity at (2)

LLVM Flang 23 reported the same mistake, in its own format:

error: Semantic errors in 11_dup.f90
11_dup.f90:7:14: error: Two entities have the same global name 'hf_step'
    subroutine second() bind(c, name='hf_step')
               ^^^^^^
11_dup.f90:5:14: Conflicting declaration
    subroutine first() bind(c, name='hf_step')
               ^^^^^

Exposing heatflow to C

The heatflow project ships one C-callable function, heatflow_step: it advances an nx by ny temperature field by one FTCS step and returns a status. It lives in the module heatflow_capi, which wraps the module procedure ftcs_sweep of heatflow_kernels (the kernel that the solvers of Modules and Submodules and Object Oriented Programming also use):

!> C API: heatflow_step advances an nx-by-ny field by one FTCS step.
module heatflow_capi
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  use heatflow_kernels, only: ftcs_sweep
  implicit none
  private
  public :: heatflow_step

contains

  function heatflow_step(nx, ny, u, unew, alpha, dt, dx, dy, residual) &
      bind(c, name='heatflow_step') result(status)
    integer(c_int), value, intent(in) :: nx, ny
    real(c_double), intent(in) :: u(nx, ny)
    real(c_double), intent(inout) :: unew(nx, ny)
    real(c_double), value, intent(in) :: alpha, dt, dx, dy
    real(c_double), intent(out) :: residual
    integer(c_int) :: status
    if (nx < 3 .or. ny < 3) then
      residual = 0.0_c_double
      status = 1_c_int
      return
    end if
    call ftcs_sweep(u, unew, alpha, dt, dx, dy, residual)
    status = 0_c_int
  end function heatflow_step
end module heatflow_capi

Every dummy argument is either value (the sizes and the scalar coefficients) or passed by address (the two fields and the residual). The field u(nx, ny) is an explicit-shape array whose bounds are the other value dummy arguments: the bounds of an explicit-shape array may be specified from earlier dummy arguments, and an explicit-shape array is exactly what a C double * pointing at nx * ny contiguous values corresponds to (18.3.6). The bind(c, name='heatflow_step') clause is redundant, since the default label is the lower-case Fortran name, but writing the label makes the exported symbol grep-able. Inside, the code calls the ordinary Fortran kernel, whose assumed-shape arguments receive a normal Fortran descriptor built by the compiler, so there is nothing C-specific below the interface.

The header is what C and C++ callers include. It is hand-written and has to be kept in step with the Fortran interface by the author, because no tool checks one against the other. extern "C" is needed only for C++:

#ifndef HEATFLOW_H
#define HEATFLOW_H
#ifdef __cplusplus
extern "C" {
#endif
/* Advance the nx-by-ny column-major field u by one FTCS step into unew.
   Returns 0 on success, 1 if the grid is smaller than 3x3. */
int heatflow_step(int nx, int ny, const double *u, double *unew,
                  double alpha, double dt, double dx, double dy,
                  double *residual);
#ifdef __cplusplus
}
#endif
#endif

The C client sets one hot cell, calls the function once and prints what came back:

#include <stdio.h>
#include <stdlib.h>
#include "heatflow.h"

int main(void) {
    enum { NX = 21, NY = 21 };
    double u[NX * NY] = {0}, unew[NX * NY] = {0}, residual = 0.0;
    u[(NX / 2) + NX * (NY / 2)] = 100.0;
    unew[(NX / 2) + NX * (NY / 2)] = 100.0;
    int status = heatflow_step(NX, NY, u, unew, 1.0, 5.0e-5, 0.05, 0.05, &residual);
    printf("status=%d residual=%.4f center=%.4f\n", status, residual, unew[(NX / 2) + NX * (NY / 2)]);
    return status;
}

The Fortran sources are compiled with the strict flags, put in an archive, and the program is linked with gcc. The Fortran runtime library is not part of the C link line, so -lgfortran is added. The need is real even though heatflow_step does no I/O: the strict flags include -fcheck=all, which makes the compiler call the runtime to report errors, and a link without -lgfortran fails with an undefined reference to _gfortran_runtime_error_at, reported once for every check:

/usr/bin/ld: ./libheatflow_capi.a(heatflow_capi.o): in function `heatflow_step':
/work/02_capi/heatflow_capi.f90:11:(.text+0x50): undefined reference to `_gfortran_runtime_error_at'

Built without -fcheck=all the same archive linked without the runtime library, but a library should not depend on that. The build with the runtime library is:

gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c heatflow_kinds.f90 heatflow_kernels.f90 heatflow_capi.f90
ar rcs libheatflow_capi.a heatflow_kinds.o heatflow_kernels.o heatflow_capi.o
gcc -std=c17 -Wall -Wextra -pedantic -c c_client.c -o c_client.o
gcc c_client.o -L. -lheatflow_capi -lgfortran -lm -o c_client
./c_client
status=0 residual=8.0000 center=92.0000

The residual 8.0 is the largest change of any point in one step (the hot cell, 100, relaxing to 92), and the same call returned the same two numbers from the Python wrapper of Calling Fortran from C, C++ and Python. The same sources built with Flang (flang-23 -c ..., ar, and flang-23 c_client.o -L. -lhf -o c_client_flang, which lets the Flang driver add its runtime) printed the identical line. A C object that refers to a symbol of the C library, such as cbrt in the callback program below, could not be linked by the Flang driver unless it was compiled with -fPIC; without the option the linker stopped with the message below, because Flang links position-independent executables by default on this platform:

/usr/bin/ld: 07_proc.o: relocation R_AARCH64_ADR_PREL_PG_HI21 against symbol `cbrt@@GLIBC_2.17' which may bind externally can not be used when making a shared object; recompile with -fPIC

The C files of those examples were therefore compiled with -fPIC. The same library built as a shared library (gfortran -shared) was linked by gcc and by g++, and both programs ran. The header was also compiled as C++ (g++ -std=c++20 -Wall -Wextra -pedantic -c) without a diagnostic, which checks that the extern "C" guard works.

Name mangling, and why bind(c) replaced the underscore convention

A Fortran compiler does not export a procedure under the name in the source. What it does with the name is the mangling scheme of the compiler and is not defined by the standard. nm on the archive shows both what bind(c) gives and what an ordinary module procedure gets (the address column is removed):

GNU Fortran 16.2:
T __heatflow_kernels_MOD_ftcs_sweep
T __heatflow_kernels_MOD_jacobi_sweep
T heatflow_step

LLVM Flang 23:
T _QMheatflow_kernelsPftcs_sweep
T _QMheatflow_kernelsPjacobi_sweep
T heatflow_step

The two compilers disagree about every name except the one with bind(c): the module procedures are encoded with the module name, in different formats. That is why a C file cannot reasonably call a module procedure without bind(c), and why a library mixing compilers must expose only bind(c) names.

Before Fortran 2003 the only way to call Fortran from C was to guess the symbol. The common convention of Unix compilers was the lower-case name followed by one trailing underscore, with every argument passed by reference and, for each character argument, a hidden length argument appended at the end of the argument list. Code written for it still exists in large numerical libraries (BLAS and LAPACK among them, see BLAS, LAPACK and Numerical Libraries), and is shown here only to explain what bind(c) removed. The external subroutine below has no bind(c):

! Not interoperable: no bind(c). The linker name is chosen by the compiler.
subroutine scale_field(n, u, factor)
  implicit none
  integer, intent(in) :: n
  double precision, intent(inout) :: u(n)
  double precision, intent(in) :: factor
  u = u*factor
end subroutine scale_field
#include <stdio.h>

/* Old convention: lower-case name plus one trailing underscore, every argument by pointer. */
void scale_field_(const int *n, double *u, const double *factor);

int main(void) {
  double u[3] = {1.0, 2.0, 3.0};
  int n = 3;
  double factor = 2.5;
  scale_field_(&n, u, &factor);
  printf("%.1f %.1f %.1f\n", u[0], u[1], u[2]);
  return 0;
}

Both compilers exported the symbol scale_field_ (checked with nm), the C program above linked against it and printed 2.5 5.0 7.5 with GNU Fortran and with Flang. The option names that change the convention exist, with these effects on the same object file:

gfortran
0000000000000000 T scale_field_
gfortran -fno-underscoring
0000000000000000 T scale_field
gfortran -fsecond-underscore
0000000000000000 T scale_field__
flang -fno-underscoring
0000000000000000 T scale_field

The first gfortran line, with no option, is the default scale_field_. -fno-underscoring removes the underscore, -fsecond-underscore appends a second one to a name that already contains an underscore (an option for compatibility with older compilers; the GNU Fortran manual documents it with the code generation options), and Flang has -funderscoring and -fno-underscoring, which gave the same result as GNU Fortran in the last line; whether a given library was built with one of them is a property of that build, which is why CMake ships a FortranCInterface module (see Building with Make, CMake and Meson) to detect the convention. A caller of old code must also know that an argument by reference is the default, and that a character argument adds a hidden length argument which is passed by the compiler, not described by the name; a mistake in either corrupts the stack or the data silently. New code uses bind(c): the symbol is the binding label on every compiler and platform, the argument passing is stated in the interface (value or by address), and a string is a plain char *.

Arrays: column-major, explicit-shape and assumed-size

A Fortran array stores its first index fastest; a C array stores its last index fastest. An interoperable Fortran array argument is an explicit-shape or assumed-size array (18.3.6), and its dimension list reads in the opposite order to the C declaration: Fortran a(18, 3:7, *) interoperates with C int b[][5][18] (18.3.6, Note 3). The last (slowest) Fortran dimension corresponds to the first (outermost) C dimension, which is the one that may be * in Fortran and empty in C. A pointer to the first element serves for the whole array, and the shape is communicated by other arguments, which is the contract of heatflow_step. The next program makes the mapping visible: C fills a[i][j] with 10*i + j for 2 rows and 3 columns, and the Fortran declaration is a(3, 2).

/* A C array of 2 rows by 3 columns: a[i][j] with j varying fastest in memory. */
void hf_fill_rows(double a[2][3]) {
  for (int i = 0; i < 2; ++i)
    for (int j = 0; j < 3; ++j) a[i][j] = 10.0 * i + j;
}
program c_row_major
  use, intrinsic :: iso_c_binding, only: c_double
  implicit none
  interface
    subroutine hf_fill_rows(a) bind(c)           ! void hf_fill_rows(double a[2][3])
      import :: c_double
      real(c_double), intent(out) :: a(3, 2)     ! the dimensions are reversed: C a[i][j] is Fortran a(j+1, i+1)
    end subroutine hf_fill_rows
  end interface
  real(c_double) :: a(3, 2)
  integer :: j

  call hf_fill_rows(a)
  do j = 1, 3
    print '(a, i0, a, 2f6.1)', 'a(', j, ', 1:2) = ', a(j, :)
  end do
  print '(a, f4.1)', 'C a[1][2] is Fortran a(3, 2) = ', a(3, 2)
end program c_row_major
a(1, 1:2) =    0.0  10.0
a(2, 1:2) =    1.0  11.0
a(3, 1:2) =    2.0  12.0
C a[1][2] is Fortran a(3, 2) = 12.0

C element a[1][2] is Fortran a(3, 2): the C row index becomes the last Fortran index, and the C column index the first. There is nothing to transpose in memory; both languages are looking at the same bytes, and a loop that wants unit stride runs over the first Fortran index and the last C index. For a two-dimensional field such as heatflow's u(nx, ny) the C code reads u[j * nx + i] (or declares double u[ny][nx]), which is what c_client.c does with u[(NX / 2) + NX * (NY / 2)]. The layout rules for Fortran arrays are in Arrays Fundamentals.

Four more facts about arrays at the boundary:

  • An array section that is not contiguous, such as u(1:10:2), is passed to an explicit-shape or assumed-size dummy argument through a contiguous temporary that the compiler creates and copies back after the call for intent(out) and intent(inout). C then sees contiguous memory, and a C routine that keeps the pointer after the call keeps a pointer to a dead temporary.

  • An allocatable array or a pointer array is never an interoperable variable (18.3.6, Note 2). Pass the first element (u(1, 1)), the section, or c_loc(u), or declare the dummy argument allocatable, assumed-shape or assumed-rank and let the C side deal with a descriptor, as in C Descriptors and ISO_Fortran_binding.

  • An assumed-size dummy x(*) has no bounds; C cannot ask Fortran for the size, so a count travels in another argument. Bounds checking (-fcheck=bounds) cannot catch an overrun made inside the C function.

  • c_loc does not accept a zero-sized array (18.2.3.7), and a C function must not dereference the address that a zero-sized actual argument produces.

Derived types and enumerations

A Fortran derived type is interoperable with a C struct if it has the bind(c) attribute, has the same number of components, and the components in the same positions interoperate (18.3.4). The names of the type and of the components do not matter. Constraints C1801-C1806 forbid sequence, type parameters, extends, type-bound procedures, an empty component list, and any pointer or allocatable component. A bit field, a flexible array member and a union have no Fortran counterpart. A C void * member becomes type(c_ptr), which is how an allocatable, a pointer or a procedure reaches a structure. The details of the layout rules, including the padding that both languages insert, are in Derived Types; the next program uses a structure with an array member, a character array member and a pointer member, filled by C and read in Fortran, together with a C enumeration:

#include <stddef.h>

typedef struct {
  int id;
  double position[2];
  char label[8];
  void *user_data;
} hf_probe;

enum hf_bc { HF_DIRICHLET = 1, HF_NEUMANN = 2, HF_PERIODIC = 3 };

/* Fill the probe array: ids 1..n, positions along the x axis, labels "P1".. */
void hf_make_probes(hf_probe *p, size_t n) {
  for (size_t i = 0; i < n; ++i) {
    p[i].id = (int)i + 1;
    p[i].position[0] = 0.25 * (double)i;
    p[i].position[1] = 0.5;
    p[i].label[0] = 'P';
    p[i].label[1] = (char)('1' + i);
    p[i].label[2] = '\0';
    p[i].user_data = NULL;
  }
}

/* Name of a boundary condition as an enum value. */
const char *hf_bc_name(enum hf_bc bc) {
  switch (bc) {
    case HF_DIRICHLET: return "dirichlet";
    case HF_NEUMANN: return "neumann";
    case HF_PERIODIC: return "periodic";
  }
  return "unknown";
}

size_t hf_probe_size(void) { return sizeof(hf_probe); }
program c_types
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_char, c_ptr, c_size_t, c_null_char, &
    c_sizeof, c_associated, c_f_pointer
  implicit none

  type, bind(c) :: hf_probe                       ! typedef struct { ... } hf_probe;
    integer(c_int) :: id
    real(c_double) :: position(2)
    character(kind=c_char) :: label(8)            ! char label[8]
    type(c_ptr) :: user_data                      ! void *user_data
  end type hf_probe

  enum, bind(c)                                   ! enum hf_bc { HF_DIRICHLET = 1, ... }
    enumerator :: hf_dirichlet = 1, hf_neumann = 2, hf_periodic = 3
  end enum

  interface
    subroutine hf_make_probes(p, n) bind(c)
      import :: hf_probe, c_size_t
      type(hf_probe), intent(out) :: p(*)         ! hf_probe *p
      integer(c_size_t), value, intent(in) :: n
    end subroutine hf_make_probes

    function hf_bc_name(bc) bind(c) result(name)
      import :: c_int, c_ptr
      integer(c_int), value, intent(in) :: bc     ! the enum is passed as an int
      type(c_ptr) :: name                         ! const char *
    end function hf_bc_name

    function hf_probe_size() bind(c) result(n)
      import :: c_size_t
      integer(c_size_t) :: n
    end function hf_probe_size
  end interface

  type(hf_probe) :: probes(3)
  character(kind=c_char), pointer :: text(:)
  type(c_ptr) :: p
  integer :: i, k

  call hf_make_probes(probes, 3_c_size_t)
  print '(a, i0, a, i0)', 'sizeof in C = ', hf_probe_size(), ', c_sizeof in Fortran = ', c_sizeof(probes(1))
  do i = 1, size(probes)
    k = findloc(probes(i)%label, c_null_char, 1) - 1   ! characters before the terminator
    print '(a, i0, a, 2f5.2, 3a, l1)', 'probe ', probes(i)%id, ' at', probes(i)%position, &
      ' label=', transfer(probes(i)%label(1:k), repeat(' ', k)), ' user_data associated=', c_associated(probes(i)%user_data)
  end do

  print '(a, 3i2)', 'enumerators: ', hf_dirichlet, hf_neumann, hf_periodic
  do i = hf_dirichlet, hf_periodic                   ! enumerators are integer(c_int) constants
    p = hf_bc_name(i)
    call c_f_pointer(p, text, [16])                 ! view 16 characters; the string ends at the first null
    k = findloc(text, c_null_char, 1) - 1
    print '(a, i0, 2a)', 'bc ', i, ' is ', transfer(text(1:k), repeat(' ', k))
  end do
end program c_types
sizeof in C = 40, c_sizeof in Fortran = 40
probe 1 at 0.00 0.50 label=P1 user_data associated=F
probe 2 at 0.25 0.50 label=P2 user_data associated=F
probe 3 at 0.50 0.50 label=P3 user_data associated=F
enumerators:  1 2 3
bc 1 is dirichlet
bc 2 is neumann
bc 3 is periodic

Flang 23 gave identical output. c_sizeof(probes(1)) matches sizeof(hf_probe), 40 bytes: 4 (id), 4 padding, 16 (position), 8 (label) and 8 (user_data). If the two disagree, a component has a different kind or the compilers pad differently, and data is corrupted silently; printing both sizes in a unit test is an inexpensive guard. The array of hf_probe is passed as an assumed-size array, type(hf_probe), intent(out) with the dimension (*), the Fortran way of writing hf_probe *p.

An enumeration made interoperable with enum, bind(c) (7.6.1) has the kind of the C integer type chosen for the corresponding C enum and enumerators that are named constants of that kind; here the three enumerators are integer(c_int) and pass to a value dummy argument of kind c_int. 18.3.3 says that an enum type interoperates with its C enumerated type and with the C integer type that interoperates with its enumerators. The heatflow module heatflow_boundary declares its boundary conditions this way (dirichlet = 1, neumann = 2, periodic = 3), which is why those values can be handed to C code without a table. The Fortran 2023 enumeration type is a different feature and is not interoperable; see Enumerations.

A string member such as label[8] is character(kind=c_char) with dimension(8), an array of one-character elements, not a character(len=8) scalar. The terminating null character is part of the array, and the Fortran code above searches for it with findloc(probes(i)%label, c_null_char, 1).

Global variables

A C variable with external linkage can be a module variable in Fortran, and the other way round (18.9). The Fortran variable needs bind(c), which can be given with name= as in the example, and must be declared in the specification part of a module (C819) or be a common block. At most one of the two sides defines the variable: a C file defines int hf_verbosity = 2; and the Fortran module only refers to it, whereas hf_steps_done is defined (and so allocated) by the Fortran module, and declared extern int hf_steps_done; in C. A variable cannot be initialised by both sides (18.9.1).

int hf_verbosity = 2;       /* defined in C, used from Fortran */
double hf_scale = 0.5;      /* defined in C, used from Fortran under another name */
extern int hf_steps_done;   /* defined in Fortran, used from C */

int hf_report(void) { return hf_steps_done * hf_verbosity; }
module heatflow_globals
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  implicit none
  private
  public :: verbosity, scale, steps_done

  integer(c_int), bind(c, name='hf_verbosity') :: verbosity   ! the C variable hf_verbosity
  real(c_double), bind(c, name='hf_scale') :: scale           ! the C variable hf_scale
  integer(c_int), bind(c, name='hf_steps_done') :: steps_done ! defined here; C refers to it
end module heatflow_globals

program c_globals
  use, intrinsic :: iso_c_binding, only: c_int
  use heatflow_globals, only: verbosity, scale, steps_done
  implicit none
  interface
    function hf_report() bind(c) result(r)
      import :: c_int
      integer(c_int) :: r
    end function hf_report
  end interface

  print '(a, i0, a, f4.2)', 'values set in C: verbosity=', verbosity, ' scale=', scale
  steps_done = 21
  verbosity = 3
  print '(a, i0)', 'C function computes steps_done*verbosity = ', hf_report()
end program c_globals
values set in C: verbosity=2 scale=0.50
C function computes steps_done*verbosity = 63

Flang 23 printed the same two lines. The binding label of a variable follows the same rule as the one of a procedure (18.9.2): case-sensitive when given with name=, the lower-case Fortran name otherwise. Module variables with bind(c) are shared state for the whole program, with all the drawbacks of global state; use them for flags and constants the C library owns (a verbosity level, an error code), and pass anything else as arguments.

C pointers and function pointers

type(c_ptr) and type(c_funptr) are derived types with private components that are interoperable with any C object pointer and any C function pointer (18.3.2). They are opaque in Fortran: there is no arithmetic, no dereference and no comparison with ==. They get their values from C, from c_loc and c_funloc, or from the constants c_null_ptr and c_null_funptr, and are turned back into Fortran pointers with c_f_pointer and c_f_procpointer. A type(c_ptr) with the value attribute is the Fortran spelling of void *.

Procedure What it does Notes

c_loc(x)

the C address of x, a c_ptr

x needs the pointer or target attribute, must be allocated or associated, and if it is an array it must be contiguous and not zero-sized; since Fortran 2018 x need not have an interoperable type (then the result is an opaque handle)

c_funloc(proc)

the C address of a procedure, a c_funptr

a C function may call the result only if the procedure is interoperable (18.10.1)

c_f_pointer(cptr, fptr [, shape, lower])

associates the Fortran pointer fptr with the C address; shape is required if fptr is an array

lower (Fortran 2023) sets the lower bounds; see the example below

c_f_procpointer(cptr, fptr)

associates a Fortran procedure pointer with a C function pointer

the interface of fptr must be interoperable with the prototype

c_associated(p [, q])

true if p is not a null pointer; with two arguments, true if p is not null and compares equal to q

works on c_ptr and c_funptr

c_sizeof(x)

the size of x in bytes, as integer(c_size_t)

the same as C sizeof of the interoperating type, times the number of elements for an array; x is not an assumed-size array

All the procedures of the module are generic. c_f_pointer, c_f_procpointer and c_f_strpointer are impure subroutines; the others (c_associated, c_funloc, c_loc, c_sizeof and f_c_string) are simple (18.2.3.1), which is what allows them in pure procedures and do concurrent.

The next program makes the C library own an array: hf_alloc_field returns a malloced array of doubles, Fortran views it as a rank-one array pointer (with default and with zero-based bounds, using lower), a Fortran allocatable array is zeroed by a C routine that takes only an address and a byte count, and the memory is returned to its owner at the end:

#include <stdlib.h>

/* The C library owns this memory: n doubles, initialised to 20.0. */
double *hf_alloc_field(size_t n) {
  double *p = malloc(n * sizeof *p);
  if (p != NULL) {
    for (size_t i = 0; i < n; ++i) p[i] = 20.0;
  }
  return p;
}

void hf_free_field(void *p) { free(p); }

/* Generic buffer API: only an address and a byte count. */
void hf_zero_bytes(void *buf, size_t nbytes) {
  unsigned char *b = buf;
  for (size_t i = 0; i < nbytes; ++i) b[i] = 0;
}
program c_pointers
  use, intrinsic :: iso_c_binding, only: c_double, c_ptr, c_size_t, c_loc, c_f_pointer, c_associated, c_sizeof
  implicit none

  interface
    function hf_alloc_field(n) bind(c) result(p)
      import :: c_size_t, c_ptr
      integer(c_size_t), value, intent(in) :: n
      type(c_ptr) :: p                         ! double *
    end function hf_alloc_field

    subroutine hf_free_field(p) bind(c)
      import :: c_ptr
      type(c_ptr), value, intent(in) :: p      ! void *
    end subroutine hf_free_field

    subroutine hf_zero_bytes(buf, nbytes) bind(c)
      import :: c_ptr, c_size_t
      type(c_ptr), value, intent(in) :: buf    ! void *
      integer(c_size_t), value, intent(in) :: nbytes
    end subroutine hf_zero_bytes
  end interface

  type(c_ptr) :: raw
  real(c_double), pointer :: field(:), shifted(:)
  real(c_double), allocatable, target :: local(:)

  raw = hf_alloc_field(5_c_size_t)
  if (.not. c_associated(raw)) error stop 'allocation failed in C'

  call c_f_pointer(raw, field, [5])             ! indices 1:5
  call c_f_pointer(raw, shifted, [5], [0])      ! Fortran 2023 lower=: indices 0:4 over the same memory
  field(2) = 99.0_c_double
  print '(a, 5f6.1)', 'field   (1:5) = ', field
  print '(a, i0, a, i0, a, f5.1)', 'shifted bounds ', lbound(shifted, 1), ':', ubound(shifted, 1), ', shifted(1) = ', shifted(1)

  allocate (local(4), source=7.0_c_double)      ! c_loc accepts an allocated allocatable (Fortran 2008)
  call hf_zero_bytes(c_loc(local), c_sizeof(local))
  print '(a, 4f4.1)', 'zeroed by C   = ', local
  print '(a, l1)', 'c_associated(c_loc(local(1)), c_loc(local)) = ', c_associated(c_loc(local(1)), c_loc(local))

  nullify (field, shifted)                      ! drop the Fortran views before the owner frees the memory
  call hf_free_field(raw)
end program c_pointers
field   (1:5) =   20.0  99.0  20.0  20.0  20.0
shifted bounds 0:4, shifted(1) =  99.0
zeroed by C   =  0.0 0.0 0.0 0.0
c_associated(c_loc(local(1)), c_loc(local)) = T

Flang 23 printed the same four lines. The call c_f_pointer(raw, shifted, [5], [0]) is the Fortran 2023 form; GNU Fortran 16.2 and Flang 23 both accepted it in this strict build. Before it, a pointer was always indexed from 1 and the usual workaround was a pointer remapping, shifted(0:4) => field, which does the same in any edition and is still valid. The allocatable local is passed through c_loc(local): a c_loc of an allocated allocatable variable is allowed (the note to 18.3.2 says it can be used where the allocatable could not be passed directly), and its argument needs the target attribute.

The rules a program must follow with these pointers are not enforced by the compiler:

  • Lifetime. A C pointer to part of a Fortran object becomes undefined under the same conditions as a Fortran pointer to it would (18.8). If the target is a local variable, an automatic array or an allocatable that is deallocated, the address is dangling after return or deallocation; and if a Fortran dummy argument has neither target nor asynchronous, every C pointer into the object it was associated with becomes undefined when the call returns. The target attribute is also what stops the compiler from assuming that nothing else can alias the variable.

  • Ownership. Whoever allocated frees: memory from C’s malloc is released with free, never with Fortran’s deallocate, and memory from Fortran’s allocate never with free. The program above nullifies the Fortran pointers before the owner frees, since fptr has no way to know.

  • Alignment and type. c_f_pointer trusts the address; the type of fptr has to match the data, and the standard (18.2.3.3) states that the target must be the C address of an interoperable entity, a c_loc of a noninteroperable one, or storage not in use by any other Fortran entity.

  • Allocatable and pointer arrays. They are not interoperable variables; to hand one to a C function that wants a plain pointer, pass c_loc(a) (or a(1)) of an allocated, contiguous, non-empty array, as the program does with local.

Callbacks: c_funloc and c_f_procpointer

Function pointers work in both directions. The first program gives qsort from the C library a Fortran comparison function: the callback is an ordinary bind(c) Fortran function, its two arguments are void * (type(c_ptr), value) and are turned into Fortran pointers with c_f_pointer, and c_funloc gives qsort its address. c_sizeof supplies the element size.

module heatflow_sort
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_ptr, c_funptr, c_size_t, c_f_pointer
  implicit none
  private
  public :: c_qsort, compare_ascending

  interface
    ! void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
    subroutine c_qsort(base, nmemb, size, compar) bind(c, name='qsort')
      import :: c_ptr, c_size_t, c_funptr
      type(c_ptr), value, intent(in) :: base
      integer(c_size_t), value, intent(in) :: nmemb, size
      type(c_funptr), value, intent(in) :: compar
    end subroutine c_qsort
  end interface

contains

  ! The callback must itself be interoperable: bind(c), value or pointer arguments, C kinds.
  function compare_ascending(a, b) bind(c) result(order)
    type(c_ptr), value, intent(in) :: a, b
    integer(c_int) :: order
    real(c_double), pointer :: x, y
    call c_f_pointer(a, x)
    call c_f_pointer(b, y)
    order = merge(-1_c_int, merge(1_c_int, 0_c_int, x > y), x < y)
  end function compare_ascending
end module heatflow_sort

program c_qsort_demo
  use, intrinsic :: iso_c_binding, only: c_double, c_size_t, c_loc, c_funloc, c_sizeof
  use heatflow_sort, only: c_qsort, compare_ascending
  implicit none
  real(c_double), target :: temperature(6) = [30.5_c_double, 12.0_c_double, 99.0_c_double, 0.5_c_double, 42.0_c_double, 7.25_c_double]

  call c_qsort(c_loc(temperature), size(temperature, kind=c_size_t), c_sizeof(temperature(1)), c_funloc(compare_ascending))
  print '(a, 6f7.2)', 'sorted by libc qsort: ', temperature
end program c_qsort_demo
sorted by libc qsort:    0.50   7.25  12.00  30.50  42.00  99.00

Flang 23 printed the same line. The second program goes the other way: a C function returns a function pointer chosen at run time, c_f_procpointer turns it into a Fortran procedure pointer whose interface is the abstract interface unary_fn (an abstract interface with bind(c), the analogue of a typedef for the C pointer type), and the same raw c_funptr is also given back to C to be called on every element of an array:

#include <math.h>

typedef double (*hf_fn)(double);

static double square(double x) { return x * x; }

/* Returns a C function pointer chosen at run time. */
hf_fn hf_pick(int which) { return which == 0 ? square : cbrt; }

/* Calls back a function pointer on every element. */
void hf_apply(double *x, int n, hf_fn f) {
  for (int i = 0; i < n; ++i) x[i] = f(x[i]);
}
program c_procedure_pointers
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_funptr, c_funloc, c_f_procpointer, c_associated
  implicit none

  abstract interface
    function unary_fn(x) bind(c) result(y)       ! double (*)(double)
      import :: c_double
      real(c_double), value, intent(in) :: x
      real(c_double) :: y
    end function unary_fn
  end interface

  interface
    function hf_pick(which) bind(c) result(f)
      import :: c_int, c_funptr
      integer(c_int), value, intent(in) :: which
      type(c_funptr) :: f
    end function hf_pick

    subroutine hf_apply(x, n, f) bind(c)
      import :: c_double, c_int, c_funptr
      real(c_double), intent(inout) :: x(*)
      integer(c_int), value, intent(in) :: n
      type(c_funptr), value, intent(in) :: f
    end subroutine hf_apply
  end interface

  procedure(unary_fn), pointer :: fn
  real(c_double) :: values(3) = [1.0_c_double, 8.0_c_double, 27.0_c_double]
  type(c_funptr) :: raw

  raw = hf_pick(1_c_int)
  if (.not. c_associated(raw)) error stop 'no function'
  call c_f_procpointer(raw, fn)                  ! a Fortran procedure pointer to the C function cbrt
  print '(a, f4.1)', 'cbrt(27) through a procedure pointer = ', fn(27.0_c_double)

  call hf_apply(values, size(values, kind=c_int), raw)    ! C calls cbrt on each element
  print '(a, 3f4.1)', 'hf_apply with cbrt   = ', values

  call hf_apply(values, size(values, kind=c_int), hf_pick(0_c_int))
  print '(a, 3f4.1)', 'hf_apply with square = ', values
end program c_procedure_pointers
cbrt(27) through a procedure pointer =  3.0
hf_apply with cbrt   =  1.0 2.0 3.0
hf_apply with square =  1.0 4.0 9.0

Again the output of Flang 23 was identical. A callback has to be interoperable (bind(c), interoperable dummy arguments); c_funloc can also be applied to a procedure that is not interoperable, but then a C function must not call the address (18.10.1), the result being usable only with c_f_procpointer on the Fortran side, and the standard recommends that such a pointer be declared void (*)() in C. A procedure defined by Fortran must not be invoked as a signal handler and must not invoke setjmp or longjmp; and if a procedure defined by other means calls setjmp or longjmp, it must not cause any Fortran procedure to be invoked (18.10.1).

Strings across the boundary

A C string is a char array ended by a null character, and a Fortran string is a character value with a length and no terminator. The two meet in three situations: passing a Fortran string to C, filling a Fortran buffer from C, and viewing a C string that C owns. Each has an idiom, and the Fortran 2023 procedures remove the boilerplate of the first and the last.

  • Fortran to C. trim(s) // c_null_char builds a C string by hand. Fortran 2023 adds f_c_string(s [, asis]) (18.2.3.9): the result has the characters of s (without trailing blanks unless asis is true) followed by c_null_char. Either value can be passed straight to a dummy argument character(kind=c_char) with dimension(*), because a character scalar actual argument may be associated with an array dummy argument by sequence association (15.5.2.12, and the note at the end of 18.3.7).

  • C fills a Fortran buffer. The buffer is an array of character(kind=c_char), and the code converts it to a character variable of the right length by looping over the characters up to the null, or by findloc as before.

  • C owns the string. The function returns a c_ptr. Find the length with strlen (a one-line interface to the C library), then c_f_pointer(p, chars, [n]) gives an array of n characters, or Fortran 2023 c_f_strpointer gives a deferred-length character(len=:), pointer that can be used as a string.

#include <stdio.h>
#include <string.h>

/* Writes a C string into the caller's buffer; returns the length without the null. */
size_t hf_version(char *out, size_t capacity) {
  int n = snprintf(out, capacity, "heatflow %d.%d", 0, 1);
  return n < 0 ? 0 : (size_t)n;
}

/* Counts the characters of the file extension. */
size_t hf_extension_length(const char *name) {
  const char *dot = strrchr(name, '.');
  return dot == NULL ? 0 : strlen(dot + 1);
}
program c_strings
  use, intrinsic :: iso_c_binding, only: c_char, c_null_char, c_ptr, c_size_t, c_f_pointer, c_associated, f_c_string
  implicit none

  interface
    function hf_version(out, capacity) bind(c) result(n)
      import :: c_char, c_size_t
      character(kind=c_char), intent(out) :: out(*)   ! char *: an array of c_char, not a character(len=*)
      integer(c_size_t), value, intent(in) :: capacity
      integer(c_size_t) :: n
    end function hf_version

    function hf_extension_length(name) bind(c) result(n)
      import :: c_char, c_size_t
      character(kind=c_char), intent(in) :: name(*)   ! const char *
      integer(c_size_t) :: n
    end function hf_extension_length

    function c_getenv(name) bind(c, name='getenv') result(p)
      import :: c_char, c_ptr
      character(kind=c_char), intent(in) :: name(*)
      type(c_ptr) :: p
    end function c_getenv

    function c_strlen(s) bind(c, name='strlen') result(n)
      import :: c_ptr, c_size_t
      type(c_ptr), value, intent(in) :: s
      integer(c_size_t) :: n
    end function c_strlen
  end interface

  character(kind=c_char) :: buffer(32)
  character(len=:), allocatable :: text
  type(c_ptr) :: p
  character(kind=c_char), pointer :: chars(:)
  integer(c_size_t) :: n

  ! C writes into a Fortran buffer; the Fortran side converts n characters to a string
  n = hf_version(buffer, size(buffer, kind=c_size_t))
  text = to_string(buffer(1:n))
  print '(3a, i0)', 'hf_version returned "', text, '", length ', n

  ! A Fortran string needs its terminator; a scalar actual argument may correspond to an array dummy
  print '(a, i0)', 'extension length (trim // c_null_char): ', hf_extension_length(trim('heatflow.csv  ') // c_null_char)
  print '(a, i0)', 'extension length (f_c_string):          ', hf_extension_length(f_c_string('heatflow.nc  '))

  ! A string owned by C comes back as a c_ptr; strlen tells how many characters to view
  p = c_getenv(f_c_string('HEATFLOW_OUT'))
  if (c_associated(p)) then
    call c_f_pointer(p, chars, [c_strlen(p)])
    print '(3a)', 'HEATFLOW_OUT = ', to_string(chars), '.'
  else
    print '(a)', 'HEATFLOW_OUT is not set'
  end if

contains

  ! An array of single characters is not a string: print it with 'a' and every element lands on its own line.
  pure function to_string(chars) result(text)
    character(kind=c_char), intent(in) :: chars(:)
    character(len=size(chars)) :: text
    integer :: i
    do i = 1, size(chars)
      text(i:i) = chars(i)
    end do
  end function to_string
end program c_strings

The program was run with HEATFLOW_OUT=results/heat.csv in the environment (the name is invented), once with GNU Fortran 16.2 and once with Flang 23, and printed the same lines:

hf_version returned "heatflow 0.1", length 12
extension length (trim // c_null_char): 3
extension length (f_c_string):          2
HEATFLOW_OUT = results/heat.csv.

With the variable unset, the branch of the if that reports the null pointer was taken and printed HEATFLOW_OUT is not set (getenv returns a null pointer, which c_associated detects). Two details of the program are lessons: the extension length is 3 for 'heatflow.csv ' with trim, and 2 for 'heatflow.nc ' with f_c_string, that is, f_c_string really did remove the trailing blanks; and a function to_string was needed because print '(3a)' of an array of single characters writes one element per format repetition, putting every character on its own line. That is an easy mistake to make with c_char arrays, and transfer(chars, repeat(' ', size(chars))) is the one-line alternative. f_c_string is available in both compilers, so the program above, which uses it for the name argument of getenv, ran unchanged with both.

Fortran 2023 c_f_strpointer is accepted by LLVM Flang 23 and is not provided by GNU Fortran 16.2. The program below uses it with strlen for the length:

! Fortran 2023 C_F_STRPOINTER (18.2.3.5). GNU Fortran 16.2 does not provide it; LLVM Flang 23 does.
program c_strpointer_getenv
  use, intrinsic :: iso_c_binding, only: c_char, c_ptr, c_size_t, c_associated, c_f_strpointer, f_c_string
  implicit none
  interface
    function c_getenv(name) bind(c, name='getenv') result(p)
      import :: c_char, c_ptr
      character(kind=c_char), intent(in) :: name(*)
      type(c_ptr) :: p
    end function c_getenv

    function c_strlen(s) bind(c, name='strlen') result(n)
      import :: c_ptr, c_size_t
      type(c_ptr), value, intent(in) :: s
      integer(c_size_t) :: n
    end function c_strlen
  end interface
  character(kind=c_char, len=:), pointer :: value
  type(c_ptr) :: p

  p = c_getenv(f_c_string('HEATFLOW_OUT'))
  if (c_associated(p)) then
    call c_f_strpointer(p, value, int(c_strlen(p)))   ! NCHARS = the exact length, found with strlen
    print '(a, i0, 3a)', 'length ', len(value), ' value "', value, '"'
  else
    print '(a)', 'HEATFLOW_OUT is not set'
  end if
end program c_strpointer_getenv

LLVM Flang 23 printed:

length 16 value "results/heat.csv"

GNU Fortran 16.2 stopped at the use statement:

08_strpointer.f90:3:79:

    3 |   use, intrinsic :: iso_c_binding, only: c_char, c_ptr, c_size_t, c_associated, c_f_strpointer, f_c_string
      |                                                                               1
Error: Symbol 'c_f_strpointer' referenced at (1) not found in intrinsic module ISO_C_BINDING

The standard says that the length of the pointer becomes the largest value, not above nchars, for which no null character is in the sequence (18.2.3.5), so that nchars is an upper bound and the usual call c_f_strpointer(p, value, 1023) is meant to work for any string of up to 1023 characters; that is the form the standard’s own getenv example uses. In Flang 23 the length was exactly nchars in these experiments (a call with nchars = 17 for the 16-character string gave a length-17 value ending with the null character, and one with 64 or 1023 crashed with a segmentation fault when the value was printed), so on that compiler the safe use passes the exact length, as the program does. The exact-length call is correct under both readings of the text. The portable code today is strlen and c_f_pointer, as in the previous program.

Strings can also be passed in a bind(c) interface as character(kind=c_char, len=*) (assumed length, a Fortran 2018 feature from TS 29113): C then receives a pointer to a descriptor, not a char *, so the C function has to be written against ISO_Fortran_binding.h. Prefer character(kind=c_char) with dimension(*) for plain C strings. The character handling of the language itself is in Characters and Strings.

Optional arguments and assumed type

Since Fortran 2018 an interoperable procedure can have optional dummy arguments (18.3.7). When Fortran calls a C function and the actual argument is absent, C receives a null pointer; when C calls a Fortran procedure, the argument is absent exactly when C passes a null pointer. An optional dummy argument cannot also have value (C869), since a value has no null. A type(*) (assumed-type) dummy argument accepts an actual argument of any type and kind, with no type information: the Fortran way of writing const void *. With dimension(*) it receives the address of the first element of an array of any type, and c_sizeof of the actual array provides the byte count:

#include <stddef.h>

/* XOR checksum of nbytes bytes, any element type. */
unsigned int hf_checksum(const void *buf, size_t nbytes) {
  const unsigned char *b = buf;
  unsigned int sum = 0;
  for (size_t i = 0; i < nbytes; ++i) sum ^= (unsigned int)b[i] << (8 * (i % 4));
  return sum;
}

/* Scales x in place by factor, or by 2 when factor is a null pointer. */
void hf_scale(double *x, int n, const double *factor) {
  double f = factor == NULL ? 2.0 : *factor;
  for (int i = 0; i < n; ++i) x[i] *= f;
}
program c_assumed_type
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_size_t, c_sizeof
  implicit none

  interface
    ! const void *buf: a type(*) dummy accepts an actual argument of any type
    function hf_checksum(buf, nbytes) bind(c) result(sum)
      import :: c_size_t, c_int
      type(*), intent(in) :: buf(*)
      integer(c_size_t), value, intent(in) :: nbytes
      integer(c_int) :: sum
    end function hf_checksum

    ! const double *factor may be NULL: optional, and absent in Fortran means NULL in C (18.3.7)
    subroutine hf_scale(x, n, factor) bind(c)
      import :: c_double, c_int
      real(c_double), intent(inout) :: x(*)
      integer(c_int), value, intent(in) :: n
      real(c_double), intent(in), optional :: factor
    end subroutine hf_scale
  end interface

  real(c_double) :: u(4) = [1.0_c_double, 2.0_c_double, 3.0_c_double, 4.0_c_double]
  integer(c_int) :: k(3) = [1_c_int, 2_c_int, 4_c_int]

  print '(a, z8.8)', 'checksum of 4 doubles: ', hf_checksum(u, c_sizeof(u))
  print '(a, z8.8)', 'checksum of 3 ints:    ', hf_checksum(k, c_sizeof(k))

  call hf_scale(u, size(u), 10.0_c_double)
  print '(a, 4f6.1)', 'factor given:  ', u
  call hf_scale(u, size(u))
  print '(a, 4f6.1)', 'factor absent: ', u
end program c_assumed_type
checksum of 4 doubles: 7FE80000
checksum of 3 ints:    00000007
factor given:    10.0  20.0  30.0  40.0
factor absent:   20.0  40.0  60.0  80.0

Flang 23 printed the same four lines. The same function accepted an array of doubles and an array of integers through type(*), which would otherwise need a void * and c_loc (and the target attribute) or two generic specifics. An assumed-type dummy argument cannot be allocatable, pointer, value or intent(out), and its name can appear only as an actual argument for another assumed-type dummy argument, as the first argument of is_contiguous, lbound, present, rank, shape, size or ubound, or of c_loc (7.3.2.2, C714 and C715); its elements cannot be read in Fortran. That is why hf_checksum is given the byte count by its caller. Assumed-type, assumed-rank buffers are also how the MPI bindings accept arrays of any type (see MPI with mpi_f08).

Fortran calling back into C, and the order of output

The example below is the pattern behind most real C-to-Fortran libraries: a C main calls a Fortran routine for the heavy work, hands it a function pointer to report progress and an opaque void * that the Fortran code passes back unchanged (the context pointer, which lets the C callback find its state without a global variable). The Fortran routine is the Jacobi iteration of heatflow with the kernel of heatflow_kernels; its tolerance is an optional argument (a null pointer in C) and its callback may be null too.

module heatflow_run
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_ptr, c_funptr, c_f_procpointer, c_associated
  use heatflow_kernels, only: jacobi_sweep
  implicit none
  private
  public :: hf_run

  abstract interface
    ! void (*report)(int step, double residual, void *context)
    subroutine report_cb(step, residual, context) bind(c)
      import :: c_int, c_double, c_ptr
      integer(c_int), value, intent(in) :: step
      real(c_double), value, intent(in) :: residual
      type(c_ptr), value, intent(in) :: context
    end subroutine report_cb
  end interface

contains

  ! int hf_run(int nx, int ny, int max_steps, const double *tolerance, void (*report)(...), void *context);
  function hf_run(nx, ny, max_steps, tolerance, report, context) bind(c, name='hf_run') result(steps)
    integer(c_int), value, intent(in) :: nx, ny, max_steps
    real(c_double), intent(in), optional :: tolerance    ! optional: a null pointer from C means "absent"
    type(c_funptr), value, intent(in) :: report          ! may be NULL
    type(c_ptr), value, intent(in) :: context            ! handed back untouched
    integer(c_int) :: steps

    real(c_double), allocatable :: u(:, :), unew(:, :)
    real(c_double) :: residual, tol
    procedure(report_cb), pointer :: notify

    tol = 1.0e-3_c_double
    if (present(tolerance)) tol = tolerance
    notify => null()
    if (c_associated(report)) call c_f_procpointer(report, notify)

    allocate (u(nx, ny), source=0.0_c_double)
    u(1, :) = 100.0_c_double                              ! hot left edge, fixed
    unew = u
    steps = 0_c_int
    do while (steps < max_steps)
      call jacobi_sweep(u, unew, residual)
      u = unew
      steps = steps + 1_c_int
      if (associated(notify) .and. mod(steps, 50_c_int) == 0_c_int) call notify(steps, residual, context)
      if (residual < tol) exit
    end do
  end function hf_run
end module heatflow_run
#include <stdio.h>

int hf_run(int nx, int ny, int max_steps, const double *tolerance,
           void (*report)(int, double, void *), void *context);

typedef struct { int calls; double last; } progress;

static void on_report(int step, double residual, void *context) {
  progress *p = context;
  p->calls += 1;
  p->last = residual;
  printf("  C callback: step %d residual %.4f\n", step, residual);
}

int main(void) {
  progress p = {0, 0.0};
  double tol = 5.0e-2;
  printf("with tolerance and callback\n");
  int steps = hf_run(21, 21, 1000, &tol, on_report, &p);
  printf("steps=%d callbacks=%d last=%.4f\n", steps, p.calls, p.last);

  printf("without tolerance (NULL) and without callback\n");
  steps = hf_run(21, 21, 1000, NULL, NULL, NULL);
  printf("steps=%d\n", steps);
  return 0;
}
sequenceDiagram participant M as C main participant R as hf_run (Fortran, bind(c)) participant K as jacobi_sweep (Fortran) participant C as on_report (C callback) M->>R: hf_run(21, 21, 1000, tol, on_report, p) Note over R: present(tolerance) is true
c_associated(report) is true
c_f_procpointer(report, notify) loop until residual below tolerance R->>K: jacobi_sweep(u, unew, residual) K-->>R: residual opt every 50th step R->>C: notify(step, residual, context) C-->>R: returns (updates progress p) end end R-->>M: steps = 187 M->>R: hf_run(21, 21, 1000, NULL, NULL, NULL) Note over R: present(tolerance) is false, no callback R-->>M: steps = 502

Built with the sources in this order, gfortran for the module and gcc for the link (add -lgfortran -lm), the program printed the following, and the Flang 23 build gave exactly the same output:

with tolerance and callback
  C callback: step 50 residual 0.4422
  C callback: step 100 residual 0.1665
  C callback: step 150 residual 0.0808
steps=187 callbacks=3 last=0.0808
without tolerance (NULL) and without callback
steps=502

With the tolerance 0.05 supplied the iteration stopped after 187 steps and the callback ran three times (steps 50, 100 and 150); with NULL for the tolerance the default 0.001 applied and the iteration needed 502 steps, with no callback. A c_funptr with the value attribute is the declaration used here for the callback because it is the type that c_associated can test for null; the program passes NULL as the callback in its second call and the Fortran code then skips it.

Two kinds of program state do not cross the boundary silently. Input/output: Fortran and C each have their own buffers for standard output, and when both write to the same file the order is processor dependent (18.10.1). The little program below, fortran_line called from a C main between two printfs with standard output piped to cat, printed these orders:

subroutine fortran_line(flush_after) bind(c, name='fortran_line')
  use, intrinsic :: iso_c_binding, only: c_int
  use, intrinsic :: iso_fortran_env, only: output_unit
  implicit none
  integer(c_int), value, intent(in) :: flush_after
  print '(a)', 'Fortran line'
  if (flush_after /= 0_c_int) flush (output_unit)
end subroutine fortran_line
#include <stdio.h>
#include <stdlib.h>

void fortran_line(int flush_after);

int main(int argc, char **argv) {
  int flush_after = argc > 1 ? atoi(argv[1]) : 0;
  printf("C line 1\n");
  if (flush_after) fflush(stdout);
  fortran_line(flush_after);
  printf("C line 2\n");
  return 0;
}
== piped, no flush
C line 1
Fortran line
C line 2
== piped, flush
C line 1
Fortran line
C line 2
== flang piped, no flush
Fortran line
C line 1
C line 2
== flang piped, flush
C line 1
Fortran line
C line 2

With nothing flushed, GNU Fortran 16.2 produced the call order, while Flang 23 printed the Fortran line first. After a flush (and fflush) on each side before control crosses the boundary, both agree. The standard does not promise either order, so a library that prints from both languages should flush before every call that crosses (or better: write from one language only). Exceptions and signals: a procedure defined by something other than Fortran must not use signal to change how a floating-point exception is handled, and must not alter the floating-point status other than by raising flags (18.10.3), and the flag values on entry to it are processor dependent. stop and error stop end the whole program, not only the Fortran part; see Error Handling and Program Termination and Floating-Point and IEEE Arithmetic.

Linking and building

A mixed program is several object files and one link step, and the question is which driver does it and which runtime libraries it must add:

Situation What to do

C main, Fortran library, GNU toolchain

link with gcc and add -lgfortran, as above; or link with gfortran, which adds the runtime itself

Fortran main, C library

link with gfortran; add -lm or other libraries as the C code needs

Flang

link with flang-23, which adds the Flang runtime; compile the C files with -fPIC when the linker complains about relocations, as it did in the callback program

C++ main

a g++ link of the shared library above worked; the header needs extern "C" (the guard in heatflow.h)

Shared library

compile the Fortran sources with -fPIC, link with gfortran -shared, and pass -L. -lname (and an rpath or LD_LIBRARY_PATH) to the consumer; the C program above linked against the shared library without naming -lgfortran, because the library carries that dependency

Static library

ar rcs libx.a *.o; the consumer names the Fortran runtime library explicitly

Mixed compilers

expose only bind(c) names, and keep the .mod files of each compiler in its own directory, since one compiler cannot read another’s

The Fortran runtime of GNU Fortran is a separate library and carries I/O units, formatted output and array runtime helpers, so a Fortran routine that prints or allocates may fail to link from gcc without -lgfortran. The build system mechanics (the order of .mod files, add_library, FortranCInterface) are on the pages about Building with Make, CMake and Meson and Compilers and Compiler Options.

Where the books are outdated

The book consulted for this page (Metcalf, Reid, Cohen and Bader’s Modern Fortran Explained for Fortran 2023: chapter 20 on interoperability, and the interoperability section of chapter 23 on the Fortran 2023 additions) is a sound introduction to the concepts. Where it, or older Fortran material, differs from current practice:

Area Outdated in the books, current approach

Restrictions of the first design

the 2003 rules (no optional, no allocatable, assumed-shape or pointer dummy arguments, no assumed type in a bind(c) interface) were lifted in Fortran 2018; use the C descriptors on C Descriptors and ISO_Fortran_binding for such arguments

Strings

building a C string with trim(s) // c_null_char and walking a returned pointer by hand is replaced by f_c_string and c_f_strpointer in Fortran 2023; only f_c_string is portable across GNU Fortran 16.2 and Flang 23 today

c_f_pointer

the lower argument is new in Fortran 2023; the older workaround is pointer bounds remapping

c_f_strpointer description

the character pointer argument fstrptr is a deferred-length pointer of kind c_char, not of kind c_ptr; its length is at most (less than or equal to) nchars, up to the first null character; and a getenv example uses c_f_strpointer, not c_f_pointer with a fixed count. Where a printed description differs from 18.2.3.5, follow the standard

Linkage conventions

trailing underscores, %VAL, hidden string lengths and integer*4-style kinds in the MATLAB and older Fortran material are compatibility mechanisms; new code uses bind(c), value and kinds from iso_c_binding

Compiler context

the books say nothing about compilers; this page records what GNU Fortran 16.2 and LLVM Flang 23 accept

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-19

iso_c_binding kinds, c_ptr, c_funptr, c_loc, c_f_pointer, bind(c), value, interoperable derived types and variables

Fortran 2003 (listed under Fortran 2003 in the GNU Fortran implementation status)

GNU Fortran 16.2 and LLVM Flang 23 build every example here

optional, assumed-type, assumed-rank, allocatable and assumed-shape dummy arguments in bind(c) procedures, c_loc of noninteroperable arrays, c_ptrdiff_t, ISO_Fortran_binding.h

Fortran 2018 (content of TS 29113, 4.3.3 and the list of new features of J3/23-007r1)

GNU Fortran documents the support; optional and assumed-type dummy arguments are exercised on this page, the descriptor-based ones on the C descriptors page

c_f_pointer(..., lower)

Fortran 2023

accepted by GNU Fortran 16.2 and LLVM Flang 23 (examples above)

f_c_string

Fortran 2023

accepted by both

c_f_strpointer

Fortran 2023

accepted by Flang 23, rejected by GNU Fortran 16.2 (not in the module); Flang’s standards-support table lists the string conversion procedures as not implemented, although the build checked here accepts them, and the length it gave differs from 18.2.3.5

Diagnosis of a non-interoperable bind(c) interface

text of the standard (C1562-C1567)

GNU Fortran reports a warning or an error; Flang 23 did not report the cases in this page

c_float128, c_float16

extensions

documented by Flang; GNU Fortran 16.2 accepts c_float128 without -std=f2023

Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers. See History, Standards and Compilers for the per-compiler support table.

Pitfalls

  • A kind that is not an iso_c_binding constant. integer, real and logical of default kind work on one platform and break on another; use c_int, c_double, c_bool.

  • value missing or extra. The compiler cannot see the C prototype. A missing value passes an address where C wants a number; an extra one passes a number where C wants an address.

  • A character(len=*) dummy argument for a char *. Use character(kind=c_char) with dimension(*); assumed length means a descriptor.

  • Forgetting the terminator. A Fortran string passed to a C const char * without c_null_char is read past its end.

  • Printing a c_char array with a. One element per format item; convert it to a string first.

  • Column-major, row-major. a[i][j] in C is a(j+1, i+1) in Fortran; a transposed loop is correct but slow.

  • Dangling c_loc. The address of a local, automatic or deallocated target; and c_loc of an array section that was copied to a temporary.

  • Freeing across allocators. deallocate on malloc memory, or free on allocate memory.

  • Linking without the Fortran runtime. an undefined reference to a name starting with _gfortran_ means -lgfortran is missing.

  • Mixing object files of different compilers' modules. Only the bind(c) names are stable.

  • Variable arguments. printf and other ... functions have no interoperable interface (18.3.7).

  • Exceptions, longjmp and signals through Fortran frames. Not permitted (18.10.1).

  • I/O ordering. Fortran and C buffers are separate; flush before crossing the boundary when both write to one file.

  • A c_f_strpointer call with a larger nchars than the string. Allowed by the standard, but it crashed LLVM Flang 23 in the experiment above; pass the exact length from strlen.

  • Trusting the compiler to compare interface and prototype. Keep one header, review it with the Fortran interface and test c_sizeof against sizeof for every shared structure.