C Descriptors and ISO_Fortran_binding

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-18. Every Fortran source file was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and every C source file with gcc -std=c11 -Wall -Wextra -pedantic (GCC 16.2), both warning-free; each program was run, and the same sources were also built and run against LLVM Flang 23 (flang-23 for the Fortran, GCC for the C, with the header that ships with Flang). The output shown is real output; where the two compilers differ, the page says so and shows both. The rules are in clauses 18.3.7 (interoperability of procedures and procedure interfaces), 18.4 (C descriptors), 18.5 (the source file ISO_Fortran_binding.h, with its structures, macros and functions) and 18.6 to 18.8 (restrictions on C descriptors, on formal parameters and on lifetimes) of the standard.

The basic interoperability of iso_c_binding kinds, bind(c) procedures, value and c_ptr is covered in Interoperability with C; this page starts where that one stops. A Fortran dummy argument is interoperable with a plain C pointer only when it has a simple layout: a scalar, an explicit-shape or assumed-size array, or an assumed-type type(*) object. Four kinds of dummy argument carry more information than a bare address: an assumed-shape array (its extents, and a stride when the actual argument is a section), an assumed-rank object (its rank), an allocatable or pointer object (an allocation or association status that C may change), and a character dummy with assumed length (its length). The standard gives such an argument to C as a C descriptor: a structure of type CFI_cdesc_t, defined in a header file ISO_Fortran_binding.h that the Fortran processor provides, together with a small library of functions that create and manipulate descriptors. The descriptor mechanism was specified by the technical specification ISO/IEC TS 29113:2012 and is part of the 2018 standard, as the GNU Fortran manual states, so it is available in Fortran 2018 and 2023 and in compilers that implement TS 29113.

The examples use the temperature field of the running example, heatflow, as the object passed across the language boundary: a real(c_double) array u of temperatures, sections of it, probes that carry an id and a temperature, and the name of the solver as a character argument. Examples mix Fortran and C: each is a Fortran file plus a C file, built with the commands in Building the examples. The data type, grid_t, that heatflow uses internally is not interoperable (it has an allocatable component), which is the usual reason to write an assumed-shape or allocatable bind(c) interface in the first place: the C code sees an array, not the type.

When a descriptor is used

A bind(c) Fortran interface is interoperable with a C prototype under the rules of 18.3.7. The part of those rules that matters here is what the C formal parameter looks like, which depends on the Fortran dummy argument:

Fortran dummy argument in the bind(c) interface C formal parameter Clause

a scalar or array that is interoperable (scalar, explicit-shape, assumed-size), with or without value

a pointer to the C type (or the C type itself with value)

18.3.7

character(*) (assumed length), nonallocatable and nonpointer

CFI_cdesc_t * (a descriptor of rank 0)

18.3.7 (5)

assumed-shape, assumed-rank or allocatable dummy, or a pointer dummy without the contiguous attribute

CFI_cdesc_t *

18.3.7 (5)

assumed-type type(*) that is not assumed-shape, assumed-rank, allocatable or pointer

void * (no descriptor)

18.3.7 (5)

allocatable or pointer dummy of type character

must have deferred length; CFI_cdesc_t *

18.3.7 (6)

an optional dummy argument

a null pointer when the actual argument is absent

18.3.7

(The standard’s wording attaches "without the CONTIGUOUS attribute" to the whole list; both compilers pass a descriptor for a contiguous assumed-shape dummy, as examples 09 and 10 below show, and 18.3.7 goes on to describe the contiguity of the object that such a descriptor describes.) The same text says that, from C, the actual argument for such a formal parameter is the address of a C descriptor for the intended effective argument, and that the type and attribute members must agree with the dummy argument. The Fortran compiler builds the descriptor when Fortran calls C; the C programmer builds it when C calls Fortran. Either way the C code reads and possibly modifies it only through the functions and members described below.

The header ISO_Fortran_binding.h

The header (18.5.1) holds structure definitions, typedefs, macros and function prototypes; it may be included in any order relative to the standard C headers and more than once. A C source file that includes it shall not use names starting with CFI_ that the header does not define. Its location is processor dependent: it is installed with the compiler. In the container used for this page it is /usr/local/lib/gcc/aarch64-linux-gnu/16.2.0/include/ISO_Fortran_binding.h for GNU Fortran, which gcc finds without a flag, and /usr/lib/llvm-23/include/flang/ISO_Fortran_binding.h for Flang, which needs -I/usr/lib/llvm-23/include/flang. The two headers are not the same file: the values of many macros differ, as the tables below show, so C code must always use the macro names and never their numeric values.

CFI_dim_t and CFI_cdesc_t

CFI_dim_t (18.5.2) describes one dimension of an array; it has at least these members, in any order:

Member Meaning

CFI_index_t lower_bound

the lower bound of the dimension

CFI_index_t extent

the number of elements in the dimension, or -1 for the last dimension of an assumed-size array

CFI_index_t sm

the memory stride of the dimension: the difference in bytes between the addresses of successive elements

CFI_cdesc_t (18.5.3) is a typedef name for a structure with a flexible array member. The first three members are base_addr, elem_len and version in that order, the last is dim, and the others lie between version and dim in any order:

Member Meaning

void *base_addr

a null pointer for an unallocated allocatable or a disassociated pointer; for a zero-sized object a non-null, otherwise processor-dependent value; otherwise the address of the object, which for an array is the address of the first element in array element order

size_t elem_len

the storage size in bytes of the object (a scalar) or of one element (an array); for a character object the length times the bytes of one character

int version

the value of CFI_VERSION in the header that defined the format of this descriptor

CFI_rank_t rank

the number of dimensions, zero for a scalar

CFI_type_t type

a type code from Attribute and type codes

CFI_attribute_t attribute

an attribute code: pointer, allocatable, or neither

CFI_dim_t dim[]

as many elements as the rank; each gives the lower bound, extent and byte stride of one dimension

Three rules about the contents are easy to miss. The lower_bound members of an allocatable array or a pointer are set by argument association, allocation or pointer association; for a nonallocatable, nonpointer object (for example an assumed-shape dummy argument) they are zero, whatever the lower bounds of the Fortran actual argument are. The strides must be consistent with a non-overlapping layout: there is an ordering of the dimensions in which the absolute value of the first stride is at least elem_len and each following stride is at least the previous stride times the previous extent, which allows reordered subscripts (a transposed view) without a copy. And the strides are in bytes and may be negative, so a C function must step with sm, never with sizeof of the element type.

The structure is meant to be read: a C function receiving a const CFI_cdesc_t * may look at every member. It is not meant to be written except through the functions below (18.6): descriptors are not initialised, updated or copied in any other way.

A C descriptor for the section u(2:4, 2:6:2) of a 4 by 6 real(c_double) array: the Fortran grid with the nine section elements shaded, the descriptor members base_addr, elem_len, version, rank, type, attribute and dim, and the 24 elements in memory with base_addr pointing at position 5 and byte strides of 8 and 64

CFI_CDESC_T, CFI_index_t, CFI_rank_t, CFI_MAX_RANK and CFI_VERSION

A C function cannot declare a CFI_cdesc_t variable, because the structure has a flexible array member. The macro CFI_CDESC_T(r) (18.5.4) takes the rank r as an integer constant expression, 0 to CFI_MAX_RANK, and expands to an unqualified type of suitable size and alignment to hold a descriptor of that rank. A variable declared with it cannot have an initializer, and its address is not usable where a CFI_cdesc_t * is expected without an explicit cast:

CFI_CDESC_T(2) storage;                       /* room for a rank-2 descriptor */
CFI_cdesc_t *d = (CFI_cdesc_t *)&storage;     /* the pointer every CFI_ function takes */

CFI_index_t is a signed integer type wide enough for the difference of two pointers (ptrdiff_t in both headers), and should be used for every subscript, extent and bound. CFI_rank_t, CFI_attribute_t and CFI_type_t are integer types wide enough for their values; their widths differ between the headers (in GNU Fortran’s header CFI_rank_t and CFI_attribute_t are int8_t and CFI_type_t is int16_t, while in Flang’s CFI_rank_t and CFI_attribute_t are unsigned char and CFI_type_t is signed char), so a literal outside the range of a code needs a cast. CFI_MAX_RANK is the largest rank supported, at least 15; both compilers use 15. CFI_VERSION encodes the version of the header and is increased when a new version is incompatible with the old one. It is 1 in GNU Fortran’s header and 20240719 in Flang’s. The version member of a descriptor holds this value so that a descriptor built with one version of the header can be recognised by a runtime built with another.

Looking at a descriptor

The first example prints everything a C function can learn from the descriptor of an assumed-shape dummy argument. The Fortran side declares u(:, :) in a bind(c) interface; the C side takes const CFI_cdesc_t *.

#include <stdio.h>
#include <ISO_Fortran_binding.h>

static const char *attribute_name(CFI_attribute_t a)
{
    if (a == CFI_attribute_pointer) return "pointer";
    if (a == CFI_attribute_allocatable) return "allocatable";
    if (a == CFI_attribute_other) return "other";
    return "unknown";
}

void describe(const CFI_cdesc_t *d)
{
    printf("  rank=%d elem_len=%zu attribute=%s double=%s\n", (int)d->rank, d->elem_len,
           attribute_name(d->attribute), d->type == CFI_type_double ? "yes" : "no");
    for (int i = 0; i < d->rank; i++)
        printf("  dim[%d]: lower_bound=%ld extent=%ld sm=%ld\n", i, (long)d->dim[i].lower_bound,
               (long)d->dim[i].extent, (long)d->dim[i].sm);
    printf("  contiguous=%d\n", CFI_is_contiguous(d));
    fflush(stdout);
}
module descr_api
    use, intrinsic :: iso_c_binding, only: c_double
    implicit none
    interface
        subroutine describe(u) bind(c, name='describe')
            import :: c_double
            real(c_double), intent(in) :: u(:, :)
        end subroutine describe
    end interface
end module descr_api

