9 minutes

Written: 2021-10-02 04:19 +0000

Updated: 2024-08-06 00:53 +0000

# Simple Fortran Derived Types and Python

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

Moving simple Fortran derived types to Python and back via C

## Background

Object oriented programming has been part of Fortran for longer than I
have been alive ^{1}. Fortran has derived types now. They’ve been
around for around for over three decades. The standards at the same
time, have been supporting more and more interoperable operations.
Details of these pleasant historical improvements are pretty much the
most the Fortran standards committee have managed to date in the 21st
century. The point of this exploration is to design interfaces to
`Python`

, mostly for the long term advancement of `f2py`

, and just for
the heck of it. Simple in the context of the title means that we
consider only derived types whose members have direct `C`

equivalents as
defined in the Fortran 2003 Reid
(2003) standard.

## C Structures and Derived Types - I

Consider, first an example of what shall be a rather simple collection of records, the prototypical darling of most simulations, the point in Euclidean space, \(\mathbb{R}^{3}\).

```
1type, bind(c) :: cartesian
2 real(c_double) :: x,y,z
3end type cartesian
```

Now by definition, ever since the `ISO_C_BINDING`

of the Fortran 2003
Reid (2003) standard, we know this is
equivalent to:

```
1typedef struct {
2 double x,y,z;
3} cartesian;
```

To try this out, we need to have a procedure which operates on the type, due to a chronic lack of imagination, let us simply take a point and add one to each axis.

```
1subroutine unit_move(cartpoint) bind(c)
2 type(cartesian), intent(inout) :: cartpoint
3 print*, "Modifying the derived type now!"
4 cartpoint%x=cartpoint%x+1
5 cartpoint%y=cartpoint%y+1
6 cartpoint%z=cartpoint%z+1
7end subroutine unit_move
```

Now we can wrap these into a nice little module:

```
1module vec
2 use, intrinsic :: iso_c_binding
3 implicit none
4
5 type, bind(c) :: cartesian
6 real(c_double) :: x,y,z
7 end type cartesian
8
9 contains
10
11 subroutine unit_move(array) bind(c)
12 type(cartesian), intent(inout) :: array
13 print*, "Modifying the derived type now!"
14 array%x=array%x+1
15 array%y=array%y+1
16 array%z=array%z+1
17 end subroutine unit_move
18
19end module vec
```

Call it with a simple `main`

.

```
1! main.f90
2program main
3 use, intrinsic :: iso_c_binding
4 use vec
5 implicit none
6 type(cartesian) :: cartvec
7 cartvec = cartesian(0.2,0.5,0.3)
8 print*, cartvec
9 call unit_move(cartvec)
10 print*, cartvec
11end program main
```

Finally we need a build system, lets say `meson`

.

```
1# meson.build
2project('fcbase', 'fortran',
3 version : '0.1',
4 default_options : ['warning_level=3'])
5
6executable('fcbase',
7 'vec.f90',
8 'main.f90',
9 install : true)
```

Now we can finally try this out.

```
1meson setup testfc
2meson compile -C testfc
3./testfc/fcbase
4 0.20000000298023224 0.50000000000000000 0.30000001192092896
5 Modifying the derived type now!
6 1.2000000029802322 1.5000000000000000 1.3000000119209290
```

This probably surprised no one. So let’s get to something more interesting.

### C and Fortran

We can start with a simple `C`

header.

```
1#ifndef VECFC_H
2#define VECFC_H
3
4typedef struct {
5 double x,y,z;
6} cartesian;
7
8#endif /* VECFC_H */
```

Along with its associated translation unit.

```
1/* vecfc.c */
2#include<stdlib.h>
3#include<stdio.h>
4#include<vecfc.h>
5
6void* unit_move(cartesian *word);
7
8int main(int argc, char* argv[argc+1]) {
9 puts("Initializing the struct");
10 cartesian a={3.0, 2.5, 6.2};
11 printf("%f %f %f",a.x,a.y,a.z);
12 puts("\nFortran function with derived type from C:");
13 unit_move(&a);
14 puts("\nReturned from Fortran");
15 printf("%f %f %f",a.x,a.y,a.z);
16 return EXIT_SUCCESS;
17}
```

The function declaration is straightforward, but for the fact that since
we are passing something both in and out of the function, it must be a
pointer, and as we have no real error propagation or feasible return
type, `void *`

makes sense too. The calling semantics are equally
transparent, creating an instance of the `struct`

and then passing it by
reference.

The meson extension is fairly trivial, we only need to add the right file and declare the project language correctly.

```
1project('cderived', 'c', 'fortran',
2 version : '0.1',
3 default_options : ['warning_level=3'])
4
5executable('cderived',
6 'vec.f90',
7 # 'vecfc.c',
8 # 'vecfc.h',
9 'main.f90',
10 install : true)
```

This seems to work.

```
1meson setup testfc
2meson compile -C testfc
3./testfc/fcbase
4Initializing the struct
53.000000 2.500000 6.200000
6Fortran function with derived type from C:
7 Modifying the derived type now!
8
9Returned from Fortran
104.000000 3.500000 7.200000%
```

