Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass arrays of strings from both C and Fortran to Fortran?

I am trying to pass an array of strings from C to a Fortran subroutine as well as from Fortran to that same Fortran subroutine. I have managed to pass single strings (i.e. 1D character arrays) successfully from both C and Fortran. However, I'm having trouble with arrays of strings. I am using ISO C binding on the Fortran side, and ideally I'd like this to be as seamless as possible on the calling side.

I have read some related questions and answers. Some, (i.e. this and this) are simply "Use ISO C" without further details, which doesn't help much. This answer was very helpful (similar answer to a different question), but only works for single strings, where it seems that the c_null_char is recognized in the single Fortran string. I can't figure out what to do for the array case without having two separate routines.

What I currently have is a C routine which I want to pass the array of strings (string) from:

#include <iostream>

extern "C" void print_hi_array(char input_string[][255]);

using namespace std;

int main() {

  char string[3][255] = {"asdf","ghji","zxcv"};   
  print_hi_array(string);

  return 0;
}

And, a similar Fortran routine:

program main
  implicit none
  call print_hi_array( (/"asdf", "ghji", "zxcv"/) )
end program

Thus far, this is what I have for the receiving end:

subroutine print_hi_array(input_string) bind(C)
  use iso_c_binding, only: C_CHAR, c_null_char

  implicit none

  character (kind=c_char, len=1), dimension (3,255), intent (in) :: input_string
  character (len=255), dimension (3) :: regular_string
  character (len=255) :: dummy_string
  integer :: i,j,k

  write (*,*) input_string

  do j = 1 , 3
    dummy_string(:) = c_null_char
    k = 1
    do i = 1 + (j-1)*255, j*255,1
      if (input_string(i) .ne.  c_null_char) then
        write (*,*) "i ",i,j, input_string(i)
        dummy_string(k:k) = input_string(i)
      endif
    k = k +1
    enddo
    regular_string(j) = dummy_string
  enddo

  write (*,*) regular_string

end subroutine print_hi_array

This works for the C function; I get this output:

 asdfghjizxcv
 j=           1
 i            1           1 a
 i            2           1 s
 i            3           1 d
 i            4           1 f
 j=           2
 i          256           2 g
 i          257           2 h
 i          258           2 j
 i          259           2 i
 j=           3
 i          511           3 z
 i          512           3 x
 i          513           3 c
 i          514           3 v
 asdf   ghji   zxcv   

However, when it's done through Fortran I get nonsense out:

asdfghjizxcv@O,B�@(P,B�]B]6(P,B�@ .......

It seems there is no c_null_char in this approach.

So, how can I write a Fortran subroutine to take in arrays of strings from both C and Fortran?

like image 704
Eli Lansey Avatar asked Dec 13 '12 22:12

Eli Lansey


2 Answers

Fortran uses spaces to fill the rest of the string if it is declared longer than its stored text. It is not zero delimited, the declared length is stored in a hidden variable. It does not contain c null char and therefore you are reading some garbage (buffer overflow). What Fortran should print when tlit prints a string with \000 is undefined by the standard and depends on the implementation.

In particular, you are also passing a character(4) array with dimension 3 to a subroutine that expects much more data (255 chars, though I am not shure about the index order). Only pointers are passed so I think it can not be checked.

It is possible to define the length of the strings in the array constructor this way:

[character(255) :: "a","ab","abc"]
like image 196
Vladimir F Героям слава Avatar answered Sep 30 '22 02:09

Vladimir F Героям слава


I see actually two ways to do that. Either, you write a loop in C and pass the strings one by one to Fortran, as you already did it before. Alternatively, if you want to pass the entire array and you want to handle the Fortran and the C arrays with the same routine, you will have to make an appropriate copy of your C-string array. Below a working, but not too much tested example:

extern "C" void print_array_c(int nstring, char input_string[][255]);

using namespace std;

int main() {

  char string[3][255] = {"asdf","ghji","zxcv"};    
  print_array_c(3, string);

  return 0;
}

Please note that I also pass the number of the strings, so that the example can handle arrays with various sizes. (The length of the strings is, however, assumed to be 255 characters.) On the Fortran size, one would need a routine to convert it Fortran strings. One possible visualization could be:

module arrayprint_module
  use, intrinsic :: iso_c_binding
  implicit none

  integer, parameter :: STRLEN = 255

contains

  !> The printing routine, works with Fortran character arrays only.
  subroutine print_array(strings)
    character(len=STRLEN), intent(in) :: strings(:)

    integer :: ii

    do ii = 1, size(strings)
      write(*,*) ii, strings(ii)
    end do

  end subroutine print_array


  !> Converts C string array to Fortran string array and invokes print_array.
  subroutine print_array_c(nstring, cptr) bind(C)
    integer(c_int), value :: nstring
    type(c_ptr), intent(in), value :: cptr

    character(kind=c_char), pointer :: fptr(:,:)
    character(STRLEN), allocatable :: fstrings(:)
    integer :: ii, lenstr

    call c_f_pointer(cptr, fptr, [ STRLEN, nstring ])
    allocate(fstrings(nstring))
    do ii = 1, nstring
      lenstr = cstrlen(fptr(:,ii))
      fstrings(ii) = transfer(fptr(1:lenstr,ii), fstrings(ii))
    end do
    call print_array(fstrings)

  end subroutine print_array_c


  !> Calculates the length of a C string.
  function cstrlen(carray) result(res)
    character(kind=c_char), intent(in) :: carray(:)
    integer :: res

    integer :: ii

    do ii = 1, size(carray)
      if (carray(ii) == c_null_char) then
        res = ii - 1
        return
      end if
    end do
    res = ii

  end function cstrlen


end module arrayprint_module

Please note, that the array you pass from C must be contigous for this to work and I assumed that the character(kind=c_char) is compatible with the fortran character type, which usually it should be.

like image 29
Bálint Aradi Avatar answered Sep 30 '22 02:09

Bálint Aradi