program describe_demo
    use, intrinsic :: iso_c_binding, only: c_double
    use, intrinsic :: iso_fortran_env, only: output_unit
    use descr_api, only: describe
    implicit none
    integer, parameter :: nx = 4, ny = 6
    real(c_double), target :: u(nx, ny)
    real(c_double) :: v(0:3, -1:1)
    integer :: i, j

    u = reshape([((real(10 * i + j, c_double), i = 1, nx), j = 1, ny)], [nx, ny])
    v = 0.0_c_double

    print '(a)', 'whole array u(1:4, 1:6):'
    flush (output_unit)
    call describe(u)

    print '(a)', 'section u(2:4, 2:6:2):'
    flush (output_unit)
    call describe(u(2:4, 2:6:2))

    print '(a)', 'column block u(:, 5:6):'
    flush (output_unit)
    call describe(u(:, 5:6))

    print '(a)', 'array v(0:3, -1:1) declared with other lower bounds:'
    flush (output_unit)
    call describe(v)
end program describe_demo

The program calls describe with the whole array, with a strided section, with a block of columns, and with an array declared with other lower bounds:

whole array u(1:4, 1:6):
  rank=2 elem_len=8 attribute=other double=yes
  dim[0]: lower_bound=0 extent=4 sm=8
  dim[1]: lower_bound=0 extent=6 sm=32
  contiguous=1
section u(2:4, 2:6:2):
  rank=2 elem_len=8 attribute=other double=yes
  dim[0]: lower_bound=0 extent=3 sm=8
  dim[1]: lower_bound=0 extent=3 sm=64
  contiguous=0
column block u(:, 5:6):
  rank=2 elem_len=8 attribute=other double=yes
  dim[0]: lower_bound=0 extent=4 sm=8
  dim[1]: lower_bound=0 extent=2 sm=32
  contiguous=1
array v(0:3, -1:1) declared with other lower bounds:
  rank=2 elem_len=8 attribute=other double=yes
  dim[0]: lower_bound=0 extent=4 sm=8
  dim[1]: lower_bound=0 extent=3 sm=32
  contiguous=1

The output is identical for GNU Fortran 16.2 and Flang 23. Everything stated above can be read in it. The element length is 8 (real(c_double)), the attribute is CFI_attribute_other because the dummy argument is neither allocatable nor a pointer, and every lower_bound is zero even for v(0:3, -1:1), whose Fortran lower bounds are 0 and -1. The whole array has strides 8 and 32 bytes: one element, and four elements (one column of four 8-byte reals). The section u(2:4, 2:6:2) has extents 3 and 3 and strides 8 and 64: stepping two columns is stepping 64 bytes, and the descriptor reports contiguous=0 because the elements are not adjacent. The block of columns u(:, 5:6) is a sub-array that happens to be contiguous, so it gets contiguous=1; no data is copied in any of these calls. The descriptor of u was not changed by Fortran or by C: the dummy argument has no intent(out), and C is not allowed to modify a descriptor that corresponds to a nonallocatable, nonpointer object (18.6).

Attribute and type codes

Attribute codes

The macros of Table 18.3 give the value of the attribute member:

Macro Fortran object GNU Fortran 16.2 Flang 23

CFI_attribute_pointer

data pointer

0

1

CFI_attribute_allocatable

allocatable

1

2

CFI_attribute_other

nonallocatable, nonpointer

2

0

The standard requires only that the values are nonnegative and distinct. The numbers in the last two columns come from example 08_errors.c below; a program that tests d->attribute == 0 is correct for one compiler and wrong for the other.

Type codes

The type member says what the elements are. Table 18.4 lists the macros; the first column below is the macro, the second the C type it stands for and the third the Fortran type it corresponds to:

Macro C type Typical Fortran declaration

CFI_type_signed_char, CFI_type_short, CFI_type_int, CFI_type_long, CFI_type_long_long, CFI_type_size_t

signed char, short int, int, long int, long long int, size_t

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

CFI_type_int8_t, CFI_type_int16_t, CFI_type_int32_t, CFI_type_int64_t

int8_t to int64_t

integer(c_int8_t) to integer(c_int64_t)

CFI_type_int_least8_t to CFI_type_int_least64_t, CFI_type_int_fast8_t to CFI_type_int_fast64_t

int_least8_t to int_fast64_t

integer(c_int_least8_t) and the fast and least kinds

CFI_type_intmax_t, CFI_type_intptr_t, CFI_type_ptrdiff_t

intmax_t, intptr_t, ptrdiff_t

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

CFI_type_float, CFI_type_double, CFI_type_long_double

float, double, long double

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

CFI_type_float_Complex, CFI_type_double_Complex, CFI_type_long_double_Complex

float _Complex, double _Complex, long double _Complex

complex(c_float_complex), complex(c_double_complex), complex(c_long_double_complex)

CFI_type_Bool

_Bool

logical(c_bool)

CFI_type_char

char

character(kind=c_char)

CFI_type_cptr

void *

type(c_ptr)

CFI_type_struct

an interoperable C structure

type(t) with bind(c)

CFI_type_other

not otherwise specified

any other type (for example a type with kind not listed)

The values for different C types can be the same (CFI_type_int and CFI_type_int32_t may be equal). The value of CFI_type_other is negative and distinct from all others, and CFI_type_struct is positive and distinct from all others; if a C type is not interoperable with a Fortran kind the processor supports, its macro is negative, otherwise positive. A processor may add type codes for its own kinds (extension kinds); Flang’s header has, for example, codes for half_float (kind=2) and float128.

The numbers are processor dependent and the two compilers choose very differently. GNU Fortran encodes the intrinsic type in the low byte and the kind in the bits above it, as its header explains (CFI_type_double is CFI_type_Real + (sizeof (double) << 8)), so a mask of CFI_type_mask recovers the intrinsic type of a code; Flang numbers its codes consecutively. The program of the section on error codes prints them for the compiler it is built with. What a C function should do is compare with the macros:

if (d->type != CFI_type_double) return CFI_INVALID_TYPE;   /* portable */

A descriptor received through a bind(c) interface has the type the interface declares, so a check like this is only needed in a generic C function whose Fortran interface accepts several types (an assumed-type or assumed-rank object), and in a C function that builds descriptors for others.

The library functions

The functions of 18.5.5 return an error indicator: zero (CFI_SUCCESS) for no error and a nonzero value, one of the macros of Error codes, otherwise (except CFI_address, which returns a pointer, and CFI_is_contiguous, which returns 1 or 0). The standard allows each function to be a macro or an identifier with external linkage, and says that suppressing a macro definition to reach the real function is undefined, so take the address of nothing in the header. When a call could detect more than one error, which one is reported is processor dependent. If an error is detected the descriptor passed to be changed is not modified. Arguments that represent subscripts, bounds, extents or strides are in the same order as the dim array of the descriptor (first dimension first, which is the Fortran order, not the C order).

Function What it does Clause

int CFI_establish(CFI_cdesc_t *dv, void *base_addr, CFI_attribute_t attribute, CFI_type_t type, size_t elem_len, CFI_rank_t rank, const CFI_index_t extents[])

initialises a descriptor declared in C with CFI_CDESC_T: for a nonallocatable, nonpointer object of known shape, an unallocated allocatable, or a data pointer

18.5.5.5

void *CFI_address(const CFI_cdesc_t *dv, const CFI_index_t subscripts[])

the C address of the element with these subscripts, or of the scalar when subscripts is a null pointer

18.5.5.2

int CFI_allocate(CFI_cdesc_t *dv, const CFI_index_t lower_bounds[], const CFI_index_t upper_bounds[], size_t elem_len)

allocates the object with the Fortran allocate mechanism and stores the address in base_addr

18.5.5.3

int CFI_deallocate(CFI_cdesc_t *dv)

deallocates with the deallocate mechanism and sets base_addr to null

18.5.5.4

int CFI_is_contiguous(const CFI_cdesc_t *dv)

1 if the array is contiguous, 0 otherwise; base_addr must not be null

18.5.5.6

int CFI_section(CFI_cdesc_t *result, const CFI_cdesc_t *source, const CFI_index_t lower_bounds[], const CFI_index_t upper_bounds[], const CFI_index_t strides[])

updates result to describe the array section source(l1:u1:s1, ...); a zero stride selects one subscript and lowers the rank

18.5.5.7

int CFI_select_part(CFI_cdesc_t *result, const CFI_cdesc_t *source, size_t displacement, size_t elem_len)

updates result to describe a part (a component, a substring, a real or imaginary part) of every element of source

18.5.5.8

int CFI_setpointer(CFI_cdesc_t *result, CFI_cdesc_t *source, const CFI_index_t lower_bounds[])

makes the pointer described by result point to the whole of source, or disassociates it when source is null

18.5.5.9

The rest of the page uses each in turn. The rules that every function shares are in 18.5.5.1; the ones that differ are stated with the function.

CFI_address and walking an array of any rank

CFI_address(dv, subscripts) computes base_addr plus, for each dimension, (subscripts[i] - lower_bound[i]) * sm[i] bytes, so a C function that uses it needs no knowledge of contiguity, the order of the dimensions or negative strides. The subscripts are in the bounds recorded in the descriptor (zero-based for a nonallocatable, nonpointer object). For a scalar the argument is a null pointer. This is how to write one C function that handles every rank and every layout: the Fortran interface declares the dummy argument assumed-rank, x(..), which is also interoperable (18.3.7), and the C function reads rank and loops over an odometer of subscripts.

#include <stddef.h>
#include <ISO_Fortran_binding.h>

/* Sum every element of a double array of any rank, in any memory layout.
   Returns the sum through *total and the number of elements through *count. */
