Shared Library

We define a simple module that contains a particular subset of Fortran features. Many of them are exported with unmangled names (via bind(c)) so the ABI can be used in a predictable way independent of the compiler or platform.

Public Interface

int KNOB

The first public symbol is a mutable global:

Fortran Implementation:

integer(c_int) :: KNOB = 1337

KNOB does not have a bound name (i.e. it may be mangled) and it is expected to be accessed via a public getter and setter.

void turn_knob(int *new_value)

Fortran Implementation:

! NOTE: This is missing ``bind(c, name='view_knob')`` because
!       the ``f2py`` parser has issues for some reason.
pure function view_knob() result(knob_value)
  integer(c_int) :: knob_value

  knob_value = KNOB

end function view_knob

subroutine turn_knob(new_value) bind(c, name='turn_knob')
  integer(c_int), intent(in) :: new_value

  KNOB = new_value

end subroutine turn_knob

C Signature:

void turn_knob(int *new_value);

As noted in the remark, the view_knob() getter also does not have a bound name because of the way a function (vs. a subroutine) is parsed by f2py (an issue has been filed).

In addition to a mutable global, there are two user defined types exposed and unmangled, hence each acts as a C struct.

UserDefined

Fortran Implementation:

type, bind(c) :: UserDefined
   real(c_double) :: buzz
   real(c_double) :: broken
   integer(c_int) :: how_many
end type UserDefined

C Signature:

typedef struct UserDefined {
  double buzz;
  double broken;
  int how_many;
} UserDefined;
double buzz
double broken
int how_many
DataContainer

Fortran Implementation:

type, bind(c) :: DataContainer
   real(c_double) :: data(4, 2)
end type DataContainer

C Signature:

typedef struct DataContainer { double data[8]; } DataContainer;
double[8] data
void foo(double bar, double baz, double *quux)

The first subroutine exported by the public interface is an implementation of f(x, y) = x + 3.75 y.

Fortran Implementation:

subroutine foo(bar, baz, quux) bind(c, name='foo')
  real(c_double), intent(in), value :: bar, baz
  real(c_double), intent(out) :: quux

  quux = bar + 3.75_dp * baz

end subroutine foo

C Signature:

void foo(double bar, double baz, double *quux);

It accepts the inputs by value. Since pass-by-reference is the default behavior, an equivalent method is provided (though not as part of the unmangled ABI):

subroutine foo_by_ref(bar, baz, quux)
  real(dp), intent(in) :: bar, baz
  real(dp), intent(out) :: quux

  call foo(bar, baz, quux)

end subroutine foo_by_ref
void foo_array(int *size, double *val, double *two_val)

Next, we define a method that accepts a variable size array and places twice the values of that array in the return value:

Fortran Implementation:

subroutine foo_array(size_, val, two_val) bind(c, name='foo_array')
  integer(c_int), intent(in) :: size_
  real(c_double), intent(in) :: val(size_, 2)
  real(c_double), intent(out) :: two_val(size_, 2)

  two_val = 2.0_dp * val

end subroutine foo_array

C Signature:

void foo_array(int *size, double *val, double *two_val);
void make_udf(double *buzz, double *broken, int *how_many, UserDefined *quuz)

The next subroutine creates an instance of the UserDefined data type, but smuggles the result out as raw bytes. The total size is size(buzz) + size(broken) + size(how_many) = 2 c_double + c_int. This is 20 bytes on most platforms, but as a struct it gets padded to 24 due to word size.

Fortran Implementation:

subroutine make_udf(buzz, broken, how_many, as_bytes) bind(c, name='make_udf')
  real(c_double), intent(in) :: buzz, broken
  integer(c_int), intent(in) :: how_many
  character(c_char), intent(out) :: as_bytes(24)
  ! Outside of signature
  type(UserDefined), target :: made_it

  made_it%buzz = buzz
  made_it%broken = broken
  made_it%how_many = how_many

  ! NOTE: We need ``sizeof(as_bytes) == sizeof(made_it)``
  as_bytes = transfer(made_it, as_bytes)

