Return an array of strings from fortran to c++

Yeah interoperability has its limits. I wonder if one could make a conforming CFI_CDESC_T which would use a smart pointer under the hood to prevent memory leaks via reference-counting.

I was reading A Tour of C++ (Third edition, C++20) and noticed a pattern for object destruction by passing a lambda to a class. With a heap-allocated C pointer it might look as follows:

double *x = malloc( n*sizeof(double) );
auto action = finally([=](){ free(x); }); // (so many brackets...)

The idea is to invoke the lambda via the destructor of a small helper class:

// Adapted from gsl-lite.h (https://github.com/gsl-lite/gsl-lite)
//
// (see also C.30 in C++ Core Guidelines, 
//     https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)
// 
template<typename Action>
class final_action
{
public:
    final_action( Action action )
    : action_( action ) {}

    ~final_action()
    {
        action_();
    }

private:
    Action action_;
};

template< class Fn >
[[nodiscard]] auto finally( Fn const & f )
{
    return final_action(( f ));
}

You can adapt the concept to a Fortran allocatable array:

void foo(int n)
{
    CFI_CDESC_T(2) samples_;
    const auto samples = (CFI_cdesc_t *) &samples_;

    CFI_establish( /* ... */ );

    // Make sure we don't forget to deallocate
    auto dealloc = finally([&]{
        if (samples->base_addr) {
            CHECK_CFI( CFI_deallocate(samples) )
        }
    });

    bar(samples); // Allocation in Fortran, samples is intent(out)

    // ...

} // dealloc called here

For fun I decided to implement a complete example which approximates the value of π by dart throwing (Monte-Carlo). The Fortran version is very dense:

! sampling.f90
!
module sampling
   use, intrinsic :: iso_c_binding, only: &
      ip => c_int, dp => c_double
   implicit none
   private
contains
   ! Draw random samples in the unit square [0,1)^2
   subroutine draw_random_samples(n,samples) bind(c)
      integer(ip), intent(in), value :: n
      real(dp), allocatable, intent(out) :: samples(:,:)
      allocate(samples(n,2))
      call random_number(samples)
   end subroutine
   ! Estimate pi (3.14159...) by dart throwing
   function f_estimate_pi(n) bind(c) result(pi)
      integer(ip), intent(in), value :: n
      real(dp) :: pi
      real(dp), allocatable :: samples(:,:)
      integer :: ncirc
      call draw_random_samples(n,samples)
      associate(x => samples(:,1), y => samples(:,2))
         pi = 4 * real(count(x**2 + y**2 < 1, dim=1), dp) / n
      end associate
   end function
end module

In the C++ version I reused the sampling procedure. For the the counting part I decided to use ranges and views for the first time (requires C++20):

/* Calculate pi using the Monte-Carlo method.
 *
 * Random numbers are generated in Fortran just for the 
 * sake of testing the F2018 enhanced C interoperability.
 */
double estimate_pi(int n)
{
    CFI_CDESC_T(2) samples_;
    const auto samples = (CFI_cdesc_t *) &samples_;

    CHECK_CFI( CFI_establish(samples,
                             NULL,
                             CFI_attribute_allocatable,
                             CFI_type_double,
                             0 /* ignored */,
                             (CFI_rank_t) 2,
                             NULL /* ignored */) )

    // Make sure we don't forget to deallocate
    auto dealloc = finally([&]{
        if (samples->base_addr) {
            CHECK_CFI( CFI_deallocate(samples) )
        }
    });

    draw_random_samples( n, samples); 

    auto inside_of_circle = [=](int i){
        // <!> Pointer arithmetic <!>
        const double x = *( (double *) samples->base_addr + i);
        const double y = *( (double *) samples->base_addr + i + n);
        return x*x + y*y < 1;
    };

    using std::ranges::count_if;
    using std::views::iota;

    int ncircle = count_if( iota( 0, n-1 ), inside_of_circle );

    return 4.0 * ((double) ncircle) / n;

} // dealloc called here

The full example is given below:

Makefile
FC  = gfortran
CXX = g++

FCFLAGS  = -Wall -O2 -march=native -std=f2018
CXXFLAGS = -Wall -O2 -march=native -std=c++20
 
LDFLAGS  = -lgfortran

picalc: picalc.cpp sampling.o
	$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)

sampling.o sampling.mod: sampling.f90
	$(FC) $(FCFLAGS) -c $<

.phony: clean

clean:
	rm *.o *.mod picalc
sampling.f90
! sampling.f90
!
module sampling
   use, intrinsic :: iso_c_binding, only: &
      ip => c_int, dp => c_double
   implicit none
   private
contains
   ! Draw random samples in the unit square [0,1)^2
   subroutine draw_random_samples(n,samples) bind(c)
      integer(ip), intent(in), value :: n
      real(dp), allocatable, intent(out) :: samples(:,:)
      allocate(samples(n,2))
      call random_number(samples)
   end subroutine
   ! Estimate pi (3.14159...) by dart throwing
   function f_estimate_pi(n) bind(c) result(pi)
      integer(ip), intent(in), value :: n
      real(dp) :: pi
      real(dp), allocatable :: samples(:,:)
      call draw_random_samples(n,samples)
      associate(x => samples(:,1), y => samples(:,2))
         pi = 4 * real(count(x**2 + y**2 < 1, dim=1), dp) / n
      end associate
   end function