int sum_any_rank(const CFI_cdesc_t *x, double *total, long *count)
{
    CFI_index_t sub[CFI_MAX_RANK];
    CFI_index_t n = 1;

    if (x->type != CFI_type_double)
        return CFI_INVALID_TYPE;
    *total = 0.0;
    *count = 0;
    if (x->rank == 0) {                     /* a scalar: no subscripts */
        *total = *(const double *)CFI_address(x, NULL);
        *count = 1;
        return CFI_SUCCESS;
    }
    for (int d = 0; d < x->rank; d++) {
        n *= x->dim[d].extent;
        sub[d] = x->dim[d].lower_bound;     /* start at the first element */
    }
    for (CFI_index_t k = 0; k < n; k++) {   /* odometer over the subscripts */
        *total += *(const double *)CFI_address(x, sub);
        (*count)++;
        for (int d = 0; d < x->rank; d++) {
            if (++sub[d] < x->dim[d].lower_bound + x->dim[d].extent)
                break;
            sub[d] = x->dim[d].lower_bound;
        }
    }
    return CFI_SUCCESS;
}
module descr_sum
    use, intrinsic :: iso_c_binding, only: c_double, c_int, c_long
    implicit none
    interface
        function sum_any_rank_c(x, total, count) result(stat) bind(c, name='sum_any_rank')
            import :: c_double, c_int, c_long
            real(c_double), intent(in) :: x(..)
            real(c_double), intent(out) :: total
            integer(c_long), intent(out) :: count
            integer(c_int) :: stat
        end function sum_any_rank_c
    end interface
end module descr_sum

program sum_demo
    use, intrinsic :: iso_c_binding, only: c_double, c_int, c_long
    use descr_sum, only: sum_any_rank_c
    implicit none
    real(c_double) :: s, total, cube(2, 3, 4), field(5, 6)
    real(c_double) :: line(7)
    integer(c_long) :: n
    integer(c_int) :: stat
    integer :: i

    s = 2.5_c_double
    line = [(real(i, c_double), i = 1, 7)]
    field = reshape([(real(i, c_double), i = 1, 30)], [5, 6])
    cube = 1.0_c_double

    stat = sum_any_rank_c(s, total, n)
    print '(a, i0, a, f8.2, a, i0)', 'scalar     stat=', stat, ' total=', total, ' count=', n
    stat = sum_any_rank_c(line, total, n)
    print '(a, i0, a, f8.2, a, i0)', 'vector     stat=', stat, ' total=', total, ' count=', n
    stat = sum_any_rank_c(field, total, n)
    print '(a, i0, a, f8.2, a, i0, a, f8.2)', 'matrix     stat=', stat, ' total=', total, ' count=', n, &
        '  sum()=', sum(field)
    stat = sum_any_rank_c(field(2:5:3, ::2), total, n)
    print '(a, i0, a, f8.2, a, i0, a, f8.2)', 'section    stat=', stat, ' total=', total, ' count=', n, &
        '  sum()=', sum(field(2:5:3, ::2))
    stat = sum_any_rank_c(cube, total, n)
    print '(a, i0, a, f8.2, a, i0)', 'rank 3     stat=', stat, ' total=', total, ' count=', n
    stat = sum_any_rank_c(transpose(field), total, n)
    print '(a, i0, a, f8.2, a, i0)', 'transposed stat=', stat, ' total=', total, ' count=', n
end program sum_demo
scalar     stat=0 total=    2.50 count=1
vector     stat=0 total=   28.00 count=7
matrix     stat=0 total=  465.00 count=30  sum()=  465.00
section    stat=0 total=   81.00 count=6  sum()=   81.00
rank 3     stat=0 total=   24.00 count=24
transposed stat=0 total=  465.00 count=30

GNU Fortran 16.2 and Flang 23 print the same lines. The one C function sums a scalar (rank == 0, subscripts omitted), a vector, a matrix, a rank-3 array, a strided section and transpose(field); the last needs no special case, because whatever the compiler passes for the temporary result of transpose, the C function reads the right elements through CFI_address. The count and the sum agree with the Fortran sum of the same array and section. Notice that the Fortran dummy argument has intent(in) and the C parameter const; the descriptor must not be modified for an intent(in) dummy (18.6), and the data must not be changed through it (18.7).

The loop is not fast: CFI_address does a multiply per dimension per element. Its use is the portable baseline; Contiguity shows the fast path for contiguous data. The assumed-rank array is also handled in Fortran with select rank, covered in Array Arguments: Assumed Shape, Size and Rank.

Contiguity

A Fortran array section need not be contiguous, and a C function written with pointer arithmetic (p[i]) is only correct for contiguous data. There are two tools. CFI_is_contiguous(dv) (18.5.5.6) returns 1 if the array described by dv is contiguous and 0 otherwise; the base_addr must not be null, and an assumed-size or allocatable array always gives 1. It lets a C function choose a fast path (a plain loop over base_addr) for contiguous data and the general path (CFI_address, or stepping by sm) otherwise. And the contiguous attribute on the Fortran interface moves the burden to the compiler: when Fortran calls a C procedure whose interface has a dummy argument with the contiguous attribute (or an assumed-length character explicit-shape or assumed-size array) and the actual argument is discontiguous, the processor shall make sure the C procedure receives a descriptor for a contiguous object (18.3.7), that is, a copy.

The next program shows both. The C function deliberately takes the fast path without testing: it reads base_addr with unit stride and prints what the descriptor says. It is exported under two names so that two interface bodies, one with and one without contiguous, can each have their own binding label.

#include <stdio.h>
#include <ISO_Fortran_binding.h>

/* A fast path that is only correct for contiguous data: it walks base_addr with unit stride. */
static double sum_fast(const CFI_cdesc_t *x)
{
    const double *p = x->base_addr;
    double s = 0.0;
    printf("  C: extent=%ld sm=%ld contiguous=%d\n", (long)x->dim[0].extent, (long)x->dim[0].sm,
           CFI_is_contiguous(x));
    fflush(stdout);
    for (CFI_index_t i = 0; i < x->dim[0].extent; i++)
        s += p[i];
    return s;
}

/* Two entry points so that each Fortran interface body has its own binding label. */
double sum_fast_plain(const CFI_cdesc_t *x) { return sum_fast(x); }
double sum_fast_contig(const CFI_cdesc_t *x) { return sum_fast(x); }
program contiguous_c_side
    use, intrinsic :: iso_c_binding, only: c_double
    use, intrinsic :: iso_fortran_env, only: output_unit
    implicit none
    interface
        function sum_fast_plain(x) result(s) bind(c, name='sum_fast_plain')
            import :: c_double
            real(c_double), intent(in) :: x(:)
            real(c_double) :: s
        end function sum_fast_plain
        function sum_fast_contig(x) result(s) bind(c, name='sum_fast_contig')
            import :: c_double
            real(c_double), contiguous, intent(in) :: x(:)
            real(c_double) :: s
        end function sum_fast_contig
    end interface
    real(c_double) :: a(10), s
    integer :: i

    a = [(real(i, c_double), i = 1, 10)]
    print '(a, f6.1)', 'Fortran sum(a(1:10:2)) =', sum(a(1:10:2))
    flush (output_unit)
    s = sum_fast_plain(a(1:10:2))
    print '(a, f6.1)', 'plain interface:      ', s
    flush (output_unit)
    s = sum_fast_contig(a(1:10:2))
    print '(a, f6.1)', 'contiguous interface: ', s
end program contiguous_c_side
Fortran sum(a(1:10:2)) =  25.0
  C: extent=5 sm=16 contiguous=0
plain interface:        15.0
  C: extent=5 sm=8 contiguous=1
contiguous interface:   25.0

GNU Fortran 16.2 and Flang 23 print the same lines. With the plain interface the descriptor of a(1:10:2) has stride 16 bytes and contiguous=0, and the loop that ignores the stride adds the first five elements, 1 to 5, and prints 15.0 instead of 25.0: nothing is diagnosed. With the contiguous interface the compiler copied the section into a temporary, the C function saw stride 8 and contiguous=1, and the answer is right. With -fcheck=all GNU Fortran also writes a run-time warning to the error stream, Fortran runtime warning: An array temporary was created for argument 'x' of procedure 'sum_fast_contig', which is the copy-in. A C function that is meant to accept any layout should not rely on contiguous, because a C caller that builds the descriptor itself may pass a discontiguous object (NOTE 5 after 18.3.7); the cases of C calling Fortran are in C calling Fortran.

Allocatable dummy arguments

An allocatable dummy argument reaches C as a descriptor with attribute equal to CFI_attribute_allocatable. C may allocate and deallocate the object only through CFI_allocate and CFI_deallocate (18.7), which use the same mechanism as the Fortran allocate and deallocate statements; these functions change the allocation status of the Fortran variable the descriptor describes. CFI_allocate(dv, lower_bounds, upper_bounds, elem_len) needs a descriptor with the rank and type already set, base_addr equal to a null pointer and attribute equal to CFI_attribute_allocatable or CFI_attribute_pointer. The lower and upper bounds (arrays of at least rank elements, ignored for a scalar) replace any bounds in the descriptor, and elem_len is used only for a character type, where it overrides the element length; otherwise it is ignored. The element length of a noncharacter type is already in the descriptor. CFI_deallocate(dv) deallocates the object, sets base_addr to a null pointer, and requires an object that was allocated by the allocate mechanism (for a pointer, one that is associated with a target that deallocate could free).

The example allocates the temperature field in C, with lower bounds 1, fills it, and frees it. The interface uses intent(out) for the allocating call: an allocatable dummy with intent(out) is deallocated on entry, so C always sees a null base_addr, which is what CFI_allocate requires.

#include <ISO_Fortran_binding.h>

/* Allocate an nx-by-ny allocatable array of double with lower bounds 1 and
   fill it with a uniform temperature. */
int make_field(CFI_cdesc_t *u, int nx, int ny, double t0)
{
    CFI_index_t lower[2] = {1, 1};
    CFI_index_t upper[2] = {nx, ny};
    int stat = CFI_allocate(u, lower, upper, 0); /* elem_len is ignored for non-character types */
    if (stat != CFI_SUCCESS)
        return stat;
    for (CFI_index_t j = 0; j < ny; j++)
        for (CFI_index_t i = 0; i < nx; i++) {
            CFI_index_t sub[2] = {1 + i, 1 + j};
            *(double *)CFI_address(u, sub) = t0 + 0.5 * (double)(i + j);
        }
    return CFI_SUCCESS;
}

