2

I'm trying to write a mixed-language (C++/Fortran) code in which memory passed from C++ to the Fortran subroutine may or may not be used and so in fact may or may not be allocated. The behavior inside the Fortran subroutine should be determined by an additional integer flag that is passed from C++ to Fortran. The problem that I am encountering seems to occur primarily with the Intel compiler (2022.1.2, 2022.1.0, 2021.4.0, 2021.1) combined with optimization (-O2). In that case, the compiler ignores my conditional statement and chooses to make an assignment from the unallocated memory, triggering a segmentation fault.

This example, consisting of one .cpp file, one .F90 file, and one Makefile, should demonstrate the problem: The line in the Fortran code that says "my_number = numbers" should never be executed based on the surrounding conditional, but with optimization, the line triggers a segmentation fault anyway.

Is there a way to prevent the optimization from ignoring my conditional or a better way to handle this overall (while maintaining the mixed-language paradigm)?

main.cpp:

extern "C" {
void test_module_mp_test_routine_(int* numbers,int* allocated);
}

int main(void)
{
  int* numbers(0);
  bool allocated(numbers);
  int allocated_int(allocated);

  test_module_mp_test_routine_(numbers,&allocated_int);

  return 0;
}

test_module.F90:

! ============================================================
  module test_module
! ============================================================

  implicit none
  private
  save

  public :: test_routine

  contains

! ============================================================
  subroutine test_routine(numbers,allocated_int)