end module

picalc.cpp

#include <iostream>
#include <cstdio>
#include <cstdlib>

#include <span>
#include <ranges>
#include <algorithm>

#include <ISO_Fortran_binding.h>

static const char *cfi_errstrs[12] = {
    "No error detected.\n",
    "The base address member of a C descriptor is a null pointer in a context that requires a non-null pointer value.\n",
    "The base address member of a C descriptor is not a null pointer in a context that requires a null pointer value.\n",
    "The value supplied for the element length member of a C descriptor is not valid.\n",
    "The value supplied for the rank member of a C descriptor is not valid.\n",
    "The value supplied for the type member of a C descriptor is not valid.\n",
    "The value supplied for the attribute member of a C descriptor is not valid.\n",
    "The value supplied for the extent member of a CFI_dim_t structure is not valid.\n",
    "A C descriptor is invalid in some way.\n",
    "Memory allocation failed.\n",
    "A reference is out of bounds.\n",
    "Unrecognized status code.\n"
};

// Returns the description string for an error code.
//
const char* cfiGetErrorString(int stat) {

    switch (stat) {
        case CFI_SUCCESS:                  return cfi_errstrs[0]  ; break; 
        case CFI_ERROR_BASE_ADDR_NULL:     return cfi_errstrs[1]  ; break;
        case CFI_ERROR_BASE_ADDR_NOT_NULL: return cfi_errstrs[2]  ; break;
        case CFI_INVALID_ELEM_LEN:         return cfi_errstrs[3]  ; break;
        case CFI_INVALID_RANK:             return cfi_errstrs[4]  ; break;
        case CFI_INVALID_TYPE:             return cfi_errstrs[5]  ; break;
        case CFI_INVALID_ATTRIBUTE:        return cfi_errstrs[6]  ; break;
        case CFI_INVALID_EXTENT:           return cfi_errstrs[7]  ; break;
        case CFI_INVALID_DESCRIPTOR:       return cfi_errstrs[8]  ; break;
        case CFI_ERROR_MEM_ALLOCATION:     return cfi_errstrs[9]  ; break;
        case CFI_ERROR_OUT_OF_BOUNDS:      return cfi_errstrs[10] ; break;
    }

    return cfi_errstrs[11];
}

#define CHECK_CFI(func)                                                        \
{                                                                              \
    int stat = (func);                                                         \
    if (stat != CFI_SUCCESS) {                                                 \
        fprintf(stderr,"%s:%d: CFI API failed with error: (%d) %s",            \
            __FILE__, __LINE__, stat, cfiGetErrorString(stat));                \
    }                                                                          \
}                                                                              \

template<typename Action>
class final_action
{
public:
    final_action( Action action )
    : action_( action ) {}

    ~final_action()
    {
        action_();
    }

private:
    Action action_;
};

template< class Fn >
[[nodiscard]] auto finally( Fn const & f )
{
    return final_action(( f ));
}

// 
namespace stdv = std::views;
namespace stdr = std::ranges;

// Draws random samples in the unit square [0,1)^2
//
// Arguments:
// [in]        n: the number of samples
// [out] samples: an array of (x,y) values, shape [n,2]
//
extern "C" void draw_random_samples(int n, CFI_cdesc_t *samples);

// Same procedure as the one below, but implemented in Fortran
extern "C" double f_estimate_pi(int n);

/* Calculate pi using the Monte-Carlo method.
 *
 * Random numbers are generated in Fortran just for the 
 * sake of testing the F2018 enhanced C interoperability.
 */
double estimate_pi(int n)
{
    CFI_CDESC_T(2) samples_;
    const auto samples = (CFI_cdesc_t *) &samples_;

    CHECK_CFI( CFI_establish(samples,
                             NULL,
                             CFI_attribute_allocatable,
                             CFI_type_double,
                             0 /* ignored */,
                             (CFI_rank_t) 2,
                             NULL /* ignored */) )

    // Make sure we don't forget to deallocate
    auto dealloc = finally([&]{
        if (samples->base_addr) {
            CHECK_CFI( CFI_deallocate(samples) )
        }
    });

    draw_random_samples( n, samples); 

    auto inside_of_circle = [=](int i)
    {
        // <!> Pointer arithmetic <!>
     
        const double x = *( (double *) samples->base_addr + i);
        const double y = *( (double *) samples->base_addr + i + n);
        
        return x*x + y*y < 1;
    };

#if 0 
    //
    // Old-fashioned approach
    //
    int ncircle = 0;
    for (int i = 0; i < n; ++i) {
        if (inside_of_circle(i)) ncircle++;
    }    
#else
    //
    // Modern approach with views and ranges
    //
    using std::ranges::count_if;
    using std::views::iota;

    int ncircle = count_if( iota(0,n-1), inside_of_circle );
#endif

    return 4.0 * ((double) ncircle) / n;

} // dealloc called here

int main(int argc, char const *argv[])
{
    if (argc != 2) {
        std::cout << "Usage: ./picalc N\n";
        return 1;
    }

    const int N = atoi(argv[1]);

    std::cout << "pi = " <<   estimate_pi( N ) << '\n';
    std::cout << "pi = " << f_estimate_pi( N ) << '\n';

    return 0;
}
2 Likes