/* Free it again; the descriptor's base_addr becomes NULL. */
int release_field(CFI_cdesc_t *u)
{
    return CFI_deallocate(u);
}
module descr_alloc
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    implicit none
    interface
        function make_field(u, nx, ny, t0) result(stat) bind(c, name='make_field')
            import :: c_double, c_int
            real(c_double), allocatable, intent(out) :: u(:, :)
            integer(c_int), value :: nx, ny
            real(c_double), value :: t0
            integer(c_int) :: stat
        end function make_field
        function release_field(u) result(stat) bind(c, name='release_field')
            import :: c_double, c_int
            real(c_double), allocatable, intent(inout) :: u(:, :)
            integer(c_int) :: stat
        end function release_field
    end interface
end module descr_alloc

program alloc_demo
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    use descr_alloc, only: make_field, release_field
    implicit none
    real(c_double), allocatable :: u(:, :)
    integer(c_int) :: stat

    stat = make_field(u, 4_c_int, 3_c_int, 20.0_c_double)
    print '(a, i0, a, l1)', 'make_field:    stat=', stat, ' allocated=', allocated(u)
    print '(a, 2i3, a, 2i3)', 'shape and lower bounds:', shape(u), '  /', lbound(u)
    print '(a, f6.2, a, f6.2)', 'u(1,1)=', u(1, 1), '  u(4,3)=', u(4, 3)

    stat = release_field(u)
    print '(a, i0, a, l1)', 'release_field: stat=', stat, ' allocated=', allocated(u)
end program alloc_demo
make_field:    stat=0 allocated=T
shape and lower bounds:  4  3  /  1  1
u(1,1)= 20.00  u(4,3)= 22.50
release_field: stat=0 allocated=F

Both compilers print the same lines. On return the Fortran variable is allocated with the shape and lower bounds C supplied, and after release_field it is unallocated again. Two details. The elements are addressed through CFI_address with Fortran subscripts, {1 + i, 1 + j}, because the lower bounds of an allocatable array are those given to CFI_allocate (and not zero, which is the rule only for nonallocatable, nonpointer objects). And the interface of make_field must not use intent(inout) when the actual argument can be unallocated and never defined: with a variable that is passed as unallocated to an intent(inout) allocatable dummy, GNU Fortran 16.2 reports -Wmaybe-uninitialized warnings about the descriptor’s bounds at the call. intent(out) states the true contract, that the callee creates the object.

Calling CFI_allocate on an already-allocated array is an error, CFI_ERROR_BASE_ADDR_NOT_NULL; the section on error codes exercises it. There is no reallocate function: a C function that must grow an array deallocates it and allocates again, copying the elements in between. A character allocatable needs a deferred length (character(kind=c_char, len=:), allocatable), and then the elem_len argument of CFI_allocate sets the length (this case is described by the standard and was not run here).

Pointer dummy arguments and array sections

A pointer dummy argument reaches C as a descriptor with attribute equal to CFI_attribute_pointer. C changes what the pointer points to with CFI_setpointer(result, source, lower_bounds) (18.5.5.9): the pointer described by result becomes associated with the whole of the object described by source (an allocated allocatable, a data pointer or a nonallocatable nonpointer object that is not assumed-size), with the lower bounds replaced by lower_bounds when that is not null, or disassociated when source is a null pointer or a disassociated pointer. The ranks and types of the two descriptors shall be equal. To point at a part of an array, C first builds a descriptor of that part with CFI_section (18.5.5.7) or CFI_select_part (18.5.5.8), then passes it to CFI_setpointer. CFI_allocate also works for a pointer: it allocates a new anonymous target for it.

CFI_section(result, source, lower_bounds, upper_bounds, strides) updates result, which must have been established with the rank of source minus the number of zero strides, the same type and element length, and the attribute CFI_attribute_other or CFI_attribute_pointer (a pointer if result is a formal parameter that corresponds to a Fortran actual argument). It describes the Fortran section source(l1:u1:s1, l2:u2:s2, ...): a null lower_bounds or upper_bounds means the lower or upper bound of that dimension of source, a null strides means 1 for every dimension, and a stride of zero selects the single subscript l (which must equal u) and removes that dimension from the result, so A(:, 42) has a smaller rank than A. The bounds are in the bounds recorded in source, that is, zero-based for a nonallocatable nonpointer object.

The example points a Fortran pointer at the interior window u(2:5, 2:4) of the field from inside C, writes through the pointer from Fortran, and disassociates it again. The dummy argument u has target so that the pointer stays valid after the call (18.8): without target on the dummy, all C pointers into the object, and the Fortran pointer with them, would become undefined on return.

#include <ISO_Fortran_binding.h>

/* Point p at the interior window u(i1:i2, j1:j2) of the whole array u, using
   Fortran (1-based) subscripts. u must have the target attribute in the caller. */
int window(CFI_cdesc_t *p, const CFI_cdesc_t *u, int i1, int i2, int j1, int j2)
{
    CFI_CDESC_T(2) section;
    CFI_cdesc_t *s = (CFI_cdesc_t *)&section;
    CFI_index_t lower[2], upper[2];
    CFI_index_t new_lower[2] = {1, 1};
    CFI_index_t strides[2] = {1, 1};   /* the pointer's own lower bounds */
    int stat;

    /* A descriptor for a nonallocatable array always has lower bounds 0. */
    lower[0] = i1 - 1 + u->dim[0].lower_bound;
    upper[0] = i2 - 1 + u->dim[0].lower_bound;
    lower[1] = j1 - 1 + u->dim[1].lower_bound;
    upper[1] = j2 - 1 + u->dim[1].lower_bound;

    stat = CFI_establish(s, NULL, CFI_attribute_other, CFI_type_double, sizeof(double), 2, NULL);
    if (stat != CFI_SUCCESS)
        return stat;
    stat = CFI_section(s, u, lower, upper, strides);   /* see below: pass strides explicitly */
    if (stat != CFI_SUCCESS)
        return stat;
    return CFI_setpointer(p, s, new_lower);          /* p now describes the section */
}

/* Disassociate the pointer. */
int unlink_window(CFI_cdesc_t *p)
{
    return CFI_setpointer(p, NULL, NULL);
}
module descr_window
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    implicit none
    interface
        function window(p, u, i1, i2, j1, j2) result(stat) bind(c, name='window')
            import :: c_double, c_int
            real(c_double), pointer, intent(out) :: p(:, :)
            real(c_double), target, intent(in) :: u(:, :)
            integer(c_int), value :: i1, i2, j1, j2
            integer(c_int) :: stat
        end function window
        function unlink_window(p) result(stat) bind(c, name='unlink_window')
            import :: c_double, c_int
            real(c_double), pointer, intent(inout) :: p(:, :)
            integer(c_int) :: stat
        end function unlink_window
    end interface
end module descr_window

program window_demo
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    use descr_window, only: window, unlink_window
    implicit none
    integer, parameter :: nx = 6, ny = 5
    real(c_double), target :: u(nx, ny)
    real(c_double), pointer :: p(:, :)
    integer(c_int) :: stat

    u = 0.0_c_double
    p => null()
    stat = window(p, u, 2_c_int, 5_c_int, 2_c_int, 4_c_int)
    print '(a, i0, a, l1)', 'window:        stat=', stat, ' associated=', associated(p)
    print '(a, 2i3, a, 2i3, a, 2i3)', 'shape:', shape(p), '  lbound:', lbound(p), '  ubound:', ubound(p)

    p = 100.0_c_double                  ! writes through the pointer into u
    print '(a, 6f6.1)', 'u(:, 1) =', u(:, 1)
    print '(a, 6f6.1)', 'u(:, 2) =', u(:, 2)
    print '(a, 6f6.1)', 'u(:, 5) =', u(:, 5)
    u(2, 2) = -1.0_c_double
    print '(a, f6.1)', 'p(1, 1) after u(2, 2) = -1:', p(1, 1)

    stat = unlink_window(p)
    print '(a, i0, a, l1)', 'unlink_window: stat=', stat, ' associated=', associated(p)
end program window_demo
window:        stat=0 associated=T
shape:  4  3  lbound:  1  1  ubound:  4  3
u(:, 1) =   0.0   0.0   0.0   0.0   0.0   0.0
u(:, 2) =   0.0 100.0 100.0 100.0 100.0   0.0
u(:, 5) =   0.0   0.0   0.0   0.0   0.0   0.0
p(1, 1) after u(2, 2) = -1:  -1.0
unlink_window: stat=0 associated=F

GNU Fortran 16.2 and Flang 23 print the same lines. The pointer’s lower bounds are 1 because window passed {1, 1} to CFI_setpointer; with a null lower_bounds they would be the lower bounds recorded in the section descriptor. p = 100.0_c_double is an ordinary Fortran assignment to the window: it changed rows 2 to 5 of columns 2 to 4 of u, and the pointer element p(1, 1) is u(2, 2). The dummy argument p is intent(out): C must not read its base_addr before giving it a value (18.6).

CFI_section with a null strides argument

CFI_section accepts a null pointer for any of lower_bounds, upper_bounds and strides (18.5.5.7). The program below calls it with all eight combinations of "null or not" in a child process (fork), so that a crash is reported instead of ending the run.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <ISO_Fortran_binding.h>
void probe(const CFI_cdesc_t *u)
{
    for (int mode = 0; mode < 8; mode++) {
        fflush(stdout);
        pid_t pid = fork();
        if (pid == 0) {
            CFI_CDESC_T(2) sec; CFI_cdesc_t *s = (CFI_cdesc_t *)&sec;
            CFI_index_t lo[2] = {1, 1}, up[2] = {2, 3}, st[2] = {1, 1};
            CFI_establish(s, NULL, CFI_attribute_other, CFI_type_double, 8, 2, NULL);
            int r = CFI_section(s, u, (mode & 1) ? lo : NULL, (mode & 2) ? up : NULL, (mode & 4) ? st : NULL);
            printf("lower=%d upper=%d strides=%d -> %d extents %ld %ld\n", mode & 1, !!(mode & 2), !!(mode & 4), r, (long)s->dim[0].extent, (long)s->dim[1].extent);
            fflush(stdout);
            _exit(0);
        }
        int status; waitpid(pid, &status, 0);
        if (!WIFEXITED(status)) printf("lower=%d upper=%d strides=%d -> crashed (signal %d)\n", mode & 1, !!(mode & 2), !!(mode & 4), WTERMSIG(status));
    }
}
program t
use, intrinsic :: iso_c_binding, only: c_double
implicit none
interface
subroutine probe(u) bind(c, name='probe')
  import :: c_double
  real(c_double), intent(in) :: u(:,:)
