This post is part of the Bridging Fortran & Python series.

An exploration of the opaque object and pointer interface approach to unleash more modern features than those offered by the ISO Fortran standard.

Background

Since this has been covered a few times before, just some quick pointers1. The ultimate context is to be able take nice, modern Fortran code with derived types and generate equally nice, user friendly, efficient Python wrappers via f2py.

Why?

Where the last post (and the test implementation in f2py) left off, the type shadowing approach for having exactly interoperable bind(c) derived types (and functions) was covered. However, there are several aspects of derived types which cannot be covered (“deep” types in the nomenclature of Pletzer et al. (2008)). A brief list:

  • Types with allocatable members
  • Types with other types as members
  • Types which have bound proceedures
  • Pointer elements
  • Child types, in an inheritance hierarchy

Of these, the first three are probably the most common use cases, which need to be addressed by any serious attempt to bride Fortran and Python.

Scope

This post is just going to cover the development of Python examples facilitated by the pointer implementation where:

  • A simple derived type, point, is manipulated via pointers

We will first demonstrate a Fortran example, followed by its call from C, before demonstrating its bridging Python-C code and finally a user-facing python example. The full code may be found in the examples folder and pull requests here. Typically the snippets here will have elided error handling to drive home the approach over the boilerplate / best practices 2.

Approaches to an automated method

The main idea here is a separation of concerns at three levels:

  • Fortran code handles its own memory / functionality
    • Wrappers are exposed at two levels:
      • One layer to provide pointer “handles” to each part of the type.
      • Another outer layer connecting to bind(c) proceedures.
  • C bindings call the bind(c) proceedures and pass handles (opaque pointers) as void*.
  • At the python level, these are stored in PyCapsule instances, and exposed to the user as a standard Python class.

The bind(c) proceedures are meant to separate the implementation from the C-interface in a manner which can be automated (eventually) by f2py. Similarly, the Python-C code is eventually to be generated as well.

point with pointers

We will start with the prototypical class in applications, a point, with 2 dimensions.

Fortran baseline

The type we want is very minimal:

1type :: Point
2   real(8) :: x
3   real(8) :: y
4end type Point

However, since we need to use this via pointers, we need a more involved set of helpers for this:

1subroutine create_point(x, y, p)
2   real(8), intent(in) :: x, y
3   type(Point), pointer :: p
4   allocate (p)
5   p%x = x
6   p%y = y
7end subroutine create_point

There isn’t much to say about these, and they are rather mechanically derived.

Building and testing

For the toy point we have:

 1module point_module
 2   implicit none
 3   type :: Point
 4      real(8) :: x
 5      real(8) :: y
 6   end type Point
 7contains
 8   subroutine create_point(x, y, p, alloc_stat)
 9      real(8), intent(in) :: x, y
10      type(Point), pointer :: p
11      integer, intent(out), optional :: alloc_stat
12      allocate (p, stat=alloc_stat)
13      if (present(alloc_stat) .and. alloc_stat /= 0) then
14         return
15      end if
16      p%x = x
17      p%y = y
18   end subroutine create_point
19
20   subroutine destroy_point(p)
21      type(Point), pointer :: p
22      if (associated(p)) then
23         deallocate (p)
24         p => null()
25      end if
26   end subroutine destroy_point
27
28   function get_x(p) result(x_value)
29      type(Point), intent(in) :: p
30      real(8) :: x_value
31      x_value = p%x
32   end function get_x
33
34   subroutine set_x(p, x)
35      type(Point), intent(inout) :: p
36      real(8), intent(in) :: x
37      p%x = x
38   end subroutine set_x
39
40   function get_y(p) result(y_value)
41      type(Point), intent(in) :: p
42      real(8) :: y_value
43      y_value = p%y
44   end function get_y
45
46   subroutine set_y(p, y)
47      type(Point), intent(inout) :: p
48      real(8), intent(in) :: y
49      p%y = y
50   end subroutine set_y
51end module point_module

Now these can be tested from a simple driver:

 1program test_point_module
 2   use point_module
 3   implicit none
 4
 5   type(Point), pointer :: pt
 6   real(8) :: x_val, y_val
 7   integer :: alloc_status
 8
 9   ! Test creating a new point
