Search code examples
cfortranfortran-iso-c-binding

ISO_C_BINDING, calling C from Fortran


I have a C code (I am not the owner of said code) that generates the following structures

struct Vector
{
  int size;
  double data[3];
};

struct VerticesVect
{
  int size;
  struct Vector* data;
};

And so on : there are a lot of additional constructs (typically structures of the previously defined structure).

I need to call said C code/library from a Fortran code. The Fortran code will use the previously C-generated structures. The Fortran code won't write / edit them in any way, it just needs them as input.

I am trying to figure out a way to declare these types in Fortran so that I can use them.

use, intrinsic :: ISO_C_BINDING

followed by

type, bind(c) :: Vector
          integer(kind=C_INT32_T) :: size
          real(kind=C_DOUBLE) :: data(3)
      end type Vector

is obviously fine.

  type, bind(c) :: VerticesVect
      integer(kind=c_int) :: size
      type(Vector), dimension(:) :: data
  end type VerticesVect

is not valid though ("data must have an explicit shape"), nor

  type, bind(c) :: VerticesVect
      integer(kind=c_int) :: size
      type(Vector), pointer :: data
  end type VerticesVect

("Component 'data' cannot have the POINTER attribute because it is a member of the BIND(C) derived type 'verticesvect'")

nor seems to be valid any combination of allocatable, pointer, positioning of (:) that I could think of.

Unfotunately, specifying a static size such that it is big enough to cover all cases is not an acceptable solution, as the "compounded" maximum sizes of all subsequent constructions end up leading to an unreasonably large total object.

Is there a way to write these declarations such that the compiler accepts it and everything maps correctly ? Can/should i use the c_ptr type ? Can/should I get rid of the bind(c) ?

Thanks for your help


Solution

  • This ...

          type(Vector), dimension(:) :: data
    

    ... attempts to declare member data as an assumed-shape array. This is wrong in the C sense that C pointers are not arrays. It is also wrong in the C-binding sense that interoperable defined types are explicitly forbidden from having members that are assumed-size or assumed-shape arrays.

    This ...

          type(Vector), pointer :: data
    

    ... seems more logical, but it is also incorrect because Fortran pointers and C pointers, though similar in concept, are not wholly analogous. The C-binding maps C pointers to type type(c_ptr). There are some caveats wrapped around that, but we don't need to dig into those here.

    Thus, the way to define an interoperable user-defined type corresponding to a C structure with a pointer member is to use type(c_ptr) for the corresponding member on the Fortran side and to give the type the bind(c) attribute:

      type, bind(c) :: VerticesVect
          integer(kind=c_int) :: size
          type(c_ptr) :: data
      end type VerticesVect
    

    Additionally, you will want to be able to access the Vector objects from the Fortran side. For that, you will probably want to engage ISO_C_BINDING's c_f_pointer() function. This is the main interoperable bridge between C pointers and Fortran pointers. For example:

        type(VerticesVect) :: vertices
    
        ! ... assign an appropriate value to 'vertices' ...
    
        type(Vector), dimension(:), pointer :: vectors
        call c_f_pointer(vertices%data, vectors, (/vertices%size/))
    
        ! ... use 'vectors' to access the data ...
    

    Although you say you need only read access, both read and write access should work fine.


    Addendum: complete example

    The following demo compiles successfully and works as expected for me:

    main.f90

    program vectors
        use, intrinsic :: iso_c_binding
    
        implicit none
    
        type, bind(c) :: Vector
            integer(kind=c_int) :: size
            real(kind=c_double) :: data(3)
        end type Vector
    
        type, bind(c) :: VerticesVect
            integer(kind=c_int) :: size
            type(c_ptr) :: data
        end type VerticesVect
    
        interface
            function generate3DVertices() bind(C, name='generate3DVertices')
                use, intrinsic :: iso_c_binding
                type(c_ptr) :: generate3DVertices
            end function generate3DVertices
        end interface
    
        type(VerticesVect), pointer :: vertices
        type(c_ptr) verticesPtr
    
        verticesPtr = generate3DVertices()
        call c_f_pointer(verticesPtr, vertices)
        call write_vectors(vertices)
    
    contains
    
        subroutine write_vectors(vertices)
            type(VerticesVect), intent(in) :: vertices
            type(Vector), dimension(:), pointer :: vectors
            integer :: i
    
            call c_f_pointer(vertices%data, vectors, (/vertices%size/))
            write(6, *) 'Fortran vectors:'
            do i = 1, vertices%size
                write(6, '(3f10.6)') vectors(i)%data
            end do
        end subroutine write_vectors
    
    end program
    

    vect.c

    #include <stdlib.h>
    #include <stdio.h>
    #include <time.h>
    
    #define NUM_VERTICES 3
    
    struct Vector {
      int size;
      double data[3];
    };
    
    struct VerticesVect {
      int size;
      struct Vector* data;
    };
    
    struct VerticesVect *generate3DVertices(void) {
        struct VerticesVect *vertices = malloc(sizeof *vertices);
    
        if (!vertices) {
            fprintf(stderr, "Memory allocation failed\n");
            exit(1);
        }
    
        vertices->size = NUM_VERTICES;
        vertices->data = calloc(NUM_VERTICES, sizeof *vertices->data);
        if (!vertices->data) {
            fprintf(stderr, "Memory allocation failed\n");
            exit(1);
        }
    
        puts(" C vectors:");
        srand(time(NULL));
        for (int i = 0; i < NUM_VERTICES; i++) {
            for (int j = 0; j < 3; j++) {
                vertices->data[i].data[j] = rand() / (double) RAND_MAX;
            }
            printf("%10.6f%10.6f%10.6f\n",
                   vertices->data[i].data[0], vertices->data[i].data[1], vertices->data[i].data[2]);
        }
        putchar('\n');
        fflush(stdout);
    
        return vertices;
    }
    

    Makefile

    OBJ = main.o vect.o
    
    FC = gfortran
    
    all: demo
    
    demo: $(OBJ)
        $(FC) $(LDFLAGS) -o $@ $^
    
    %.o: %.f90
        $(FC) $(FCFLAGS) -c -o $@ $<
    

    The program calls calls a C function to generate random vectors and print C's interpretation of them, then prints the Fortran interpretation of the same. When I run it, the vectors printed by C are identical to the ones printed by Fortran (to the 6-digit precision used on both sides).