end subroutine
end interface
real(c_double) :: u(4,5)
u = 0
call probe(u)
end program

Output with GNU Fortran 16.2 (standard output only):

lower=0 upper=0 strides=0 -> crashed (signal 11)
lower=1 upper=0 strides=0 -> crashed (signal 11)
lower=0 upper=1 strides=0 -> crashed (signal 11)
lower=1 upper=1 strides=0 -> crashed (signal 11)
lower=0 upper=0 strides=1 -> 0 extents 4 5
lower=1 upper=0 strides=1 -> 0 extents 3 4
lower=0 upper=1 strides=1 -> 0 extents 3 4
lower=1 upper=1 strides=1 -> 0 extents 2 3

Output with Flang 23:

lower=0 upper=0 strides=0 -> 0 extents 4 5
lower=1 upper=0 strides=0 -> 0 extents 3 4
lower=0 upper=1 strides=0 -> 0 extents 3 4
lower=1 upper=1 strides=0 -> 0 extents 2 3
lower=0 upper=0 strides=1 -> 0 extents 4 5
lower=1 upper=0 strides=1 -> 0 extents 3 4
lower=0 upper=1 strides=1 -> 0 extents 3 4
lower=1 upper=1 strides=1 -> 0 extents 2 3

This is a real difference between the compilers, and a defect of the GNU Fortran 16.2 runtime as installed for this page: whenever strides is a null pointer, CFI_section ends with a segmentation fault (the signal 11 above; the Fortran runtime also writes its usual backtrace to the error stream), although the standard allows it and says it means stride 1. Passing an explicit array of ones works with every combination of the other two arguments. Flang 23 accepts every combination. Portable code therefore always passes strides explicitly, which is what example 04_pointer_window.c does.

Selecting a part of every element

CFI_select_part(result, source, displacement, elem_len) (18.5.5.8) describes an array in which every element is a part of the corresponding element of source: a component of a structure, a substring of a character element, or the real or imaginary part of a complex element. The result must be an established descriptor with the same rank as source and the type (and for a character part, the element length) of the part; displacement is the byte offset of the part within an element, from offsetof for a structure, with displacement plus the size of the part at most source->elem_len, and the address at that offset must be suitably aligned for the part’s type. elem_len matters only for character parts. The source must be an allocated allocatable array, an associated array pointer, or a nonallocatable, nonpointer array that is not assumed-size. The result keeps the strides of the source, so the part array has the memory stride of the whole elements.

The example is a set of heatflow probes, an interoperable derived type with an id and a temperature t, and a C function that scales just the temperature of every probe in a strided section, leaving the ids alone.

#include <stddef.h>
#include <ISO_Fortran_binding.h>

typedef struct {
    int id;
    double t;
} probe_t;

/* Scale the temperature component of every probe by "factor" without touching the ids:
   describe the array of the t components and write through it. Returns the sum of the new values. */
int scale_probes(const CFI_cdesc_t *probes, double factor, double *total)
{
    CFI_CDESC_T(1) comp;
    CFI_cdesc_t *c = (CFI_cdesc_t *)&comp;
    CFI_index_t extent[1];
    int stat;

    if (probes->type != CFI_type_struct)
        return CFI_INVALID_TYPE;
    extent[0] = probes->dim[0].extent;
    stat = CFI_establish(c, NULL, CFI_attribute_other, CFI_type_double, sizeof(double), 1, extent);
    if (stat != CFI_SUCCESS)
        return stat;
    stat = CFI_select_part(c, probes, offsetof(probe_t, t), 0);
    if (stat != CFI_SUCCESS)
        return stat;
    *total = 0.0;
    for (CFI_index_t i = 0; i < c->dim[0].extent; i++) {
        CFI_index_t sub[1] = {c->dim[0].lower_bound + i};
        double *t = CFI_address(c, sub);
        *t *= factor;
        *total += *t;
    }
    return CFI_SUCCESS;
}
module descr_probes
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    implicit none
    type, bind(c) :: probe_t
        integer(c_int) :: id
        real(c_double) :: t
    end type probe_t
    interface
        function scale_probes(probes, factor, total) result(stat) bind(c, name='scale_probes')
            import :: probe_t, c_double, c_int
            type(probe_t), intent(inout) :: probes(:)
            real(c_double), value :: factor
            real(c_double), intent(out) :: total
            integer(c_int) :: stat
        end function scale_probes
    end interface
end module descr_probes

program probes_demo
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    use descr_probes, only: probe_t, scale_probes
    implicit none
    type(probe_t) :: probes(6)
    real(c_double) :: total
    integer(c_int) :: stat
    integer :: i

    probes = [(probe_t(id=i, t=10.0_c_double * i), i = 1, 6)]
    stat = scale_probes(probes(2:6:2), 0.5_c_double, total)
    print '(a, i0, a, f6.1)', 'stat=', stat, '  total of the new values=', total
    print '(a, 6i5)', 'ids:', probes%id
    print '(a, 6f5.1)', 't:  ', probes%t
end program probes_demo
stat=0  total of the new values=  60.0
ids:    1    2    3    4    5    6
t:   10.0 10.0 30.0 20.0 50.0 30.0

Both compilers print the same lines. The array probes(2:6:2) is passed as a descriptor with type CFI_type_struct (which the C function checks) and a stride of two probes. CFI_select_part gives a descriptor of the t components with the same extent and stride, so the loop touches elements 2, 4 and 6 only; the ids are unchanged, and the sum of the three new temperatures is 20 + 40 + 60 halved, that is 60. The C structure probe_t has the same layout as the Fortran bind(c) type, which is what interoperability of the two types requires (18.3.4).

Assumed-length character dummies

A nonallocatable, nonpointer character(*) dummy argument of a bind(c) interface arrives as a descriptor of rank 0 (a scalar), type CFI_type_char for kind=c_char, and elem_len equal to the length times the bytes of one character (for c_char that is the length). The string is not null terminated; elem_len is the only length. This is the descriptor-based alternative to passing character(kind=c_char), dimension(*) plus a length, covered in Characters and Strings.

#include <stdio.h>
#include <ISO_Fortran_binding.h>

/* A Fortran character(len=*) dummy arrives as a rank-0 descriptor whose elem_len is the length. */
int show_name(const CFI_cdesc_t *s)
{
    if (s->type != CFI_type_char)
        return CFI_INVALID_TYPE;
    printf("rank=%d elem_len=%zu text=[%.*s]\n", (int)s->rank, s->elem_len, (int)s->elem_len,
           (const char *)s->base_addr);
    fflush(stdout);
    return CFI_SUCCESS;
}
program character_descriptor
    use, intrinsic :: iso_c_binding, only: c_char, c_int
    implicit none
    interface
        function show_name(s) result(stat) bind(c, name='show_name')
            import :: c_char, c_int
            character(kind=c_char, len=*), intent(in) :: s
            integer(c_int) :: stat
        end function show_name
    end interface
    character(kind=c_char, len=20) :: name = 'heatflow'
    integer(c_int) :: stat

    stat = show_name(name)
    stat = show_name(trim(name))
    stat = show_name(name(1:4))
end program character_descriptor
rank=0 elem_len=20 text=[heatflow            ]
rank=0 elem_len=8 text=[heatflow]
rank=0 elem_len=4 text=[heat]

Both compilers print the same lines. The length is the declared length for name (20, with trailing blanks), the length of the expression for trim(name) (8) and for the substring name(1:4) (4); the C function must print with %.*s and elem_len, since there is no terminator. CFI_select_part can select a substring of each element of a character array in the same way as a structure component; elem_len then gives the length of the substring.

C calling Fortran

When C calls a bind(c) Fortran procedure that has an assumed-shape, assumed-rank, allocatable, pointer or assumed-length character dummy argument, the C code must build the descriptor. The recipe has two steps:

  1. declare storage with CFI_CDESC_T(rank) and cast its address to CFI_cdesc_t *;

  2. initialise it with CFI_establish (a whole object) and, for a part, update it with CFI_section or CFI_select_part.

CFI_establish(dv, base_addr, attribute, type, elem_len, rank, extents) (18.5.5.5) takes a data object dv large enough for the rank, which shall not be the same object as a C formal parameter that corresponds to a Fortran actual argument, nor one that describes an allocated allocatable. base_addr is a null pointer or the address of a suitably aligned storage sequence. If it is not null, the object it describes is a scalar or a contiguous array, extents (rank elements, not negative) gives its shape, and, for an array pointer, the lower bounds are zero. elem_len matters only for CFI_type_struct, CFI_type_other and character types, where it shall be greater than zero and the storage size of an element. Three cases cover what C needs to build:

  • a C array or scalar: attribute is CFI_attribute_other, base_addr the address, extents the shape in Fortran dimension order;

  • an unallocated allocatable: attribute is CFI_attribute_allocatable and base_addr is null; Fortran can allocate it;

  • a descriptor to be filled in later: attribute is CFI_attribute_other and base_addr null, as the result argument of CFI_section or CFI_select_part.

The mapping from a C array to a Fortran one is the point where the languages disagree on order: C’s a[j][i] has i varying fastest, which is Fortran’s u(i + 1, j + 1), so the extents are given as {nx, ny} for double a[ny][nx], and the Fortran dimension order is the reverse of the C declaration order.

The example builds a descriptor for a C temperature array and passes it to a Fortran assumed-shape procedure, passes a strided section of it, and has Fortran allocate an array into a C-owned allocatable descriptor, which C reads and frees. The Fortran module is compiled separately and linked with the C main.

module field_ops
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    use, intrinsic :: iso_fortran_env, only: output_unit
    implicit none