10   call create_point(1.0d0, 2.0d0, pt, alloc_status)
11
12   if (.not. associated(pt)) then
13      print *, "Point creation failed with status ", alloc_status
14      stop
15   else
16      print *, "Point created: (", pt%x, ", ", pt%y, ")"
17   end if
18
19   ! Test setters and getters
20   call set_x(pt, 5.0d0)
21   call set_y(pt, 10.0d0)
22   x_val = get_x(pt)
23   y_val = get_y(pt)
24
25   print *, "After setting new values:"
26   print *, "x =", x_val, "y =", y_val
27
28   if (x_val == 5.0d0 .and. y_val == 10.0d0) then
29      print *, "Point values updated correctly."
30   else
31      print *, "Error in updating point values."
32   end if
33
34   ! Test destruction
35   call destroy_point(pt)
36
37   if (.not. associated(pt)) then
38      print *, "Point destroyed successfully."
39   else
40      print *, "Error in destroying point."
41   end if
42end program test_point_module

Personally I use meson for everything, but this is trivially compiled anyway:

1❯ gfortran point.f90 ftest.f90
2❯ ./a.out
3 Point created: (   1.0000000000000000      ,    2.0000000000000000      )
4 After setting new values:
5 x =   5.0000000000000000      y =   10.000000000000000
6 Point values updated correctly.
7 Point destroyed successfully.

Just to build on for when it gets less trivial, the meson equivalent is:

1project('pyclass_bindc', 'c', 'fortran',
2        version : '0.1',
3        default_options : ['warning_level=2',
4                           'buildtype=debug',
5                           'debug=true',
6                          ])
7executable('ftest',
8           'ftest.f90',
9           'point.f90')

Which can be used with:

1meson setup bbdir
2meson compile -C bbdir
3❯ ./bbdir/ftest
4 Point created: (   1.0000000000000000      ,    2.0000000000000000      )
5 After setting new values:
6 x =   5.0000000000000000      y =   10.000000000000000
7 Point values updated correctly.
8 Point destroyed successfully.

C-Wrappers

Lets focus a bit on what we need to do, creation and destruction are fairly straightforward since we just need to remember to return a c_ptr and create interface it to our existing functions..

 1function c_new_point(x, y) result(cnewpt) bind(c)
 2   real(c_double), value :: x, y !! important
 3   type(c_ptr) :: cnewpt
 4   type(Point), pointer :: p
 5   integer :: stat
 6   call create_point(x, y, p, stat)
 7   if (stat /= 0) then
 8      print *, "Allocation failed"
 9      cnewpt = C_NULL_PTR
10      return
11   end if
12   cnewpt = c_loc(p)
13end function c_new_point

Note the use of the c_loc intrinsic which is used to get a pointer compatible with C as required by the return type too. For bindings, it is never a good idea to neglect error handling, so we do guard for allocation failures, using the NULL helper in the iso_c intrinsic. The value attribute is important enough (and a recurring theme) so it is relegated to the box.

Warning

The value attribute is required in the to ensure data is copied and interpreted correctly from C to Fortran.

Similar considerations apply to the rest of the bindings, the full source of which is reproduced at the end of this section.

To call these from C we need to remember that we want to grab a pointer to a pointer, that is, the signatures will be void**. So we have:

1extern void *c_new_point(double x, double y);
2extern void c_delete_point(void **p);
3extern double c_get_x(void **p);
4extern void c_set_x(void **p, double x);
5extern double c_get_y(void **p);
6extern void c_set_y(void **p, double y);

bind(C) is on the Fortran side is used to ensure no name mangling.

Building and testing

The additional wrapper file is:

 1! cwrappers.f90
 2! C interoperable wrappers
 3module c_wrappers
 4   use, intrinsic :: iso_c_binding, only: c_ptr, c_double, c_null_ptr, &
 5                                                                             c_loc, c_associated, c_f_pointer
 6   use point_module
 7   implicit none
 8
 9contains
