arrayscstringfortranfortran-iso-c-binding

Pass dynamically allocated array of strings from C to Fortran


I am trying to pass a dynamically allocated array of strings (char ***str) from C to Fortran but I seem to be missing something when it comes to dereferencing the C pointer in Fortran thus yielding garbage strings as output (see MWE below).

Side Question

Is calling deallocate(fptr) enough to stop the program below from leaking memory (I would think not)? If not would it be possible to free msgs (in C) from Fortran?

To Reproduce

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void str_alloc(char*** msgs, size_t * n) {
  *n = 3;
  int str_len = 10;
  (*msgs) = malloc(*n * sizeof(char*));
  for (int i = 0; i < *n; i++) {
    (*msgs)[i] = malloc((str_len + 1) * sizeof(char));
    strcpy((*msgs)[i], "012346789");
  }
}
program main
    use iso_c_binding
    implicit none
    interface
        subroutine str_alloc(msgs, n) bind(C, name="str_alloc")
            use iso_c_binding
            type(c_ptr), intent(out) :: msgs
            integer(c_size_t), intent(out) :: n
        end subroutine
    end interface

    integer, parameter :: STRLEN = 10
    character(kind=c_char, len=STRLEN), allocatable :: names(:)

    call str_array_from_c(names)

    contains
    subroutine str_array_from_c(strs)
        character(len=STRLEN), allocatable, intent(out) :: strs(:)

        type(c_ptr) :: cptr
        integer(c_size_t) :: i, n, lenstr
        character(kind=c_char, len=1), pointer :: fptr(:,:)

        call str_alloc(cptr, n)
        call c_f_pointer(cptr, fptr, [int(STRLEN, kind=c_size_t), n])
        allocate(strs(n))
        print*, "Output C-str array from fortran"
        do i = 1_c_size_t, n
            lenstr = cstrlen(fptr(:, i))
            strs(i) = transfer(fptr(1:lenstr,i), strs(i))
            print*, fptr(1:STRLEN, i), "|", strs(i)
        end do
        deallocate(fptr)
    end subroutine str_array_from_c

    !> Calculates the length of a C string.
    function cstrlen(carray) result(res)
        character(kind=c_char, len=1), intent(in) :: carray(:)
        integer :: res
        integer :: i
        do i = 1, size(carray)
          if (carray(i) == c_null_char) then
            res = i - 1
            return
          end if
        end do
        res = i
    end function cstrlen

end program main

Compile

gcc -c -g -Wall -o cfile.o cfile.c
gfortran -g -Wall -o main.o cfile.o main.f90

Output

 Output C-str array from fortran
 0�P�|0�
 p�|
 !|

Additional Info

enter image description here

Related posts

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

Passing an array of C-strings to Fortran (iso_c_binding)


Solution

  • On the C side you have an array of pointers, which is NOT the same as a two-dimensional Fortran array. Fortran doesn't have the "array of pointers" concept, so you have to have an array of derived type with a pointer component.

    First, the cptr returned from the C code has to be converted to an array of the derived type, each element of which contains a pointer to the string. You then iterate through each of these, converting the subcomponent C_PTR to a Fortran pointer, which is then your array of characters.

    Second, you can't DEALLOCATE something that Fortran didn't allocate. You would have to call back into C to free those strings.

    Here is a revised version of your Fortran code that works. The C code didn't change.

    program main
        use iso_c_binding
        implicit none
        interface
            subroutine str_alloc(msgs, n) bind(C, name="str_alloc")
                use iso_c_binding
                type(c_ptr), intent(out) :: msgs
                integer(c_size_t), intent(out) :: n
            end subroutine
        end interface
    
        integer, parameter :: STRLEN = 10
        character(kind=c_char, len=STRLEN), allocatable :: names(:)
    
        call str_array_from_c(names)
    
        contains
        subroutine str_array_from_c(strs)
            character(len=STRLEN), allocatable, intent(out) :: strs(:)
    
            type(c_ptr) :: cptr
            type,bind(C) :: c_array_t
                type(c_ptr) :: s
            end type c_array_t
            type(c_array_t), pointer :: c_array(:)
            integer(c_size_t) :: i, n, lenstr
            character(kind=c_char, len=1), pointer :: fptr(:)
    
            call str_alloc(cptr, n)
            call c_f_pointer(cptr,c_array, [n])
            allocate(strs(n))
            print*, "Output C-str array from fortran"
            do i = 1_c_size_t, n
                call c_f_pointer(c_array(i)%s, fptr, [int(STRLEN, kind=c_size_t)])
                lenstr = cstrlen(fptr)
                strs(i) = transfer(fptr(1:lenstr), strs(i))
                print*, fptr(1:STRLEN), "|", strs(i)
            end do
        end subroutine str_array_from_c
    
        !> Calculates the length of a C string.
        function cstrlen(carray) result(res)
            character(kind=c_char, len=1), intent(in) :: carray(:)
            integer :: res
            integer :: i
            do i = 1, size(carray)
              if (carray(i) == c_null_char) then
                res = i - 1
                return
              end if
            end do
            res = i
        end function cstrlen
    
    end program main
    

    When I build and run this I get:

     Output C-str array from fortran
     012346789|012346789
     012346789|012346789
     012346789|012346789