contains
    ! Assumed-shape dummy: C must pass the address of a descriptor.
    subroutine scale_field(u, factor) bind(c, name='scale_field')
        real(c_double), intent(inout) :: u(:, :)
        real(c_double), value :: factor
        print '(a, 2i3, a, 2i3)', '  Fortran sees shape', shape(u), '  lbound', lbound(u)
        u = factor * u
        flush (output_unit)
    end subroutine scale_field

    ! Allocatable dummy: Fortran allocates, C owns the descriptor.
    subroutine fill_field(u, nx, ny) bind(c, name='fill_field')
        real(c_double), allocatable, intent(out) :: u(:, :)
        integer(c_int), value :: nx, ny
        integer :: i, j
        allocate (u(0:nx - 1, 0:ny - 1))
        do j = 0, ny - 1
            do i = 0, nx - 1
                u(i, j) = real(10 * i + j, c_double)
            end do
        end do
        print '(a, 2i3, a, 2i3)', '  Fortran allocated shape', shape(u), '  lbound', lbound(u)
        flush (output_unit)
    end subroutine fill_field
end module field_ops
#include <stdio.h>
#include <ISO_Fortran_binding.h>

void scale_field(const CFI_cdesc_t *u, double factor);   /* Fortran: u is intent(inout), so the descriptor is read-only */
void fill_field(CFI_cdesc_t *u, int nx, int ny);

int main(void)
{
    /* A C array: a[j][i] has i varying fastest, which is Fortran's u(i+1, j+1). */
    double a[3][4];
    for (int j = 0; j < 3; j++)
        for (int i = 0; i < 4; i++)
            a[j][i] = 10.0 * (i + 1) + (j + 1);

    /* 1. Describe a C array so that a Fortran assumed-shape dummy can receive it. */
    CFI_CDESC_T(2) whole;
    CFI_cdesc_t *w = (CFI_cdesc_t *)&whole;
    CFI_index_t extents[2] = {4, 3};                       /* extents in Fortran order: i, then j */
    int stat = CFI_establish(w, a, CFI_attribute_other, CFI_type_double, sizeof(double), 2, extents);
    printf("establish: stat=%d contiguous=%d sm=(%ld,%ld)\n", stat, CFI_is_contiguous(w),
           (long)w->dim[0].sm, (long)w->dim[1].sm);
    fflush(stdout);
    scale_field(w, 2.0);
    printf("a[0][0]=%.1f a[2][3]=%.1f\n", a[0][0], a[2][3]);

    /* 2. A strided section: every second column (j = 0 and 2), rows i = 1..3. */
    CFI_CDESC_T(2) part;
    CFI_cdesc_t *p = (CFI_cdesc_t *)&part;
    CFI_index_t lower[2] = {1, 0}, upper[2] = {3, 2}, strides[2] = {1, 2};
    stat = CFI_establish(p, NULL, CFI_attribute_other, CFI_type_double, sizeof(double), 2, NULL);
    stat |= CFI_section(p, w, lower, upper, strides);
    printf("section: stat=%d extents=(%ld,%ld) contiguous=%d\n", stat, (long)p->dim[0].extent,
           (long)p->dim[1].extent, CFI_is_contiguous(p));
    fflush(stdout);
    scale_field(p, 0.5);
    printf("a[0][0]=%.1f a[0][1]=%.1f a[1][1]=%.1f\n", a[0][0], a[0][1], a[1][1]);

    /* 3. An unallocated allocatable: Fortran allocates it, C reads it and frees it. */
    CFI_CDESC_T(2) field;
    CFI_cdesc_t *f = (CFI_cdesc_t *)&field;
    stat = CFI_establish(f, NULL, CFI_attribute_allocatable, CFI_type_double, sizeof(double), 2, NULL);
    printf("allocatable before: stat=%d base_addr=%s\n", stat, f->base_addr == NULL ? "NULL" : "set");
    fflush(stdout);
    fill_field(f, 3, 2);
    CFI_index_t sub[2] = {2, 1};
    printf("allocatable after: lower=(%ld,%ld) extent=(%ld,%ld) u(2,1)=%.1f\n", (long)f->dim[0].lower_bound,
           (long)f->dim[1].lower_bound, (long)f->dim[0].extent, (long)f->dim[1].extent,
           *(double *)CFI_address(f, sub));
    stat = CFI_deallocate(f);
    printf("deallocate: stat=%d base_addr=%s\n", stat, f->base_addr == NULL ? "NULL" : "set");
    return 0;
}
establish: stat=0 contiguous=1 sm=(8,32)
  Fortran sees shape  4  3  lbound  1  1
a[0][0]=22.0 a[2][3]=86.0
section: stat=0 extents=(3,2) contiguous=0
  Fortran sees shape  3  2  lbound  1  1
a[0][0]=22.0 a[0][1]=21.0 a[1][1]=44.0
allocatable before: stat=0 base_addr=NULL
  Fortran allocated shape  3  2  lbound  0  0
allocatable after: lower=(0,0) extent=(3,2) u(2,1)=21.0
deallocate: stat=0 base_addr=NULL

GNU Fortran 16.2 and Flang 23 print the same lines. The established whole-array descriptor has byte strides 8 and 32 and is contiguous; Fortran sees an assumed-shape dummy with shape 4 by 3 and lower bounds 1 (an assumed-shape dummy always has lower bound 1 in the callee, whatever the descriptor says) and doubles the field in place, so a[2][3] is 86 = 2 * 43. The section of rows 1 to 3 and columns 0 and 2 (zero-based) has extents 3 and 2 and is not contiguous; Fortran scales the six selected elements by 0.5 and the others are left alone (a[0][1] back to 21, a[1][1] still 44). The allocatable descriptor is established with a null base address, Fortran allocates it with bounds 0 to 2 and 0 to 1 (the descriptor reports lower bounds 0 and extents 3 and 2, and u(2,1) is read at the subscripts it was given), and C frees it with CFI_deallocate, which sets base_addr back to null. The rule that makes this program legal and not just working is the lifetime rule of 18.8: the lifetime of a descriptor that C established for an allocatable object must not end while the object is still allocated, so C deallocates before main returns.

The two C descriptors that C passes to Fortran in this program are for dummy arguments with intent(inout): C must not modify them while Fortran has them (18.6), and it does not. scale_field is declared in C with const CFI_cdesc_t *: the descriptor of a nonallocatable object is read-only for the callee anyway.

The contiguous attribute when C is the caller

When C calls a Fortran procedure whose dummy argument is contiguous (or an assumed-length character explicit-shape or assumed-size array) and the descriptor describes a discontiguous object, the Fortran processor shall handle the difference in contiguity (18.3.7): the Fortran procedure must see a contiguous array. The next pair of files tests it.

module contig_side
    use, intrinsic :: iso_c_binding, only: c_double, c_int
    use, intrinsic :: iso_fortran_env, only: output_unit
    implicit none
contains
    ! contiguous: the dummy argument is a contiguous array whatever the descriptor says
    function sum_contig(x) result(s) bind(c, name='sum_contig')
        real(c_double), contiguous, intent(in) :: x(:)
        real(c_double) :: s
        print '(a, l1, a, i0)', '  Fortran: is_contiguous=', is_contiguous(x), ' size=', size(x)
        flush (output_unit)
        s = sum(x)
    end function sum_contig

    ! no contiguous attribute: the layout is passed through unchanged
    function sum_any(x) result(s) bind(c, name='sum_any')
        real(c_double), intent(in) :: x(:)
        real(c_double) :: s
        print '(a, l1, a, i0)', '  Fortran: is_contiguous=', is_contiguous(x), ' size=', size(x)
        flush (output_unit)
        s = sum(x)
    end function sum_any
end module contig_side
#include <stdio.h>
#include <ISO_Fortran_binding.h>

double sum_contig(const CFI_cdesc_t *x);
double sum_any(const CFI_cdesc_t *x);

int main(void)
{
    double a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    CFI_CDESC_T(1) whole, every_other;
    CFI_cdesc_t *w = (CFI_cdesc_t *)&whole, *e = (CFI_cdesc_t *)&every_other;
    CFI_index_t extent[1] = {10}, lower[1] = {0}, upper[1] = {9}, stride[1] = {2};

    CFI_establish(w, a, CFI_attribute_other, CFI_type_double, sizeof(double), 1, extent);
    CFI_establish(e, NULL, CFI_attribute_other, CFI_type_double, sizeof(double), 1, NULL);
    CFI_section(e, w, lower, upper, stride);          /* a(1:10:2): 1 3 5 7 9 */
    printf("descriptor: extent=%ld sm=%ld contiguous=%d\n", (long)e->dim[0].extent, (long)e->dim[0].sm,
           CFI_is_contiguous(e));
    fflush(stdout);
    printf("sum_any    = %.1f\n", sum_any(e));
    fflush(stdout);
    printf("sum_contig = %.1f\n", sum_contig(e));
    return 0;
}

Output with GNU Fortran 16.2:

descriptor: extent=5 sm=16 contiguous=0
  Fortran: is_contiguous=F size=5
sum_any    = 25.0
  Fortran: is_contiguous=T size=5
sum_contig = 25.0

Output with Flang 23:

descriptor: extent=5 sm=16 contiguous=0
  Fortran: is_contiguous=F size=5
sum_any    = 25.0
  Fortran: is_contiguous=T size=5
sum_contig = 15.0

GNU Fortran 16.2 copies the five selected elements, sees is_contiguous true and sums 1 + 3 + 5 + 7 + 9 = 25, as the standard requires. Flang 23 (LLVM 23.1.4) reports is_contiguous true for the contiguous dummy argument but reads the memory as if the stride were one element, and the sum is 15.0 (1 + 2 + 3 + 4 + 5): the wrong answer, with no diagnostic. The non-contiguous procedure sum_any, which makes no such claim, is correct in both, so this is a defect in Flang’s handling of C-built descriptors on entry to a contiguous dummy, not in the descriptor. Until it is fixed, a Fortran procedure that C may call with a discontiguous descriptor should take an assumed-shape dummy without contiguous and, when it needs contiguous memory, test is_contiguous(x) and copy x itself.

