I’ve been working on PRIK (Python Runtime Interop Kit), a tool that generates native Python bindings for Fortran and C, and I recently ran into an interesting Fortran interoperability question.
Suppose an existing library contains:
module state
use iso_c_binding, only: c_double
implicit none
real(c_double) :: a(100) ! no TARGET
end module state
PRIK would ideally expose a as a zero-copy NumPy view, without requiring the original library to be modified to add TARGET.
One approach is to generate a small bind(C) bridge:
interface
subroutine capture(x) bind(C)
import c_double
real(c_double) :: x(*)
end subroutine
end interface
call capture(a)
So there is no C_LOC, no CFI_cdesc_t, and no TARGET.
My question is:
Is saved valid for as long as the original module variable a exists?
Since a module variable has static storage, this seems different from retaining the address of a local or temporary array. I’m particularly interested in whether this is fully standard-conforming and whether there are any compiler/optimization concerns.
This is one of the practical edge cases I’m dealing with.
I’d be very interested in hearing how others would approach this case.
I’m not sure about the Python interface part of the question, so here are some comments that would apply “as if” everything were written in fortran. With the interface that you have defined, the following calls would both be allowed:
call capture( a )
call capture( a(1:100:2) )
In the first case, you would expect the address of the original array to be passed to the subroutine, and the saved pointer within the subroutine would point to that original array. However, in the second case, the fortran compiler would make a temporary copy of the odd elements of the array and pass the address of that temporary copy to the subroutine. In this case, the saved pointer would be to that temporary copy, which would evaporate upon return from the subroutine, leaving a dangling pointer. But as far as the language bindings are concerned, the compiler is free to make a temporary copy in both cases; it is just a practical matter that the first case usually works without the copy. In fortran to fix this problem, both the actual argument and the dummy argument need to have the target attribute (or the pointer attribute, but that doesn’t work for the C interop case). I think, but I’m not certain, that the target attribute is required, according to the language semantics, also in the C interop case. Also in the C interop case, you cannot pass the array slice as an actual argument, but I think the compiler will warn you about that at compile time. As you note, there is no issue with the actual argument a(:) disappearing, it has an implicit save (which you could make explicit if you want to make the code clear), the issue is what happens during the argument association.
In addition to what @RonShepard wrote about argument passing, my understanding of the standard is that it does not require the module variable a to occupy a fixed physical address for the life of the program. It’s not a target, so nothing in Fortran can observe its address. Your C function should not “observe” the address either to be conforming. Since it does (in your case), then you should expect that it can change. In some ways this is consistent with what @RonShepard wrote about the temporaries for argument passing. Fortran can do that underneath the hood and it doesn’t break things.
Thanks! In my case i just need the first call call capture( a ) to work. is there any test case where we would get a copy or the physical address changes? or is it merely unguaranteed but works everywhere in practice? I failed to produce an error by trying different compilers with different optimization flags.
I think this is the best characterization of the posted code. To conform to the standard and guarantee that it will work, I think you need to add the target attribute to the module array and to the dummy argument of the interface.
My understanding is that it is unguaranteed, and consequently you should not rely on it.
For example, for LFortran I would like to reserve the right to change the memory address, for optimization purposes. For example, for various device offloading, the compiler should have the right to copy the array as needed over to the device and host, for platforms that do not have unified memory.
In my case i just need the first call call capture( a ) to work
I think you need this to work in all cases, not just the first call. Consider that you save the pointer, then the compiler relocates the array, and then you want to read it from Python later. It will segfault.
Thanks, I think it is clear. Weirdly enough, F2PY seems to handle it: it can expose a non-TARGET Fortran module array as a NumPy view, and changes made from Python are visible to the Fortran code.
The purpose of changing its address would be to temporarily move it to a device (a GPU), as far as I understand. But:
shared-memory device: no need to move data around, address is the same for both already.
non-shared-memory device? device should have their own copy, which means that as the device changes the data, the two won’t be in sync, at least until the device sends the data back via one of its queues. So what should other consumers (libraries, programs) of that variable do at that time? They are in a race condition with the device and there is no way to introduce an access barrier with that syntax.
So I think the likelihood was 100% as far as my understanding of Fortran, but if you know of an application where changing that address is indeed possible/useful/actually happens, I’d be very interested to know!
While I can agree with you that in practice it works because of the reasons you gave, I wouldn’t say the likelyhood is 100%, and particularly if considering the future.
It’s like dummy argument aliasing, which is not allowed unless all of them are INTENT(IN). In practice I have seen many codes with aliased dummy arguments that were modified, and these codes used to work. Because in practice, pre-F90 compilers had little -if any- reason to make a copy. However it became more error-prone with Fortran 90 and the ability to pass array sections, which can trigger copy-in/copy-out. So, what was apparently safe in F77 became less safe in F90+.
We should not write potentially unsafe codes, particularly when there is a safer way that is not much more complex.
what we generate is a Fortran shim getter for every module variable, and in it we get the address, the extent …etc, for scalar types we just have setters and getters, but for arrays in order to avoid making copies we need to get the address in order to create a numpy view.
You can do that, and it has been done that way many times, and also some vendor APIs work that way (e.g. Intel oneMKL Inspector-executor Sparse BLAS is one I can think of).
It works because the underlying ELF machinery works that way and is shared across programming languages.
module state
use iso_c_binding, only: c_double
implicit none
real(c_double) :: a(100) ! no TARGET
real(c_double), bind(c) :: b(100)
end module state
$ flang -c -pedantic fortran/state.f90
$ nm state.o
0000000000000000 S __QMstateEa
0000000000000320 C _b
0000000000000000 t ltmp0
0000000000000000 s ltmp1
Meaning of S and C letters
From Claude:
S __QMstateEa — an external (uppercase) symbol living in a section other than__text, __data, or __bss. For Mach-O, LLVM puts zero-initialized external globals into __DATA,__common (a zerofill section), while only local/static ones go to __DATA,__bss — so a shows as S rather than B. The 0 is its offset within that section. This is an ordinary, fully-defined symbol; a second definition elsewhere would be a duplicate-symbol link error.
C _b — a common symbol (tentative definition), i.e. a .comm directive. It has no section and no address yet; the number printed, 0x320 = 800 = 100 × 8, is the symbol’s size, not an offset. The linker allocates it at link time and merges it with any other common symbol of the same name and takes the largest size.
Flang deliberately lowers bind(c) variables without an initializer to common linkage so they interoperate with a C tentative definition like double b[100]; — both sides can “define” b and the linker just merges them, matching what a C compiler does with -fcommon. Note that clang has defaulted to -fno-common since clang 11, so a C double b[100]; will itself be a real S/B definition; a common _b from flang still merges fine with a real definition (the real one wins), so this works either way.
The Fortran way to use a from a procedure outside the state module would be via use association.
Capturing the address at runtime essentially bypassed the issue of accessing the mangled names:
! state.f90
! flang -shared -fPIC state.f90 -o libstate.dylib
module state
use iso_c_binding, only: c_double
implicit none
real(c_double) :: a(10) ! no TARGET
real(c_double), bind(c) :: b(10)
contains
subroutine print()
print *, a + b
end subroutine
end module state
# drive_state.py
import ctypes
lib = ctypes.CDLL("./libstate.dylib")
a = (ctypes.c_double * 10).in_dll(lib, "_QMstateEa")
b = (ctypes.c_double * 10).in_dll(lib, "b")
for i in range(10):
a[i] = i
b[i] = 10 - i
fprint = getattr(lib, "_QMstatePprint") # module procedure print
fprint.restype = None
fprint.argtypes = []
fprint()
If you were compiling with gfortran the variable would be __state_MOD_a, and with ifx/ifort state_mp_a_. So you’d be limited to one compiler. Array b on the other hand just has the given C name.
In principle you could let PRIK know what Fortran compiler was produced to create the shared library and just bind to the mangled module variables without the shim procedure.
One can do such things, but the question is what happens if something changes. Maybe a future contributor to the project adds a private statement to the module or a protected attribute to the array variable. The Python code could silently break. Reading the definition, a Fortran programmer might assume the entity is only changed in the Fortran part of the program, not the Python one. Using target or bind(c) would be the “warrant” that permits access by means other than use or host association. There is a name for this type of problem in programming: Action at a distance (computer programming) - Wikipedia
@Said-H cannot you write a getter/setter in Fortran (thus it will be portable) for the module array that is not target and then you call the setter/getter from Python?
Absolutely, it is always possible either for the user to add them in his Fortran code or we can do it automatically and provide the setters/getters for the user to be available to him in Python, the question would just be how to present them for multi-dimensional arrays.