10
11   ! Wrapper for creating a new point
12   function c_new_point(x, y) result(cnewpt) bind(c)
13      real(c_double), value :: x, y
14      type(c_ptr) :: cnewpt
15      type(Point), pointer :: p
16      integer :: stat
17      call create_point(x, y, p, stat)
18      if (stat /= 0) then
19         print *, "Allocation failed"
20         cnewpt = C_NULL_PTR
21         return
22      end if
23
24      cnewpt = c_loc(p)
25   end function c_new_point
26
27   ! Wrapper to set the x value of a point
28   subroutine c_set_x(p_cptr, x) bind(c)
29      type(c_ptr), intent(in) :: p_cptr
30      real(c_double), intent(in), value :: x
31      type(Point), pointer :: p
32
33      call c_f_pointer(p_cptr, p)
34      call set_x(p, x)
35   end subroutine c_set_x
36
37   ! Wrapper to get the x value of a point
38   function c_get_x(p_cptr) bind(c) result(x)
39      type(c_ptr), intent(in) :: p_cptr
40      real(c_double) :: x
41      type(Point), pointer :: p
42
43      call c_f_pointer(p_cptr, p)
44      x = get_x(p)
45   end function c_get_x
46
47   ! Wrapper to set the y value of a point
48   subroutine c_set_y(p_cptr, y) bind(c, name='c_set_y')
49      type(c_ptr), intent(in) :: p_cptr
50      real(c_double), intent(in), value :: y
51      type(Point), pointer :: p
52
53      call c_f_pointer(p_cptr, p)
54      call set_y(p, y)
55   end subroutine c_set_y
56
57   ! Wrapper to get the y value of a point
58   function c_get_y(p_cptr) bind(c, name='c_get_y') result(y)
59      type(c_ptr), intent(in) :: p_cptr
60      real(c_double) :: y
61      type(Point), pointer :: p
62
63      call c_f_pointer(p_cptr, p)
64      y = get_y(p)
65   end function c_get_y
66
67   ! Wrapper for deallocating a point
68   subroutine c_delete_point(p_cptr) bind(c, name='c_delete_point')
69      type(c_ptr), intent(inout) :: p_cptr
70      type(Point), pointer :: p
71
72      if (.not. c_associated(p_cptr)) return
73      call c_f_pointer(p_cptr, p)
74      call destroy_point(p)
75      p_cptr = c_null_ptr
76   end subroutine c_delete_point
77
78end module c_wrappers

With the corresponding C file to test:

 1#include <stdio.h>
 2
 3/* Declarations of Fortran functions */
 4extern void *c_new_point(double x, double y);
 5extern void c_delete_point(void **p);
 6extern double c_get_x(void **p);
 7extern void c_set_x(void **p, double x);
 8extern double c_get_y(void **p);
 9extern void c_set_y(void **p, double y);
10
11int main() {
12  void *point = c_new_point(3.0, 4.0);
13  if (point == NULL) {
14    fprintf(stderr, "Failed to create point\n");
15    return 1;
16  }
17  /* Retrieve the x and y values using the Fortran function */
18  double x = c_get_x(&point);
19  double y = c_get_y(&point);
20
21  printf("Point coordinates: x = %f, y = %f\n", x, y);
22
23  /* Set the x and y values using the Fortran function */
24  c_set_x(&point, 5.0);
25  c_set_y(&point, 6.0);
26
27  printf("Point coordinates after set: x = %f, y = %f\n", c_get_x(&point),
28         c_get_y(&point));
29
30  /* Clean up and delete the point using the Fortran function */
31  c_delete_point(&point);
32
33  return 0;
34}

Which can be compiled and run with the following addition to the existing meson.build:

1executable('ctestf',
2           'testc.c',
3           'point.f90',
4           'cwrappers.f90')

With:

1meson compile -C bbdir
2./bbdir/ctestf
3Point coordinates: x = 3.000000, y = 4.000000
4Point coordinates after set: x = 5.000000, y = 6.000000

Python-C Interface

The key take-away here is to use a PyCapsule object which:

represents an opaque value, useful for C extension modules who need to pass an opaque value (as a void* pointer) through Python code to other C code.

In our case the opaque value needs to travel a bit further along and go through to Fortran and back, but the principle (and implementation) is the same. The C function calls will be the same, the only difference being the structure used, unlike in “shadowing” approaches, the struct will have neither C interop nor Fortran information. So we start with:

1/* Point object definition */
2typedef struct {
3  PyObject_HEAD
4  PyObject *capsule;
5} PyPointObject;

This is simplicity itself. Naturally we need to actually grab the right object from the capsule before use. Additionally, there are a few nuances, e.g. before defining creation we need to define cleanup:

1void point_capsule_destructor(PyObject *capsule) {
2  void *point = PyCapsule_GetPointer(capsule, "point._Point");
3  if (point) {
4    c_delete_point(&point);
5  }
6}