Error codes

Table 18.5 defines the error codes returned by the functions; CFI_SUCCESS is the integer constant zero and every other macro is nonzero and distinct. A processor may detect other errors and signal them with other values.

Macro Error condition

CFI_SUCCESS

no error detected

CFI_ERROR_BASE_ADDR_NULL

the base_addr of a descriptor is null in a context that requires a non-null value

CFI_ERROR_BASE_ADDR_NOT_NULL

the base_addr of a descriptor is not null in a context that requires null

CFI_INVALID_ELEM_LEN

the element length supplied for a descriptor is not valid

CFI_INVALID_RANK

the rank supplied for a descriptor is not valid

CFI_INVALID_TYPE

the type supplied for a descriptor is not valid

CFI_INVALID_ATTRIBUTE

the attribute supplied for a descriptor is not valid

CFI_INVALID_EXTENT

the extent supplied in a CFI_dim_t is not valid

CFI_INVALID_DESCRIPTOR

a descriptor is invalid in some way

CFI_ERROR_MEM_ALLOCATION

memory allocation failed

CFI_ERROR_OUT_OF_BOUNDS

a reference is out of bounds

GNU Fortran’s header adds two codes of its own, CFI_FAILURE (1) and CFI_INVALID_STRIDE (9), which are not in the standard; Flang’s has none beyond the table. The numbers differ completely between the two headers, as the next program shows. It calls the functions with arguments that are wrong in a way each error code is for, and prints the code with its macro name. The C function is called from a Fortran main program, which matters for GNU Fortran (see below).

#include <stdio.h>
#include <ISO_Fortran_binding.h>

static const char *error_name(int e)
{
    if (e == CFI_SUCCESS) return "CFI_SUCCESS";
    if (e == CFI_ERROR_BASE_ADDR_NULL) return "CFI_ERROR_BASE_ADDR_NULL";
    if (e == CFI_ERROR_BASE_ADDR_NOT_NULL) return "CFI_ERROR_BASE_ADDR_NOT_NULL";
    if (e == CFI_INVALID_ELEM_LEN) return "CFI_INVALID_ELEM_LEN";
    if (e == CFI_INVALID_RANK) return "CFI_INVALID_RANK";
    if (e == CFI_INVALID_TYPE) return "CFI_INVALID_TYPE";
    if (e == CFI_INVALID_ATTRIBUTE) return "CFI_INVALID_ATTRIBUTE";
    if (e == CFI_INVALID_EXTENT) return "CFI_INVALID_EXTENT";
    if (e == CFI_INVALID_DESCRIPTOR) return "CFI_INVALID_DESCRIPTOR";
    if (e == CFI_ERROR_MEM_ALLOCATION) return "CFI_ERROR_MEM_ALLOCATION";
    if (e == CFI_ERROR_OUT_OF_BOUNDS) return "CFI_ERROR_OUT_OF_BOUNDS";
    return "(not a standard code)";
}

static void report(const char *what, int e)
{
    printf("%-52s -> %2d %s\n", what, e, error_name(e));
    fflush(stdout);
}

void run_errors(void)
{
    CFI_CDESC_T(CFI_MAX_RANK) store;
    CFI_cdesc_t *d = (CFI_cdesc_t *)&store;
    CFI_CDESC_T(2) res;
    CFI_cdesc_t *r = (CFI_cdesc_t *)&res;
    double x[6] = {0};
    CFI_index_t ext[2] = {2, 3}, neg[2] = {2, -3};

    printf("CFI_VERSION=%d CFI_MAX_RANK=%d\n", CFI_VERSION, CFI_MAX_RANK);
    printf("attributes: pointer=%d allocatable=%d other=%d\n", CFI_attribute_pointer,
           CFI_attribute_allocatable, CFI_attribute_other);
    printf("types: int=%ld long=%ld float=%ld double=%ld char=%ld cptr=%ld struct=%ld other=%ld\n",
           (long)CFI_type_int, (long)CFI_type_long, (long)CFI_type_float, (long)CFI_type_double,
           (long)CFI_type_char, (long)CFI_type_cptr, (long)CFI_type_struct, (long)CFI_type_other);

    report("establish, rank CFI_MAX_RANK+1",
           CFI_establish(d, x, CFI_attribute_other, CFI_type_double, 8, CFI_MAX_RANK + 1, ext));
    report("establish, attribute 99",
           CFI_establish(d, x, 99, CFI_type_double, 8, 2, ext));
    report("establish, type -100",
           CFI_establish(d, x, CFI_attribute_other, (CFI_type_t)-100, 8, 2, ext));
    report("establish, struct with elem_len 0",
           CFI_establish(d, x, CFI_attribute_other, CFI_type_struct, 0, 2, ext));
    report("establish, extent -3",
           CFI_establish(d, x, CFI_attribute_other, CFI_type_double, 8, 2, neg));
    report("establish, allocatable with a base address",
           CFI_establish(d, x, CFI_attribute_allocatable, CFI_type_double, 8, 2, ext));

    report("establish, unallocated allocatable",
           CFI_establish(d, NULL, CFI_attribute_allocatable, CFI_type_double, 8, 2, NULL));
    {
        CFI_index_t lo[2] = {1, 1}, up[2] = {2, 3};
        report("allocate, 2x3", CFI_allocate(d, lo, up, 0));
        report("allocate again, base address is not NULL", CFI_allocate(d, lo, up, 0));
    }
    report("deallocate", CFI_deallocate(d));
    report("deallocate again, base address is NULL", CFI_deallocate(d));
    {
        CFI_index_t lo[2] = {1, 1}, up[2] = {1000000000000000L, 1000000000000000L};
        report("allocate, absurd extents", CFI_allocate(d, lo, up, 0));
    }

    CFI_establish(d, x, CFI_attribute_other, CFI_type_double, 8, 2, ext);
    CFI_establish(r, NULL, CFI_attribute_other, CFI_type_double, 8, 2, NULL);
    {
        CFI_index_t lo[2] = {0, 0}, up[2] = {1, 3}, st[2] = {1, 1};
        report("section, upper bound 3 on an extent-3 dimension", CFI_section(r, d, lo, up, st));
        up[1] = 2;
        report("section, in bounds", CFI_section(r, d, lo, up, st));
    }
    report("setpointer, result is not a pointer", CFI_setpointer(r, d, NULL));

}
program errors_demo
    implicit none
    interface
        subroutine run_errors() bind(c, name='run_errors')
        end subroutine run_errors
    end interface
    call run_errors()
end program errors_demo

Output with GNU Fortran 16.2 (standard output):

CFI_VERSION=1 CFI_MAX_RANK=15
attributes: pointer=0 allocatable=1 other=2
types: int=1025 long=2049 float=1027 double=2051 char=261 cptr=7 struct=6 other=-1
establish, rank CFI_MAX_RANK+1                       ->  5 CFI_INVALID_RANK
establish, attribute 99                              ->  0 CFI_SUCCESS
establish, type -100                                 ->  6 CFI_INVALID_TYPE
establish, struct with elem_len 0                    ->  4 CFI_INVALID_ELEM_LEN
establish, extent -3                                 ->  8 CFI_INVALID_EXTENT
establish, allocatable with a base address           ->  7 CFI_INVALID_ATTRIBUTE
establish, unallocated allocatable                   ->  0 CFI_SUCCESS
allocate, 2x3                                        ->  0 CFI_SUCCESS
allocate again, base address is not NULL             ->  3 CFI_ERROR_BASE_ADDR_NOT_NULL
deallocate                                           ->  0 CFI_SUCCESS
deallocate again, base address is NULL               ->  2 CFI_ERROR_BASE_ADDR_NULL
allocate, absurd extents                             -> 11 CFI_ERROR_MEM_ALLOCATION
section, upper bound 3 on an extent-3 dimension      -> 12 CFI_ERROR_OUT_OF_BOUNDS
section, in bounds                                   ->  0 CFI_SUCCESS
setpointer, result is not a pointer                  ->  7 CFI_INVALID_ATTRIBUTE

The same program with Flang 23 reports the same macro names with different numbers, and differs in three results:

Call GNU Fortran code GNU Fortran macro Flang code Flang macro

establish, rank CFI_MAX_RANK+1

5

CFI_INVALID_RANK

14

CFI_INVALID_RANK

establish, attribute 99

0

CFI_SUCCESS

16

CFI_INVALID_ATTRIBUTE

establish, type -100

6

CFI_INVALID_TYPE

15

CFI_INVALID_TYPE

establish, struct with elem_len 0

4

CFI_INVALID_ELEM_LEN

13

CFI_INVALID_ELEM_LEN

establish, extent -3

8

CFI_INVALID_EXTENT

0

CFI_SUCCESS

establish, allocatable with a base address

7

CFI_INVALID_ATTRIBUTE

12

CFI_ERROR_BASE_ADDR_NOT_NULL

establish, unallocated allocatable

0

CFI_SUCCESS

0

CFI_SUCCESS

allocate, 2x3

0

CFI_SUCCESS

0

CFI_SUCCESS

allocate again, base address is not NULL

3

CFI_ERROR_BASE_ADDR_NOT_NULL

12

CFI_ERROR_BASE_ADDR_NOT_NULL

deallocate

0

CFI_SUCCESS

0

CFI_SUCCESS

deallocate again, base address is NULL

2

CFI_ERROR_BASE_ADDR_NULL

11

CFI_ERROR_BASE_ADDR_NULL

allocate, absurd extents

11

CFI_ERROR_MEM_ALLOCATION

19

CFI_ERROR_MEM_ALLOCATION

section, upper bound 3 on an extent-3 dimension

12

CFI_ERROR_OUT_OF_BOUNDS

20

CFI_ERROR_OUT_OF_BOUNDS

section, in bounds

0

CFI_SUCCESS

0

CFI_SUCCESS

setpointer, result is not a pointer

7

CFI_INVALID_ATTRIBUTE

16

CFI_INVALID_ATTRIBUTE

