Fortran is strictly typed language. And it is really strictly typed. For example, you cannot pass REAL(8)
(double precision) variable to where single precision REAL(4)
is expected. There is no even type casting in C-sense, but only type conversion. At least this was true until TRANSFER
function and C-interoperability were introduced. Now type casting in all its glory and ugliness is possible in Fortran. It must be very explicit though and it does require a few more steps.
This post demonstrates how a universal storage can be created for any Fortran-object. This storage is a building block for a container of objects, such as a list or a dictionary.
A universal storage object, USTORAGE for short, will store any object as a sequence of bytes. It is naturally implemented using an array of one-byte integers. To guarantee the storage size, C_INT8_T
integer kind from ISO_C_BINDING
intrinsic module is used. Since the size of a stored object is not known in advance, the array either must be allocatable or we must use parameterized derived type (PMT) facility of Fortran 2003. The former will be inefficient for small objects as it will require pointer dereferencing and will reduce memory locality. Thus, we will use the latter. However, there is unfortunately a long-standing bug in GNU Fortran for PMTs. So, the code below does not work for it. It does work with Intel Fortran Classic (ifort
) compiler:
<...>
use iso_c_binding, only: c_int8_t, c_loc, c_f_pointer, c_ptr
implicit none
type, public :: ustorage_t(len)
integer, len :: len
integer(c_int8_t), dimension(len) :: data
contains
procedure, pass(self) :: store => ustorage_store
procedure, pass(self) :: retrieve => ustorage_retrieve
end type
<...>
The type supports only two operations, to store the object and to retrieve it. The object is copied bit-by-bit into the storage for the former and retrieved bit-by-bit for the latter.
Let's look at the STORE
implementation:
subroutine ustorage_store(self, item)
class(ustorage_t(*)), intent(inout) :: self
type(*), intent(in) :: item
integer(c_int8_t), dimension(:), pointer :: pitem
call ustorage_get_pointer(item, self%len, pitem)
self%data(:) = pitem(:)
end subroutine
The main thing that happens here is the association of PITEM
pointer with an assumed-type (TYPE(*)
) object ITEM
— effectively untyped object. Once this is done, the rest is just a simple copy. This operation is performed in USTORAGE_GET_POINTER
subroutine:
subroutine ustorage_get_pointer(item, n, pitem)
type(*), intent(in), target :: item
integer, intent(in) :: n
integer(c_int8_t), dimension(:), pointer, intent(inout) :: pitem
type(c_ptr) :: cp
cp = c_loc(item)
call c_f_pointer(cp, pitem, [n])
end subroutine
This routine uses ISO_C_BINDING
module extensively as usual Fortran intrinsics will allow neither association nor conversion. So, the trick is to get actual memory address of ITEM
using C_LOC
function and then convert it to Fortran-pointer. For storage TRANSFER
function could also be used. However, that function will not help for retrieval since its use would require the assignment to an assumed-type object. By contrast, USTORAGE_GET_POINTER
makes retrieval quite simple:
subroutine ustorage_retrieve(self, item)
class(ustorage_t(*)), intent(in) :: self
type(*), intent(inout) :: item
integer(c_int8_t), dimension(:), pointer :: pitem
call ustorage_get_pointer(item, self%len, pitem)
pitem(:) = self%data(:)
end subroutine
The use of USTORAGE_T
object requires one extra step: the LEN
parameter needs to be specified by a compile-time constant. SIZEOF
intrinsic helps with that. Unfortunately, in contrast to C, one cannot use the type as an argument to this intrinsic but instead need to create a dummy object:
type point_t
real :: x, y
end type
integer, parameter :: point_size = sizeof(point_t(0.0, 0.0))
type(ustorage_t(point_size)) :: stored_point
type(point_t) :: p, q
p%x = 1.0; p%y = 2.0
call stored_point%store(p)
call stored_point%retrieve(q)
write (*,*) q%x, q%y
After this code, Q
will contain a copy of P
. Note that the user-side code does not have to deal with pointers and memory addresses — all these details are well hidden.