Advanced Fortran 90: Callbacks with the Transfer Function

·

·

This post is not specific to the Mac, but there are probably many MR readers that have to use Fortran in some aspect of their work, so it certainly can’t hurt…

Callbacks for Mac

A few weeks ago I was lamenting the difficulty of implementing decent callback functionality in Fortran 90. A callback is common place in C programming; it basically allows you to customize the behavior of a subroutine. Take this simple example:

#include <stdio.h>

void Woof()  
{
    printf("Woof\n");
}

void Meouw() 
{
    printf("Meouw\n");
}

typedef void (*SoundFunction)();
void MakeSoundTenTimes(SoundFunction soundFunc)
{
    int i;  
    for ( i = 0; i < 10; ++i ) soundFunc();
}

int main()  
{
    MakeSoundTenTimes(Woof);
    MakeSoundTenTimes(Meouw);
}

The point of this rather obscure example is that the main program can call the MakeSoundTenTimes function passing a function pointer. (If you are not familiar with C’s cryptic function pointer syntax, don’t worry about it: it is not important.) The loop in MakeSoundTenTimes can call the function passed in, effectively customizing its behavior. It’s a bit like rewriting the inside of the MakeSoundTenTimes each time: the function passed determines the results.

You can actually do this in Fortran 90. It looks like this:

module Sounds

contains

  subroutine Woof()
    print *,'Woof'
  end subroutine

  subroutine Meouw() 
    print *,'Meouw'
  end subroutine

  subroutine MakeSoundTenTimes(soundFunc)
    integer :: i
    interface
      subroutine soundFunc()
      end subroutine
    end interface
    do i = 1, 10 
      call soundFunc()
    enddo
  end subroutine

end module

program main
  use Sounds
  call MakeSoundTenTimes(Woof)
  call MakeSoundTenTimes(Meouw)
end program

Callbacks with Arbitrary Arguments

This works fine, but problems start to arise if you need to pass some data to the callback. In C, it is conventional to include a void pointer, which can point to any type of data. But Fortran 90 doesn’t have void pointers, so how do you pass arbitrary data to the callback? To see how you can do this, let’s again begin with an example from C which passes data via a void pointer:

#include <stdio.h>

void IncrementAndPrintFloat(void *data)  
{
    double *d = data; 
    (*d)++; 
    printf("%f\n", *d);
}

void IncrementAndPrintInteger(void *data)  
{
    int *i = data; 
    (*i)++; 
    printf("%d\n", *i);
}

typedef void (*IncrementFunction)(void*);
void IncrementTenTimes(IncrementFunction incrFunc, void *data)
{
    int i;  
    for ( i = 0; i < 10; ++i ) incrFunc(data);
}

int main()  
{
    double f = 5.0;
    int i = 10; 
    IncrementTenTimes(IncrementAndPrintFloat, &f);
    IncrementTenTimes(IncrementAndPrintInteger, &i);
}

This is quite similar to the original example, but now the callbacks take a void* argument, as does the IncrementTenTimes function that calls back. The callback functions know what type of data is passed to them, so they cast the void pointer to the appropriate type, increment, and print. The IncrementTenTimes function, on the other hand, does not know anything about what the data represents. It is just a generic pointer that gets passed on to a callback function. This is actually a strength of the callback pattern — the behavior of the calling-back function and the callback function are largely decoupled, facilitating code reuse.

For a long time, I didn’t think you could have this sort of callback in Fortran 90, but I recently realized that you can. It involves a relatively obscure intrinsic function called transfer. If you ask the majority of Fortran programmers what transfer does, chances are you will get a blank look or a rather vague answer. What transfer actually is is a means of casting data from one type to another.

So what would the above C example look like in Fortran 90 with the transfer function. Here it is:

module Increments

contains

  subroutine IncrementAndPrintReal(data)
    character(len=1) :: data(:)
    real             :: r
    r = transfer(data, r)
    r = r + 1.0 
    print *,r
    data = transfer(r, data)
  end subroutine

  subroutine IncrementAndPrintInteger(data)
    character(len=1) :: data(:) 
    integer          :: i    
    i = transfer(data, i)
    i = i + 1 
    print *,i
    data = transfer(i, data)
  end subroutine

  subroutine IncrementTenTimes(incrFunc, data)
    character(len=1) :: data(:) 
    integer :: i
    interface
      subroutine incrFunc(data)
        character(len=1) :: data(:) 
      end subroutine
    end interface
    do i = 1, 10 
      call incrFunc(data)
    enddo   
  end subroutine

end module

program main
  use Increments
  character(len=1), allocatable :: data(:) 
  integer                       :: lengthData
  real                          :: r = 5.0 
  integer                       :: i = 10

  lengthData = size(transfer(r, data))
  allocate(data(lengthData))
  data = transfer(r, data)
  call IncrementTenTimes(IncrementAndPrintReal, data)
  deallocate(data)

  lengthData = size(transfer(i, data))
  allocate(data(lengthData))
  data = transfer(i, data)
  call IncrementTenTimes(IncrementAndPrintInteger, data)

end program

Now I’m the first to admit this is a little more verbose than the C version, and that’s putting it mildly, but it is possible to do, and could be a very handy tool in certain instances.

So how does it work? Let’s take a transfer function call from the main program, and dissect that:

data = transfer(r, data)

What this does is copy the bytes of the variable r, returning it with the type of data. The second argument to the transfer function is there purely to tell the function what type of data it should return. The data returned by transfer is then stored in the variable data, which is an array of characters.

If the second argument to transfer is an array, no matter how long, it will figure out how big the return array needs to be to fully accommodate the type passed in as first argument. That’s pretty nifty, and you can see that we use that to determine how big our data array needs to be to be able to contain the real or integer variable being passed. In particular, this line

lengthData = size(transfer(r, data))

does the conversion, but only uses the result to send to the size intrinsic to determine how long the data array should be. The data array is then allocated (malloced for our C friends) and transfer is called again, this time with the result actually getting stored in data.

One final thing before signing off: we have used an array of character(len=1) to store generic data here, but you can use any array type you like. transfer will figure out from the type you are using how many elements the array needs to have. I have chosen to use an array of characters, because a character usually corresponds to one byte, and that way I won’t be wasting any memory. But you could also use an array of integers, for example, and then your data granularity would be something like 4-bytes, depending on the operating system and compiler.

Fortran is often underestimated as a programming language, but there is plenty in there for scientific developers. Some aspects of the language, like transfer, can seem a bit abstract to begin with, and their potential not fully realized. Hopefully this little tutorial has shown you that transfer is useful, and encourages you to seek out other uses yourself. (Hint: You can also build data containers for generic types, like dynamic arrays and dictionaries, using transfer.)


Leave a Reply

Your email address will not be published. Required fields are marked *