Creation is simply via PyCapsule_New once appropriate steps (unpacking, error handling) are taken.

 1static PyObject *PyPoint_new(PyTypeObject *type, PyObject *args,
 2                             PyObject *kwds) {
 3  double x, y;
 4  if (!PyArg_ParseTuple(args, "dd", &x, &y)) {
 5    return NULL;
 6  }
 7
 8  PyPointObject *self = (PyPointObject *)type->tp_alloc(type, 0);
 9  if (self != NULL) {
10    void *point = c_new_point(x, y);
11    if (point == NULL) {
12      Py_DECREF(self);
13      return PyErr_NoMemory();
14    }
15    self->capsule =
16        PyCapsule_New(point, "point._Point", point_capsule_destructor);
17    if (self->capsule == NULL) {
18      c_delete_point(&point);
19      Py_DECREF(self);
20      return NULL;
21    }
22  }
23  return (PyObject *)self;
24}

Since the rest of this is standard, including PyFloat_FromDouble and the rest of the C-Python API, we will move onto the building and testing phase.

Building and testing

The full file, including boilerplate defining the module (detailed in earlier posts) is:

  1#ifndef PY_SSIZE_T_CLEAN
  2#define PY_SSIZE_T_CLEAN
  3#include <stdio.h>
  4#endif /* PY_SSIZE_T_CLEAN */
  5
  6#include "Python.h"
  7#include "point.h"
  8#include "structmember.h"
  9
 10void point_capsule_destructor(PyObject *capsule) {
 11  void *point = PyCapsule_GetPointer(capsule, "point._Point");
 12  if (point) {
 13    c_delete_point(&point);
 14  }
 15}
 16
 17/* Point object definition */
 18typedef struct {
 19  PyObject_HEAD
 20  PyObject *capsule;
 21} PyPointObject;
 22
 23/* Deallocates the PyPointObject */
 24static void PyPoint_dealloc(PyPointObject *self) {
 25  Py_XDECREF(self->capsule);
 26  Py_TYPE(self)->tp_free((PyObject *)self);
 27}
 28
 29/* Creates a new PyPointObject */
 30static PyObject *PyPoint_new(PyTypeObject *type, PyObject *args,
 31                             PyObject *kwds) {
 32  double x, y;
 33  if (!PyArg_ParseTuple(args, "dd", &x, &y)) {
 34    return NULL;
 35  }
 36
 37  PyPointObject *self = (PyPointObject *)type->tp_alloc(type, 0);
 38  if (self != NULL) {
 39    void *point = c_new_point(x, y);
 40    if (point == NULL) {
 41      Py_DECREF(self);
 42      return PyErr_NoMemory();
 43    }
 44    self->capsule =
 45        PyCapsule_New(point, "point._Point", point_capsule_destructor);
 46    if (self->capsule == NULL) {
 47      c_delete_point(&point);
 48      Py_DECREF(self);
 49      return NULL;
 50    }
 51  }
 52  return (PyObject *)self;
 53}
 54
 55/* Getter for the 'x' attribute */
 56static PyObject *PyPoint_getx(PyPointObject *self, void *closure) {
 57  void *point = PyCapsule_GetPointer(self->capsule, "point._Point");
 58  double x = c_get_x(&point);
 59  return PyFloat_FromDouble(x);
 60}
 61
 62/* Setter for the 'x' attribute */
 63static int PyPoint_setx(PyPointObject *self, PyObject *value, void *closure) {
 64  if (value == NULL) {
 65    PyErr_SetString(PyExc_TypeError, "Cannot delete the x attribute");
 66    return -1;
 67  }
 68  if (!PyFloat_Check(value)) {
 69    PyErr_SetString(PyExc_TypeError, "The x attribute value must be a float");
 70    return -1;
 71  }
 72  double x = PyFloat_AsDouble(value);
 73  void *point = PyCapsule_GetPointer(self->capsule, "point._Point");
 74  c_set_x(&point, x);
 75  return 0;
 76}
 77
 78/* Getter for the 'y' attribute */
 79static PyObject *PyPoint_gety(PyPointObject *self, void *closure) {
 80  void *point = PyCapsule_GetPointer(self->capsule, "point._Point");
 81  double y = c_get_y(&point);
 82  return PyFloat_FromDouble(y);
 83}
 84
 85/* Setter for the 'y' attribute */
 86static int PyPoint_sety(PyPointObject *self, PyObject *value, void *closure) {
 87  if (value == NULL) {
 88    PyErr_SetString(PyExc_TypeError, "Cannot delete the y attribute");
 89    return -1;
 90  }
 91  if (!PyFloat_Check(value)) {
 92    PyErr_SetString(PyExc_TypeError, "The y attribute value must be a float");
 93    return -1;
 94  }
 95  double y = PyFloat_AsDouble(value);
 96  void *point = PyCapsule_GetPointer(self->capsule, "point._Point");
 97  c_set_y(&point, y);
 98  return 0;
 99}
