Home > Blockchain >  Calling a fortran function with a derived type containing assumed size arrays
Calling a fortran function with a derived type containing assumed size arrays

Time:09-27

I would like to write functions or subprograms that return a derived type containing arrays with a size that is unknown at compile time. The rank is known. I would like to call these from C. One method on the fortran side is perhaps to use a parameterised derived type. Can I call this from C? Is there another preferred method? Can I make this a function rather than a subroutine? My fortran code so far is given below:

module dt

 use iso_c_binding
 implicit none
 
 type, bind(c) :: results(n)
    integer(c_int), len :: n
    real(c_double), dimension(n) :: myarray
 end type results

contains

 subroutine set_results(res, n)
   use iso_c_binding
   integer(c_int) :: n
   integer(c_int) :: i
   type(results(n)), intent(out) :: res
   do i=1,n
      res%myarray(i) = i
   end do
 end subroutine set_results
 
end module dt


program use_dt

 use dt
 implicit none

 type(results(10)) :: myresults

 call set_results(myresults, 10)
 print *,myresults
 
end program use_dt

CodePudding user response:

A solution is to declare the arrays as type(c_ptr) in the Fortran derived type, and convert them to Fortran pointers with the c_f_pointer() routine. AFAIK the allocation has to be performed on the C side.

module dt
 use iso_c_binding
 implicit none
 
 type, bind(c) :: results
    integer(c_int) :: n
    type(c_ptr) :: myarray
 end type results

contains

 subroutine set_results(res) bind(c)
   use iso_c_binding
   integer :: i
   type(results), intent(out) :: res
   
   real(c_double), pointer :: myarrayf(:)
   
   call c_f_pointer(res%myarray,myarrayf,[res%n])
   do i=1,res%n
      myarrayf(i) = i
   end do
 end subroutine set_results
 
end module dt

The tedious part is that you have to call c_f_pointer() for each individual array in the derived type.

CodePudding user response:

Let's assume for the time being that you can allocate storage of the correct size in the calling program (the C desktop application). In other words all dimensions are known before entering the Fortran subprogram.

For interoperable types, the easiest option is to use assumed-size arrays, meaning array dimensions are need to be passed explicitly (or are assumed to be known constants):

module interop
use, intrinsic :: iso_c_binding, only: &
   rp => c_double, &
   ip => c_int
implicit none
contains
   !> Set results in an assumed-size array
   !> void set_results_v1(int n, double res[]);
   subroutine set_results_v1(n,res) bind(c)
      integer(ip), value :: n
      real(rp), intent(out) :: res(*)  ! <- contiguous storage
      integer(ip) :: i
      do i = 1, n
         res(i) = i
      end do
   end subroutine
end module

For a 2D array of size n × m, your procedure interface might look like this:

! void fill_random_2d(int n, int m, int lda, double *a)       // in C/C  
! void fill_random_2d(int n, int m, int lda, double a[][lda]) // in C99
   subroutine fill_random_2d(n,m,lda,a) bind(c)
      integer(ip), value :: n, m, lda
      real(rp), intent(inout) :: a(lda,*) !  
      call random_number(a(1:n,1:m))
   end subroutine

The integer lda can be used to account for any array padding. With 2D arrays you need to be careful with differences due to opposite storage order, i.e. Fortran (column-major) vs C/C (row-major). Arguably, it's easier to assume column-oriented storage in C and do multi-dimensional indexing manually if necessary.


The newer, but potentially more powerful option is to the use the C descriptors introduced in Fortran 2018. Descriptors can be used to create Fortran objects in C/C which have no direct equivalent. Descriptors allow you to interoperate also with assumed-shape, allocatable, and pointer arrays.

Using an assumed-shape array, your routine is written as

   !> Set results using assumed-shape array 
   !> (Fortran 2018 C-descriptor needed for interoperation)
   subroutine set_results_v2(res) bind(c)
      real(rp), intent(out) :: res(:) ! <- contiguous or strided storage
      integer :: i
      do i = 1, size(res)
         res(i) = i
      end do
   end subroutine

Here's how the calling the Fortran procedure will look like in C :

// For GCC compile using
//  g   -Wall main.cpp interop.o -lgfortran
// where interop.o contains the external routine.
#include <vector>
#include <iostream>

#include <ISO_Fortran_binding.h>

/**
 * Fortran routine prototypes  
 */ 
extern "C" {
void set_results_v2(CFI_cdesc_t *res);
}


int main(int argc, char const *argv[])
{
    const int N = 10;

    std::vector<double> myresult(N);
    
    CFI_CDESC_T(1) res_desc; /* Rank-1 array descriptor */

    // The C descriptor is an opaque struct that stores
    // information about a Fortran objects which have
    // no proper equivalent in C. Such structs can be 
    // used as dummy arguments in inter-language calls. 

    // The descriptor can only be modified by functions provided
    // in the header "ISO_Fortran_binding.h". These functions
    // expect a pointer to the array descriptor.

    auto res = reinterpret_cast<CFI_cdesc_t *>(&res_desc);

    // First we establish a local array of extents
    // In this case we are wrapping a std::vector, which
    // can be seen as a 1-dimensional array.

    CFI_index_t res_extents[1] = { N };

    // Now we establish the C-descriptor.
    // In this case `res` will be a rank-1 array
    // of type double referring to the data
    // in the std::vector `myresult`.

    int err = CFI_establish(
            res,                  /* pointer to the descriptor */
            myresult.data(),      /* pointer to the data */
            CFI_attribute_other,  /* attribute */
            CFI_type_double,      /* type of  */
            sizeof(double),       /* size of element */
            1,                    /* rank */
            res_extents);         /* array extents */

    // In case something went wrong, a non-zero
    // value is returned, which can be used for
    // error-checking

    if (err != CFI_SUCCESS) 
        return 1;

    // Now we have a complete descriptor which
    // can be passed to a Fortran routine.

    set_results_v2(res);

    for(auto v : myresult) {
        std::cout << v << ' ';
    }
    std::cout << '\n';

    return 0;
}

If you'd like to pass a 2-dimensional assumed-shape array you just need to adjust the rank-constant (replace 1 with 2), and adjust the extents, e.g.

    CFI_index_t res_extents[2] = { N, M };

It is also possible to pass array slices, provide custom array bounds, and more. To learn more about this approach I would recommend reading Metcalf, Reid, & Cohen (2018), Modern Fortran Explained, Oxford University Press, pgs. 397-421.

  • Related