end subroutine make_udf

C Signature:

void make_udf(double *buzz, double *broken, int *how_many, UserDefined *quuz);

This concept of “data smuggling” is necessary for the use of user defined types with f2py, since it has no support for them.

void udf_ptr(intptr_t *ptr_as_int)

A related way to smuggle data for use with f2py is to allocate memory for the struct and then pass a pointer to that memory as an opaque integer. Once this is done, the Fortran subroutine can convert the integer into a Fortran pointer and then write to the memory location owned by the foreign caller:

Fortran Implementation:

subroutine udf_ptr(ptr_as_int) bind(c, name='udf_ptr')
  integer(c_intptr_t), intent(in) :: ptr_as_int
  ! Outside of signature
  type(c_ptr) :: made_it_ptr
  type(UserDefined), pointer :: made_it

  made_it_ptr = transfer(ptr_as_int, made_it_ptr)
  call c_f_pointer(made_it_ptr, made_it)

  made_it%buzz = 3.125_dp
  made_it%broken = -10.5_dp
  made_it%how_many = 101

end subroutine udf_ptr

C Signature:

void udf_ptr(intptr_t *ptr_as_int);

This approach is problematic because it is so brittle. The memory must be handled by the caller rather than by Fortran directly. If the subroutine were responsible for the memory, the object would likely be allocated on the stack and the memory location re-used by subsequent calls to the subroutine.

void make_container(double *contained, DataContainer *container)

The next subroutine takes an array as input and sets the data attribute of a returned DataContainer instance as the input. This acts as a check that the operation happens as a data copy rather than a reference copy.

Fortran Implementation:

subroutine make_container(contained, container) &
     bind(c, name='make_container')
  real(c_double), intent(in) :: contained(4, 2)
  type(DataContainer), intent(out) :: container

  container%data = contained

end subroutine make_container

C Signature:

void make_container(double *contained, DataContainer *container);
void just_print(void)

The just_print() subroutine simply prints characters to the screen. However, printing requires libgfortran, which slightly complicates foreign usage.

Fortran Implementation:

subroutine just_print() bind(c, name='just_print')

  print *, "======== BEGIN FORTRAN ========"
  print *, "just_print() was called"
  print *, "========  END  FORTRAN ========"

end subroutine just_print

C Signature:

void just_print(void);

Object File

For some foreign usage of example, we’ll directly use a compiled object file. To create example.o:

$ gfortran \
>   -J fortran/ \
>   -c fortran/example.f90 \
>   -o fortran/example.o

Shared Object

It’s more common for foreign usage of native code to be done via a shared object file:

$ gfortran \
>   -shared -fPIC \
>   -J fortran/ \
>   fortran/example.f90 \
>   -o fortran/example.so

Here, we manually build a “position independent” shared library in the same directory as the source. However, in many cases, native code comes with an installer that puts the library in a standard place, e.g. a symlink to libatlas can be found in /usr/lib/libatlas.so. Shared object files are typically named lib{pkg}.so so that they can be included by the compiler with -l{pkg}. The compiler uses a default list of “search directories” when finding such shared libraries.

References

  • Examples of user-defined types
  • StackOverflow question about user-defined types
  • The sphinx-fortran project was started to provide autodoc capabilities for Fortran libraries, but it is not actively maintained (as of this writing, August 2018)
  • The AutoAPI redesign of autodoc will hopefully mature into a capable way of documenting Fortran code (and code from other languages) using Sphinx
  • The FORD (FORtran Documentation) project is a modern way to generate documentation for Fortran code, though it is “Yet Another” documentation generator (example documentation)
  • The breathe project / library seeks to be a bridge Python XML-based doxygen and Sphinx, though in practice the formatting of doxygen produced documentation is not in line with typical Sphinx documentation