Which error is reported when a call has more than one is processor dependent (18.5.5.1), so a portable program tests for CFI_SUCCESS and treats any other value as failure, and uses the macros only to report. The three differences visible here are of that kind: GNU Fortran accepts the invalid attribute value 99 (it does not validate it), reports an allocatable descriptor with a base address as CFI_INVALID_ATTRIBUTE where Flang reports CFI_ERROR_BASE_ADDR_NOT_NULL, and Flang does not reject a negative extent where GNU Fortran does. Neither compiler reports CFI_INVALID_DESCRIPTOR here, since nothing in the program makes a descriptor invalid in a way the library checks.

GNU Fortran checks arguments only with -fcheck=bounds

The GNU Fortran library validates the arguments of the CFI_ functions only when the program was compiled with -fcheck=bounds (which -fcheck=all includes) when compiling the Fortran main program. With checks on, each failing call also prints a message on the error stream, which the program above produced:

CFI_establish: Rank must be between 0 and 15, 0 < rank (0 !< 16).
CFI_establish: Invalid type (type = -100).
CFI_establish: The supplied elem_len must be greater than zero.
CFI_establish: Extents must be nonnegative (extents[1] = -3).
CFI_establish: If base address is not NULL, the established C descriptor must be for a nonallocatable entity.
CFI_allocate: Base address of C descriptor must be NULL.
CFI_deallocate: Base address is already NULL.
CFI_allocate: Failure in memory allocation.
CFI_section: Upper bounds must be within the bounds of the Fortran array (source->dim[1].lower_bound <= upper_bounds[1] <= source->dim[1].lower_bound + source->dim[1].extent - 1, 0 !<= 3 !<= 2).
CFI_setpointer: Result shall be the address of a C descriptor for a Fortran pointer.

When the same program is linked without -fcheck=bounds the library does no validation, a call such as CFI_establish with a rank above CFI_MAX_RANK reports CFI_SUCCESS, and the run of 08_errors ended with a segmentation fault in the following call. With a C main and no Fortran main program, as in the example of the section on C calling Fortran, the options are never set and the library does no validation either. Flang returns the error codes without printing messages and does not depend on a compile option. The practical advice is to test the error indicator of every call, to build debug versions of Fortran main programs with -fcheck=bounds while developing C code that builds descriptors, and to treat a segmentation fault inside a CFI_ function as an invalid argument until shown otherwise.

Restrictions and lifetimes

The standard collects the rules that C must obey in 18.6 to 18.8. Violating them is undefined behaviour in C, and nothing detects it:

  • Descriptors are changed only by the library (18.6). A C descriptor shall not be initialised, updated or copied other than by the functions of 18.5.5, so never assign a CFI_cdesc_t or copy it with memcpy, and never write base_addr, dim or the other members.

  • Descriptors received are read-only in some cases (18.6). If the address of a descriptor is a formal parameter that corresponds to a Fortran actual argument (or a C actual argument that corresponds to a Fortran dummy argument), it shall not be modified when the corresponding dummy argument has intent(in) or when the descriptor is for a nonallocatable nonpointer object. Modification includes establishing or updating it. An allocatable or pointer dummy argument without intent(in) is the case where C may change the descriptor, through the library functions.

  • A pointer intent(out) descriptor is undefined on entry (18.6). Its base_addr shall not be accessed before it is given a value.

  • An assumed-shape dummy takes no assumed-size descriptor (18.6). A descriptor for an assumed-size array shall not be passed as a C actual argument to an assumed-shape dummy.

  • Allocation only through CFI_allocate and CFI_deallocate (18.7). Not malloc or free for an allocatable object, and a Fortran pointer can become associated with a target by CFI_allocate.

  • Data intents are real (18.7). For a nonpointer dummy argument in a bind(c) interface, an object whose address is a formal parameter shall not be defined or become undefined by C when the dummy has intent(in), and shall not be referenced before it is defined when the dummy has intent(out).

  • Stay inside the object (18.7). A pointer derived from base_addr shall not be used to reach memory that is not part of the object the descriptor describes.

  • Lifetimes (18.8). A descriptor whose address is a formal parameter becomes undefined on return from the call; if the dummy argument does not have the target or asynchronous attribute, all C pointers into the object become undefined on return as well, so do not keep them in C. A descriptor that C passed to Fortran must live until the call returns. If the lifetime of a descriptor C established for an allocatable object ends before the program exits, the object shall be unallocated by then. A Fortran pointer associated with an object defined by C (by CFI_setpointer or CFI_establish) becomes undefined when the C object’s lifetime ends.

Building the examples

All examples were built inside a container with GNU Fortran 16.2, GCC 16.2 and Flang 23.1.4. For a program with a Fortran main (examples 01 to 06, 08, 10 and 11), the C file is compiled to an object file and linked by the Fortran driver, which supplies the Fortran runtime:

gcc -std=c11 -Wall -Wextra -pedantic -c 01_describe.c -o describe.o
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all 01_describe.f90 describe.o -o describe

gcc -std=c11 -Wall -Wextra -pedantic -fPIC -I/usr/lib/llvm-23/include/flang -c 01_describe.c -o describe.o
flang-23 01_describe.f90 describe.o -o describe

The -fPIC for the Flang build is needed because Flang links position-independent executables by default and an object compiled by this GCC is not; it is not part of the descriptor interface. With the header of Flang on the include path gcc finds the Flang ISO_Fortran_binding.h first, so a program is built against the header of the compiler whose runtime it links; never mix the header of one compiler with the library of the other.

When main is in C (examples 07 and 09), compile the Fortran file to an object file and link with the C driver, naming the Fortran runtime library: -lgfortran for GNU Fortran. Flang’s driver insists on its own Fortran main, so the C-main examples were linked with gcc against Flang’s runtime archive instead (libflang_rt.runtime.a in /usr/lib/llvm-23/lib/clang/23/lib/aarch64-unknown-linux-gnu/, plus -lm -lstdc++); the location is the installation’s own, so read it from flang-23 -### or the vendor documentation of your build. The trailing-underscore and name-mangling problems of old Fortran-to-C calls do not arise: bind(c, name='...') fixes the symbol, and CMake’s FortranCInterface is only for legacy code. See Calling Fortran from C, C++ and Python for linking order, runtime libraries and CMake.

Standard version provenance and compiler support

Feature Introduced Support checked on 2026-10-18

iso_c_binding, bind(c), c_ptr, interoperable scalars and explicit-shape arrays

Fortran 2003

all compilers (see Interoperability with C)

C descriptors and ISO_Fortran_binding.h; assumed-shape, assumed-rank, allocatable, pointer and assumed-length character dummies in bind(c) interfaces

ISO/IEC TS 29113:2012, part of Fortran 2018

GNU Fortran 16.2 and Flang 23 each ship the header and run every example

type(*) and dimension(..) dummy arguments

TS 29113, Fortran 2018

both accept the examples

CFI_section with a null strides argument

as the standard (18.5.5.7)

GNU Fortran 16.2 crashes, Flang 23 accepts

contiguous dummy receiving a discontiguous C descriptor

as the standard (18.3.7)

GNU Fortran 16.2 correct, Flang 23 gives a wrong result

Validation of arguments of CFI_ functions

processor dependent (18.5.5.1)

GNU Fortran 16.2 only with -fcheck=bounds, Flang 23 always

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.

Where the books are outdated

The Fortran book consulted for this section (Modern Fortran Explained, which devotes a chapter to C descriptors) explains the design of the descriptor and the functions of the header correctly, because the standard text it follows is the same. Where the page you are reading goes further:

Area What the book does not cover, and the current approach

Compiler status

the book is written for the standard and does not say how compilers behave; on 2026-10-18 GNU Fortran 16.2 and Flang 23 both implement the header, but with different values for every macro, a different CFI_VERSION, a crash in CFI_section with a null strides argument (GNU Fortran) and a wrong result for a contiguous dummy with a C-built discontiguous descriptor (Flang)

Argument checking

the book states which errors the functions report, not that checking depends on the compiler; with GNU Fortran it needs -fcheck=bounds in the Fortran main program

Build and link

no compiler options, header locations or linking commands; see Building the examples

Standard identity

the book follows the 2018 and 2023 text; the references below cite the Fortran 2023 final draft J3/23-007r1 by clause

Pitfalls

  • Using numeric values of CFI_ macros. Attribute, type and error codes differ between GNU Fortran and Flang; compare with the macro names.

  • Walking base_addr with p[i] on an assumed-shape array. Wrong for a section (a stride other than one element); use CFI_address, step by sm in bytes, or insist on CFI_is_contiguous or the contiguous attribute.

  • Assuming lower bounds. They are zero for a nonallocatable nonpointer dummy, and the bounds given to CFI_allocate or CFI_setpointer for allocatables and pointers.

  • Counting dimensions in C order. Extents and subscripts are in Fortran order, the reverse of a C array declaration.

  • Declaring CFI_cdesc_t by value. It has a flexible array member; use CFI_CDESC_T(rank) and a cast.

  • Writing a descriptor that C did not establish. Assigning, copying or editing members is undefined; use the functions.

  • Forgetting to cast the CFI_CDESC_T variable to CFI_cdesc_t * in calls.

  • Allocating or freeing an allocatable object with malloc or free. Use CFI_allocate and CFI_deallocate.

  • Keeping C pointers into an object after the call when the dummy has no target or asynchronous attribute.

  • Calling CFI_allocate on an allocated array (error CFI_ERROR_BASE_ADDR_NOT_NULL) or CFI_deallocate on an unallocated one (CFI_ERROR_BASE_ADDR_NULL); test the returned indicator.

  • Passing NULL strides to CFI_section with GNU Fortran 16.2. It crashes; pass an array of ones.

  • Linking a program built against one compiler’s header with the other compiler’s runtime.

  • Relying on contiguous for descriptors built by C with Flang 23. Test is_contiguous and copy in Fortran.

  • Treating a character descriptor as null terminated. Use elem_len.

  • Compiling without -fcheck=bounds and wondering why a bad CFI_establish call does not report. GNU Fortran does not validate the arguments without it.