100
101/* Define the properties of the Point type */
102static PyGetSetDef PyPoint_getseters[] = {
103    {"x", (getter)PyPoint_getx, (setter)PyPoint_setx, "x coordinate", NULL},
104    {"y", (getter)PyPoint_gety, (setter)PyPoint_sety, "y coordinate", NULL},
105    {NULL} /* Sentinel */
106};
107
108/* Initializes the PyPointType object */
109static PyTypeObject PyPointType = {
110    PyVarObject_HEAD_INIT(NULL, 0).tp_name = "point.Point",
111    .tp_doc = "Point objects",
112    .tp_basicsize = sizeof(PyPointObject),
113    .tp_itemsize = 0,
114    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
115    .tp_new = PyPoint_new,
116    .tp_dealloc = (destructor)PyPoint_dealloc,
117    .tp_getset = PyPoint_getseters,
118};
119
120static PyMethodDef point_methods[] = {
121    {NULL, NULL, 0, NULL} /* Sentinel */
122};
123
124/* Initializes the point module */
125static PyModuleDef pointmodule = {
126    PyModuleDef_HEAD_INIT,
127    .m_name = "point",
128    .m_doc = "Module that provides a Point object.",
129    .m_size = -1,
130    .m_methods = point_methods,
131};
132
133PyMODINIT_FUNC PyInit_point(void) {
134  if (PyType_Ready(&PyPointType) < 0) {
135    return NULL;
136  }
137
138  PyObject *m = PyModule_Create(&pointmodule);
139  if (m == NULL) {
140    return NULL;
141  }
142
143  Py_INCREF(&PyPointType);
144  if (PyModule_AddObject(m, "Point", (PyObject *)&PyPointType) < 0) {
145    Py_DECREF(&PyPointType);
146    Py_DECREF(m);
147    return NULL;
148  }
149
150  return m;
151}

Which needs some additions to the meson.build 3

 1py_mod = import('python')
 2py3 = py_mod.find_installation('python3')
 3py3_dep = py3.dependency()
 4message(py3.path())
 5message(py3.get_install_dir())
 6
 7# Python Class Representation
 8py3.extension_module('point',
 9           'point.f90',
10           'cwrappers.f90',
11           'pointcapsule.c',
12           dependencies : py3_dep,
13           install : true)

With a simple pytest in tests/test_simple.py:

1from point import Point
2
3def test_point():
4    p = Point(1, 2)
5    assert p.x == 1
6    assert p.y == 2
7    assert p != Point(1, 2)

Can be compiled and run with:

1meson compile -C bbdir
2export PYTHONPATH=$PYTHONPATH:$(pwd)/bbdir
3pytest

Note that we have not (yet) defined any type bound proceedures, so there are no operations defined between Point objects or anything else.

Binding type bound proceedures

Given the structure discussed so far, adding a type bound proceedure is nearly seamless. We need to modify the type definition.

1type :: Point
2   real(8) :: x
3   real(8) :: y
4contains
5   procedure, pass(self) :: euclidean_distance
6end type Point

Followed by adding a “handle” for it:

1real(8) function euclidean_distance(self, other) result(distance)
2   class(Point), intent(in) :: self, other
3   distance = sqrt((self%x - other%x)**2 + (self%y - other%y)**2)
4end function euclidean_distance

Along with a wrapper:

1function c_euclidean_distance(this_cptr, other_cptr) bind(c) result(y)
2   type(c_ptr), value :: this_cptr, other_cptr
3   real(c_double) :: y
4   type(Point), pointer :: p1, p2
5   call c_f_pointer(this_cptr, p1)
6   call c_f_pointer(other_cptr, p2)
7   y = p1%euclidean_distance(p2)
8end function c_euclidean_distance

Which can be called from C via:

1extern double c_euclidean_distance(void *p1, void *p2);

