2

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
 **********
gnikit
  • 1,031
  • 15
  • 25
  • "Is there a benefit in including the optional keyword in both the Fortran method's argument list and the interface block?" If you want the argument to be optional, you have to give the `optional` attribute. This seems obvious to me, so I'm probably missing something about the specifics of what you are asking. Can you reference in the question text the specific parts of the example code you are asking about to make it as clear as possible? – francescalus Aug 24 '22 at 11:53
  • Re 1: you are right, that is not what I am doing. I am leaving `name_c` completely uninitialized if `name` is `.not. present`. I want to do that. I am curious as to why that works though. When `name_c` is not initialised and passed on to the C interface is its value set to `c_null_ptr`? I thought its value is undefined (can hold any garbage). – gnikit Aug 24 '22 at 12:03
  • Re 2: see in the MWE, Fortran code, line with comment `! 1. does the optional do anything?`. Does adding the `optional` keyword in the `interface`, **in addition** to it being present in the `null_return_f90` argument list, have any impact in the code shown? As far as I can tell, no it does not and hence it can be removed. – gnikit Aug 24 '22 at 12:10
  • For `name_c` I did indeed miss the difference between the two procedures you have. To stop other people making that same mistake in a quick read through it would indeed be helpful if you could refer to the names of things instead of simply "the interface" and "the method". – francescalus Aug 24 '22 at 12:13

2 Answers2

2

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.

francescalus
  • 30,576
  • 16
  • 61
  • 96
  • If I've understood correctly the first part of your response, removing `optional` from `err_c` in the `C_API` would be incorrect, and should not work. For me with gfortran 12.1.0 it seems to produce the expected output even without `optional` in the `C_API`. I will leave the `optional` keywords in both the `C_API` and the parent Fortran procedures then. – gnikit Aug 24 '22 at 12:44
  • With regards to the second part, would it mean then that if `name_c` was not `allocatable` and it was an `integer(c_int)` instead, the above would fail/lead to unexpected behaviour? – gnikit Aug 24 '22 at 12:49
  • 1
    By "must" i mean in the sense of meeting the requirements of the Fortran standard. It's possible to write non-compliant code that works (or appears to) because of how a particular compiler implements things. If you remove the `optional` attribute and then compile with all run-time checks you will hopefully get an error message when the actual argument is not present. – francescalus Aug 24 '22 at 12:49
  • Thanks, that was one of my fears with this code, that it would end up being be non-compliant to the standard and then stuff would suddenly start breaking. – gnikit Aug 24 '22 at 12:51
  • 1
    Yes, making the local variable allocatable is a vital part of having this work. If `name_c` were not allocatable then it would be treated as a present argument that hasn't been defined and this could easily go wrong. You'd need to have separate calls, one for present and one for absent. – francescalus Aug 24 '22 at 12:52
  • Or simply make the local variables `allocatable`, e.g. `integer(c_int), allocatable :: l_array_sz` and if the array, in this case, is present you allocate the variable `if (present(array)) l_array_sz = size(array)` making the call to a C interface now safe, `call C_API(array_c=array, array_size_c=l_array_sz)` – gnikit Aug 24 '22 at 13:05
  • Could you highlight in your post, and I will mark it as the answer, the importance of the how local variables need to be `allocatable` or a `pointer` that is disassociated to reliably be detected as not/present if they are to be passed to C interfaces optionally. see 15.5.2.12 paragraph 1 in (J3/18-007r1, WD 1539-1, 2018-08-28) (I think) – gnikit Aug 24 '22 at 13:25
  • Could you expand on what you are trying to do with the `array` version? You don't seem to have any explicit size arrays in your question? – francescalus Aug 24 '22 at 21:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247552/discussion-between-gnikit-and-francescalus). – gnikit Aug 25 '22 at 11:27
2

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 

Federico Perini
  • 1,414
  • 8
  • 13
  • I think the argument that `optional` in the interface should be present has to do with standard compliance, as @francescalus mentioned. What is still unclear to me is whether `not present` in Fortran translates to `NULL` in C i standard compliant – gnikit Aug 25 '22 at 11:27
  • IMHO `present` in fortran is broader than in C, as `not present` could be triggered by both 1) missing argument 2) argument is present and allocatable, but not allocated. I know the meaning of 2) is similar to `NULL` in C, but I'm not sure if there are edges cases that should be considered (for example, how about `integer, allocatable, optional, intent(in) :: i(:)`?) – Federico Perini Aug 25 '22 at 12:03