Home > Back-end >  Propagating optional arguments from C to Fortran and vice versa
Propagating optional arguments from C to Fortran and vice versa

Time:08-25

I am wondering what is the correct way of interfacing with C, when the C methods have optional arguments (i.e. you are allowed to pass NULL) and you want the optional arguments to propage to the Fortran API.

  1. Is there a benefit in including the optional keyword in both the Fortran method's argument list and the interface block? see: err_c argument in null_return_f90>C_API.
  2. Is it legal to initialise a local variable with an optional input variable and pass that to the C interface? (this feels wrong, it doesn't feel like it should work, yet it does). see: name_c variable, its initialisation and input to C_API in null_str_f90 subroutine.

I have provided a MWE below demonstrating what I am asking and as far as I can tell, the answers to my questions are 1. it doesn't matter 2. yes, but I am not entirely convinced they are correct, especially 2.

If there is a better way on how to write the Fortran API I would be open to that as well, but what I cannot do is edit the C API.

Related post: Calling Fortran subroutines with optional arguments from C

MWE

program main
    use, intrinsic :: iso_c_binding
    implicit none    
    integer(c_int) :: err

    call null_str_f90("abc")
    call null_str_f90()
    print*, repeat("*", 10)
    call null_return_f90()
    call null_return_f90(err)
    print*, "error code:", err

    contains

    function istring_(o) result(v)
        character(len=*), intent(in) :: o
        character(len=:, kind=c_char), allocatable :: v
        v = trim(o)//c_null_char
    end function istring_

    subroutine null_return_f90(err)
        interface
        subroutine C_API(err_c) bind(C, name="null_return")
          use, intrinsic :: iso_c_binding
          integer(c_int), optional, intent(out) :: err_c  ! 1. does the optional do anything?
        end subroutine C_API
        end interface
        integer(c_int), optional, intent(out) :: err
        call C_API(err_c=err)  ! 1. Is this safe & portable?
    end subroutine null_return_f90

    subroutine null_str_f90(str)
        interface
        subroutine C_API(str_c) bind(C, name="null_str")
          use, intrinsic :: iso_c_binding
          character(len=1, kind=c_char), dimension(*), optional, intent(in) :: str_c
        end subroutine C_API
        end interface
        character(len=*), intent(in), optional :: str
        ! Local variables
        character(len=:, kind=c_char), allocatable :: name_c
        if (present(str)) name_c = istring_(str)
        call C_API(str_c=name_c)  ! 2. Is this safe & portable?
    end subroutine null_str_f90
end program main
#include <stdio.h>

void null_str(const char *str) {
  if (str) {
    printf("str is present: str is %s\n", str);
  } else {
    printf("str is not present\n");
  }
}

void null_return(int *opt) {
  if (opt) {
    *opt = 1;
  } else {
    printf("opt is not present\n");
  }
}

Compiling

Turn on aggressive non-IEEE compliant optimisations to ensure this will still work.

gcc -c -Ofast -Wall null_args.c -o null_args.o
gfortran -Ofast -Wall null_args.o null_args.f90 -o null_args
./null_args

Output

str is present: str is abc
str is not present
 **********
opt is not present
 error code:           1
 **********

CodePudding user response:

For null_return_f90 and its C_API, we must have err_c in that latter as an optional argument. Without it being optional, we cannot associate an absent err with it in that call. If it is optional, then it's fine to use the optional and not present actual argument err.

Looking at the subroutine null_str_f90 we have the following:

character(len=:, kind=c_char), allocatable :: name_c

if (present(str)) name_c = istring_(str)
call C_API(str_c=name_c)  ! 2. Is this safe & portable?

Your concern is that if the actual argument str is not present, then name_c is undefined1 when it comes to the call to C_API. This is true, but is not a concern.

name_c remains not allocated rather than simply undefined. An allocatable actual argument which is not allocated (or a pointer which is not associated) is allowed to be associated with an ordinary optional dummy argument. In this case it will be treated as a not present actual argument, just as if it weren't given at all.


1 You state "uninitialized" but definition and initialization are very different things, and initialization is not relevant to this case.

CodePudding user response:

Keeping in mind that function arguments:

  • in C they are passed by value, hence, they are unchanged on output; the only way to change a variable which is not the function's return value, is to pass a pointer to it (your int* err_c is a pointer to integer(s))
  • in Fortran they are passed by reference by default, unless otherwise specified (value keyword).

whenever you write a Fortran interface to a C function that involves a pointer, you have these options:

  1. Copy the C approach, set it as a pointer

type(c_ptr), intent(in), value :: c_err

  • You can always shape it to an appropriate Fortran derived type and shape by using c_f_pointer, and/or access the C pointer of a Fortran target variable using c_loc
  • You lose information about the original type (int) in this case
  1. Use the Fortran approach, i.e. reference that pointer to a given type, shape, intent. For example in your case, you could use any of
 integer(c_int), intent(out) :: c_err
 integer(c_int), intent(out) :: c_err(*)

Note that the C routine, per se, doesn't tell you which one is right, but the Fortran compiler will check it.

Now you're trying to use the optional keyword to hide the fact of a C pointer being NULL as opposed to a Fortran variable being present or not. The C standard does not allow for optional inputs; while the Fortran standard does not like null stuff. It seems like even Fortran standard gurus do not fully agree on what's allowed by the standard or not (see here), I'd go with using the optional keyword in the interface only if that's useful on the Fortran side.

See this example:

module test_c_opt
    use iso_c_binding
    implicit none

    interface

        ! Pass-by-reference interface
        subroutine C_reference(err_c,message) bind(C, name="null_return")
          import c_int,c_char
          integer(c_int), intent(out) :: err_c
          character(len=1,kind=c_char), intent(in) :: message(*)
        end subroutine C_reference

        subroutine C_reference_arr(err_c,message) bind(C, name="null_return")
          import c_int,c_char
          integer(c_int), intent(out) :: err_c(*)
          character(len=1,kind=c_char), intent(in) :: message(*)
        end subroutine C_reference_arr

        ! Pass-by-value interface
        subroutine C_value(err_c,message) bind(C, name="null_return")
          import c_ptr,c_char
          type(c_ptr), intent(in), value :: err_c
          character(len=1,kind=c_char), intent(in) :: message(*)
        end subroutine C_value

     end interface

end module test_c_opt

program test
    use test_c_opt
    use iso_c_binding

    implicit none

    integer(c_int), target :: ierr,ierr10(10)
    integer(c_int), allocatable, target :: ierra,ierra10(:)
    character(len=255,kind=c_char) :: msg

    msg = 'reference, static'    //c_null_char; call C_reference(ierr,msg)
    msg = 'value    , static'    //c_null_char; call C_value(c_loc(ierr),msg)
    msg = 'reference, not alloc' //c_null_char; call C_reference(ierra,msg)
    msg = 'value    , not alloc' //c_null_char; call C_value(c_loc(ierra),msg)
    allocate(ierra)
    msg = 'reference, allocated' //c_null_char; call C_reference(ierra,msg)
    msg = 'value    , allocated' //c_null_char; call C_value(c_loc(ierra),msg)

    msg = 'reference, not alloc(10)' //c_null_char; call C_reference_arr(ierra10,msg)
    allocate(ierra10(10))
    msg = 'reference, alloc(10)' //c_null_char; call C_reference_arr(ierra10,msg)
    print *, 'from fortran, ierra(10)=',ierra10

    !msg = 'reference, static(10)'//c_null_char; call C_reference(ierr10,msg) ! error: fortran checks this is not a scalar
    msg = 'value    , static(10)'//c_null_char; call C_value(c_loc(ierr10),msg)

end program test
#include <stdio.h>

void null_return(int *opt, char* message) {
  if (opt) {
    *opt = 1;
    printf("when [%s], opt is present, set opt=%d \n", message, *opt);
  } else {
    printf("when [%s], opt is not present\n");
  }
}

it works in all cases without optional anyways, which prints (gfortran-12)

when [reference, static], opt is present, set opt=1 
when [value    , static], opt is present, set opt=1 
when [reference, not alloc], opt is not present
when [value    , not alloc], opt is not present
when [reference, allocated], opt is present, set opt=1 
when [value    , allocated], opt is present, set opt=1 
when [reference, not alloc(10)], opt is not present
when [reference, alloc(10)], opt is present, set opt=1 
 from fortran, ierra(10)=           1           0           0           0           0           0           0           0           0           0
when [value    , static(10)], opt is present, set opt=1 

  • Related