### Cython and Fortran

So far so good. Let’s get to something slightly more interesting.
Although in the long run `cython`

isn’t really the approach `numpy`

is
going towards at the moment, it still makes sense to give it a shot as a
first approximation.

```
1cdef struct cartesian:
2 double x,y,z
3
4cdef extern:
5 void* unit_move(cartesian *word)
6
7def unitmv(dpt={'x':0,'y':0,'z':0}):
8 cdef cartesian A=dpt
9 unit_move(&A)
10 return(A)
```

The most interesting aspect is that, having defined our `C`

structure
and the external function, we need something to offer to `Python`

. Since
`cython`

structures can be mapped from simple `python`

dictionaries, we
take an input of one, construct the `structure`

on the fly in the
function and then use our Fortran subroutine. The return type can also
be coerced into a list, and that’s what can be printed or manipulated
further. This isn’t the most efficient way to work with this, but it
will do for now.

The `meson`

update follows from the explanation in the previous post,
namely, that we need to include our `Python`

sources.

```
1project('pointeuc', 'c', 'fortran',
2 version : '0.1',
3 default_options : ['warning_level=3'])
4
5add_languages('cython')
6
7py_mod = import('python')
8py3 = py_mod.find_installation()
9py3_dep = py3.dependency()
10
11incdir_numpy = run_command(py3,
12 ['-c', 'import os; os.chdir(".."); import numpy; print(numpy.get_include())'],
13 check : true
14).stdout().strip()
15
16inc_np = include_directories(incdir_numpy)
17
18py3.extension_module('veccyf',
19 'veccyf.pyx',
20 'vec.f90',
21 include_directories: inc_np,
22 dependencies : py3_dep)
```

The `NumPy`

dependency in this situation is really optional, but makes
for a more interoperable structure. Now we’re ready to try this out in
an interactive manner.

```
1meson setup testcpyth
2meson compile -C testcpyth
3cd testcpyth
4python -c "import veccyf; A=veccyf.unitmv({'x':3.3,'y':5.2,'z':2.6}); print(A)"
5 Modifying the derived type now!
6{'x': 4.3, 'y': 6.2, 'z': 3.6}
```

Taking this a bit slower, it is still as neat as ever.

```
1import veccyf
2point = {'x': 4.3,
3 'y': 2.6,
4 'z': 6.4}
5opoint = veccyf.unitmv(point)
6print(opoint)
```

Aside from the output string, this works as expected, and is pretty
neat, all things considered. This might feed into an `f2py`

workflow,
but then, it adds a layer of indirection which seems overly circuitous.
The design could have also been far improved by using a
`bind(c, name=uniquname)`

as well.

Note that a much more rational approach would have been to generate a class in Python.

#### Prognosis for F2PY

The essential information used from the `fortran`

code for the `cython`

types (and `C`

structures) was essentially:

- The types of each variable for the derived type
`double`

for every one of them in this case

- The names of each variable
`x`

,`y`

, and`z`

here

Apart from this, for functions which operate on aforementioned types,we require:

- The external declaration (due to the
`iso_c_binding`

) - A mapping from structure to a python object, typically a dictionary

It so happens that much of the information needed for generation of the
relevant `C`

code is already present in `f2py`

, and can be inspected via
the `crackfortran`

module.

```
1# python -m numpy.f2py.crackfortran vec.f90 -show
2Reading fortran codes...
3 Reading file 'vec.f90' (format:free)
4{'before': '', 'this': 'use', 'after': ', intrinsic :: iso_c_binding '}
5Line #2 in vec.f90:" use, intrinsic :: iso_c_binding "
6 analyzeline: Could not crack the use statement.
7Line #5 in vec.f90:" type, bind(c) :: cartesian "
8 analyzeline: No name/args pattern found for line.
9Post-processing...
10 Block: vec
11 Block: unknown_type
12 Block: unit_move
13Post-processing (stage 2)...
14[{'args': [],
15 'block': 'module',
16 'body': [{'args': [],
17 'block': 'type',
18 'body': [],
19 'entry': {},
20 'externals': [],
21 'from': 'vec.f90:vec',
22 'interfaced': [],
23 'name': 'unknown_type',
24 'parent_block': <Recursion on dict with id=4500228096>,
25 'sortvars': ['x', 'y', 'z'],
26 'varnames': ['x', 'y', 'z'],
27 'vars': {'x': {'kindselector': {'kind': 'c_double'},
28 'typespec': 'real'},
29 'y': {'kindselector': {'kind': 'c_double'},
30 'typespec': 'real'},
31 'z': {'kindselector': {'kind': 'c_double'},
32 'typespec': 'real'}}},
33 {'args': ['array'],
34 'block': 'subroutine',
35 'body': [],
36 'entry': {},
37 'externals': [],
38 'from': 'vec.f90:vec',
39 'interfaced': [],
40 'name': 'unit_move',
41 'parent_block': <Recursion on dict with id=4500228096>,
42 'saved_interface': '\n'
43 ' subroutine unit_move(array) \n'
44 ' type(cartesian), intent(inout) :: '
45 'array\n'
46 ' end subroutine unit_move',
47 'sortvars': ['array'],
48 'vars': {'array': {'attrspec': [],
49 'intent': ['inout'],
50 'typename': 'cartesian',
51 'typespec': 'type'}}}],
52 'entry': {},
53 'externals': [],
54 'from': 'vec.f90',
55 'implicit': None,
56 'interfaced': [],
57 'name': 'vec',
58 'sortvars': [],
59 'vars': {}}]
```