! ============================================================

  implicit none

  integer :: numbers,allocated_int
  logical :: have_numbers
  integer :: my_number


  continue


  have_numbers = (allocated_int == 1)

  my_number = 0
  if (have_numbers) then
    my_number = numbers
  end if

  write(*,*)"allocated_int = ",allocated_int

  if (have_numbers) then
    write(*,*)"my_number = ",my_number
  end if

  return
  end subroutine test_routine

  end module test_module`

Makefile:

CXX=icpc
FORT=ifort

main: main.cpp test_module.o
        $(CXX) -O2 -o $@ $^ -lifcore

test_module.o: test_module.F90
        $(FORT) -O2 -c $<

clean:
        rm -f test_module.{o,mod} main
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
D.B.
  • 23
  • 5
  • 1
    `my_number = 0` runs unconditionally. Are you *sure* it faults on `my_number = numbers` after already writing it before the `if`? ICC has had bugs when auto-vectorizing conditionals, like [Crash with icc: can the compiler invent writes where none existed in the abstract machine?](https://stackoverflow.com/q/54524947) but your code does unconditionally write. It may optimize away the dead store of 0, like turning it into `my_number = allocated_int == 1 ? numbers : 0`. Or is the problem that it *reads from* `numbers`? That would be assigning *from*, not *to*, and would also be a bug. – Peter Cordes Jul 01 '23 at 20:29
  • Your comment prompted me to go back, add -g to the compile, and run the code through valgrind. Valgrind reports "Invalid read of size 4" in test_module.F90:31 ("my_number = numbers"). (Address 0x0 is not stack'd, malloc'd or (recently) free'd.) Of course my contention is that the code should never have run line 31 since "have_numbers" is false. I wasn't sure, when you said "would also be a bug," did you mean a bug in the compiler? Or in the code? – D.B. Jul 02 '23 at 00:03
  • Your comment also prompted me to revise the original post to read "...make an assignment _from_ the unallocated memory..." – D.B. Jul 02 '23 at 00:18
  • 1
    Ok, then yes, deref of the NULL pointer. If Fortran allows "allocatable" objects to be unallocated, then this is a different but similar compiler bug to the one I linked. Inventing reads from pointers that might be null, instead of inventing writes to memory that might be read-only or not thread-safe. But I barely know any Fortran; if `allocated_int` objects must be non-null (and valid), then the compiler is allowed to invent a read from them, and it's your C program that has a bug in calling Fortran with a null pointer/reference. – Peter Cordes Jul 02 '23 at 00:36
  • 3
    In the Fortran procedure, you are promising that `numbers` and `allocated_int` correspond to allocated memory. If one of them isn't, you have only yourself to blame for any consequence, regardless of whether you reference the non-allocated one or not. If an argument is potentially not allocated, it must be optional or a pointer/allocatable object. – francescalus Jul 02 '23 at 00:53
  • Yes, if you want `numbers` to be optional, then make it optional (`integer, optional, intent(in) :: numbers`) and test its presence (`if (present(numbers)) ...`). Either way, why aren't you using standardized Fortran--C interoperability instead of hacking things like `test_module_mp_test_routine_`? – francescalus Jul 02 '23 at 01:10
  • 3
    [This other question and its answers](https://stackoverflow.com/q/40089567/3157076) possibly contain all you need to know. – francescalus Jul 02 '23 at 01:17
  • That makes sense. Thank you both! – D.B. Jul 02 '23 at 01:21
  • Given that this is only a minimum example, I think my preference is to always pass the argument (properly) and then test whether it is associated, as the linked other question demonstrates. I don't know what the proper etiquette is at that point for selecting an accepted answer. – D.B. Jul 02 '23 at 01:33
  • You can flag your question as a duplicate of another question, or if it's not a close enough duplicate, write a self-answer briefly explaining the situation, linking other Q&As for more details as necessary. – Peter Cordes Jul 02 '23 at 18:09

2 Answers2

3

Within a Fortran procedure like

subroutine sub(x)
   real :: x
end subroutine

the argument x is an ordinary (data) dummy variable. Within Fortran, if such a dummy variable is argument associated with an allocatable actual argument, that actual argument must be allocated (Fortran 2018, 15.5.2.4 p7); if argument associated with a pointer argument, that pointer argument must be pointer associated (F2018, 15.5.2.3 p1).

If we have something like

program bad
   implicit none

   real, allocatable :: a
   real, pointer :: b => null

   call sub(a)
   call sub(b)

contains

   subroutine sub(x)
     real :: x
   end subroutine

end program bad

we have problems in each call to sub. You can ask your friendly compiler to tell you about these problems, instead of presenting a segmentation fault, or whatever other nastiness it chooses to perform. With ifort -check all is the way to ask.

Now, when calling a Fortran subroutine like this by means other than Fortran (using C interoperation, for example, or C++ hacky stuff) our compiler-generated runtime test may not work, and Fortran's rules about allocation/pointer association could be seen as a little fuzzy. Still, we can work fully within the meaning of the code and Fortran's requirements by stating the goal here as "optional arguments". If we don't want to use a variable's value within the procedure, we avoid providing one.

Going back to our Fortran program with a change:

program lessbad
   implicit none

   real, allocatable :: a
   real, pointer :: b => null

   call sub(a)
   call sub(b)

contains

   subroutine sub(x)
     real, optional :: x  ! A difference
   end subroutine

end program lessbad

For the optional x we are allowed to argument associate an unallocated actual argument or a disassociated pointer actual argument. Simply, in these cases x is taken as not present (F2018, 15.5.2.12).

Optional arguments are interoperable with C in Fortran 2018, as implemented by recent Intel compilers:

subroutine test_routine(numbers) bind(c)
  use, intrinsic :: iso_c_binding, only : c_int
  integer(c_int), optional :: numbers

  logical :: have_numbers
  integer :: my_number

  have_numbers = PRESENT(numbers)

  my_number = 0
  if (have_numbers) then
    my_number = numbers
  end if

  if (have_numbers) then
    write(*,*)"my_number = ",my_number
  end if
end subroutine test_routine

An optional dummy argument is absent if (and only if) it is associated with a (C) null pointer (F2018, 18.3.6 p7).

In the example of the question, the pointer nature of the dummy argument is not used: it's not necessary for the dummy to be a pointer. In more general cases it may be necessary to have the dummy be a pointer: here this other answer shows how to use c_associated to test whether a type(c_ptr) is associated. In even more general cases it's possible to have a dummy a Fortran pointer (instead of a C pointer), and use associated, but that's somewhat more advanced.

As also stated in that other answer, small changes to the invocation and C prototype definition are required, to support these alternative approaches.

francescalus
  • 30,576
  • 16
  • 61
  • 96
  • Thank you for this answer. I find that this answer (using `optional` and `present`) works even without `bind(c)` (excluding the name mangling of course) but that in [my answer](https://stackoverflow.com/a/76605713/3157076) (using `pointer` and `c_associated`), `bind(c)` is required to avoid a segmentation fault. Am I over-extending this question if I ask what `bind(c)` is doing here beyond name mangling? – D.B. Jul 03 '23 at 17:46
  • 2
    `bind(c)` here ensures that the procedure is interoperable. We want it to be interoperable because we're invoking it from outside Fortran. Being interoperable means we know exactly what the Fortran compiler is doing with that procedure interface: without it the compiler could use some "magic" calling conventions, secret stack hacks, and so on, which are things we'd rather not care about. – francescalus Jul 03 '23 at 18:04
0

The problem here (as pointed out by @Peter Cordes and @francescalus) is that the compiler has been misled. Declaring the argument "numbers" as simply an integer scalar "promises" the compiler that the memory is valid. The solution is to declare the argument properly as a pointer (which it is), which deters the optimizer from inventing a read from the (potentially) null pointer.

This revised code block (inspired by Calling Fortran subroutines with optional arguments from C++) fixes the original problem:

! ============================================================
  subroutine test_routine(numbers_ptr) bind(C)
! ============================================================

  use, intrinsic :: iso_c_binding

  implicit none

  type(c_ptr),value :: numbers_ptr
  integer(c_int) :: my_number
  integer(c_int),pointer :: numbers


  continue


  my_number = 0
  if (c_associated(numbers_ptr)) then
    call c_f_pointer(numbers_ptr,numbers)
    my_number = numbers
  end if

  if (c_associated(numbers_ptr)) then
    write(*,*)"my_number = ",my_number
  else
    write(*,*)"not associated"
  end if

  return
  end subroutine test_routine

where main.cpp is revised to use the unmangled routine name and pass only the one argument.

D.B.
  • 23
  • 5