Universal Storage Object in Fortran

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.

links

social