Finally the python-C code requires only the direct addition of the method:

 1static PyObject *PyPoint_euclidean_distance(PyPointObject *self,
 2                                            PyObject *args) {
 3  PyObject *other;
 4  if (!PyArg_ParseTuple(args, "O", &other)) {
 5    return NULL;
 6  }
 7
 8  /* Check if 'other' is a PyPointObject instance by comparing type names */
 9  if (strcmp(other->ob_type->tp_name, "point.Point") != 0) {
10    PyErr_SetString(PyExc_TypeError, "Argument must be a Point instance");
11    return NULL;
12  }
13
14  /* Ensure 'other' is a capsule before getting the pointer */
15  if (!PyCapsule_CheckExact(((PyPointObject *)other)->capsule)) {
16    PyErr_SetString(PyExc_TypeError, "Argument is not a Point capsule");
17    return NULL;
18  }
19
20  void *self_point = PyCapsule_GetPointer(self->capsule, "point._Point");
21  void *other_point =
22      PyCapsule_GetPointer(((PyPointObject *)other)->capsule, "point._Point");
23
24  if (!self_point || !other_point) {
25    PyErr_SetString(PyExc_RuntimeError, "Failed to get Point from capsule");
26    return NULL;
27  }
28
29  double distance = c_euclidean_distance(self_point, other_point);
30  return PyFloat_FromDouble(distance);
31}
32
33/* Define the methods */
34static PyMethodDef point_methods[] = {
35    {"euclidean_distance", (PyCFunction)PyPoint_euclidean_distance,
36     METH_VARARGS, "Calculate the Euclidean distance to another Point"},
37    {NULL, NULL, 0, NULL} /* Sentinel */
38};

This can now be used to pass a more stringent set of tests:

 1from point import Point
 2
 3def test_point():
 4    p = Point(1, 2)
 5    assert p.x == 1
 6    assert p.y == 2
 7    assert p != Point(1, 2)
 8
 9def test_point_euclid():
10    p = Point(1, 2)
11    p2 = Point(2, 5)
12    assert p.euclidean_distance(p2) == 3.1622776601683795
13
14def test_fails():
15    p = Point(1, 2)
16    with pytest.raises(TypeError) as e_info:
17        p.euclidean_distance(1)
18    assert str(e_info.value) == "Argument must be a Point instance"

Towards allocatable components and f2py support

So far we have not really covered new ground compared to the previous iteration using C compatible struct information. The remarkable extensibility of the pointer approach really shines when we consider adding a new type, with an allocatable component4.

That being said, there is more to be done over on the f2py side before it makes sense to delve too deeply into enhancements on the Fortran-C-Python pipeline. The biggest blocker at the moment is:

  • Suport for C_PTR in f2py
    • This includes void* handling

The exact details shall be covered later, mostly because the Python-C code gets a little rough. It is easier with f2py which provides efficient routines to and from C and Python via numpy arrays but still too long to be in the same post.

Conclusions

More examples notwithstanding, the design discussed here is generic enough to form a more coherent and pragmatic basis for further improvements. Some of these are enumerated below for a future attempt:

  • Circumventing the need to copy data from Python / C to Fortran 5
    • Either by careful usage of transfer or just via c_loc with compatible types
  • Using the CFI descriptors to operate efficiently on discontinous memory 6
    • Actually generally using the CFI descriptors more, since 2018 Reid (2018), some of the pointer highjinks are enshrined as cannon
      • c_loc and c_funloc may have non-interoperable arguments Metcalf, Reid, and Cohen (2018)

All that being said, with meson support in f2py from 1.26 (npgh-24532), basic types based modern Fortran can already be used, so the future remains bright.

References

Metcalf, Michael, John Ker Reid, and Malcolm Cohen. 2018. Modern Fortran Explained: Incorporating Fortran 2018. Numerical Mathematics and Scientific Computation. Oxford [England]: Oxford University Press.

Pletzer, Alexander, Douglas McCune, Stefan Muszala, Srinath Vadlamani, and Scott Kruger. 2008. “Exposing Fortran Derived Types to C and Other Languages.” Computing in Science Engineering 10 (4): 86–92. https://doi.org/10.1109/MCSE.2008.94.

Reid, John. 2018. “The New Features of Fortran 2018.” https://doi.org/10.1145/3206214.3206215.


  1. Pun intended ↩︎

  2. Hence real(8) over the more explicit alternatives ↩︎

  3. It is at this point (linking, handling dependencies) that meson pays off over hand compilation ↩︎

  4. This Fortran-Lang discourse thread includes examples from Ivan Pribec demonstrating this ↩︎

  5. This Fortran-Lang discourse thread includes examples from Ivan Pribec demonstrating this ↩︎

  6. A popular NumPy usage pattern, and a visible bottleneck in interactive workflows ↩︎


Series info

Bridging Fortran & Python series

  1. NumPy, Meson and f2py
  2. Simple Fortran Derived Types and Python
  3. Exploring ISO_C_BINDING and type-bound procedures
  4. Fortran OOP and Python
  5. Types from Fortran to Python via Opaque Pointers <-- You are here!