The information is clearly present, which brings us to the next phase.

### Python-C and Fortran

The most natural way to feed into `f2py`

would be to directly write out
an equivalent `Python-C`

API which effectively mimics the extension
module of the last section.

The crucial definition essentially takes a dictionary, unrolls into a
list, creates the `C`

structure, then passes it through Fortran and then
back.

```
1static PyObject* eucli_unitinc(PyObject* self, PyObject* args) {
2 cartesian a;
3 PyObject* dict;
4 PyArg_ParseTuple(args, "O", &dict);
5 PyObject* vals = PyDict_Values(dict);
6 a.x = PyFloat_AsDouble(PyList_GetItem(vals,0));
7 a.y = PyFloat_AsDouble(PyList_GetItem(vals,1));
8 a.z = PyFloat_AsDouble(PyList_GetItem(vals,2));
9 // Call Fortran on it
10 unit_move(&a);
11 //
12 PyObject* ret = Py_BuildValue("{s:f,s:f,s:f}",
13 "x", a.x,
14 "y", a.y,
15 "z", a.z);
16 return ret;
17}
```

This also works well enough with minor modifications to `meson`

.

```
1python -c "import eucli; A=eucli.unitinc({'x':3.3,'y':5.2,'z':2.6}); print(A)"
2 Modifying the derived type now!
3{'x': 4.3, 'y': 6.2, 'z': 3.6}
```

#### Boilerplate

The rest of the hand written wrapper is boilerplate, but the full file is reproduced here:

```
1/* Make "s#" use Py_ssize_t rather than int. */
2#ifndef PY_SSIZE_T_CLEAN
3#define PY_SSIZE_T_CLEAN
4#include <stdio.h>
5#endif /* PY_SSIZE_T_CLEAN */
6
7#include "Python.h"
8
9// Fortran-C stuff
10
11typedef struct {
12 double x,y,z;
13} cartesian;
14
15void* unit_move(cartesian *word);
16
17// Python-C stuff
18
19static PyObject* eucli_unitinc(PyObject* self, PyObject* args) {
20 cartesian a;
21 PyObject* dict;
22 PyArg_ParseTuple(args, "O", &dict);
23 PyObject* vals = PyDict_Values(dict);
24 a.x = PyFloat_AsDouble(PyList_GetItem(vals,0));
25 a.y = PyFloat_AsDouble(PyList_GetItem(vals,1));
26 a.z = PyFloat_AsDouble(PyList_GetItem(vals,2));
27 // Call Fortran on it
28 unit_move(&a);
29 //
30 PyObject* ret = Py_BuildValue("{s:f,s:f,s:f}",
31 "x", a.x,
32 "y", a.y,
33 "z", a.z);
34 return ret;
35}
36
37static PyMethodDef EucliMethods[] = {
38 {"unitinc", (PyCFunction)eucli_unitinc, METH_VARARGS,
39 "Add 1 to the axes of a point"},
40 {NULL, NULL, 0, NULL} /* Sentinel */
41};
42
43static struct PyModuleDef eucli_module = {
44 PyModuleDef_HEAD_INIT,
45 "eucli",
46 "blah",
47 -1,
48 EucliMethods
49};
50
51PyMODINIT_FUNC PyInit_eucli(void)
52{
53 PyObject *m;
54 m = PyModule_Create(&eucli_module);
55 if (!m) {
56 return NULL;
57 }
58
59 return m;
60}
```

## Conclusions

Slowly but surely, compilers have caught up with to the standards, and
soon, with some support, `numpy`

shall too. A subsequent post shall
cover more exotic derived types, typically (until 2018) treated by
non-portable opaque pointer tricks. There are still some rather awkward
design decisions in this case; including things like considering the
mapping and their interoperability with `NumPy`

arrays, but by and large
this would depend on the functions which use these. In terms of derived
types of simple structures, without allocatable arrays, the extensions
appear to be straightforward.

## References

Reid, John. 2003. “The New Features of Fortran 2003,” 38. https://wg5-fortran.org/N1551-N1600/N1579.pdf.

This is more surprising with each passing year ↩︎

## Series info

### Bridging Fortran & Python series

- NumPy, Meson and f2py
**Simple Fortran Derived Types and Python**<-- You are here!- Exploring ISO_C_BINDING and type-bound procedures
- Fortran OOP and Python
- Types from Fortran to Python via Opaque Pointers