17 minutes
Written: 2023-11-12 23:36 +0000
Updated: 2024-08-06 00:53 +0000
Types from Fortran to Python via Opaque Pointers
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.
- Wrappers are exposed at two levels:
C
bindings call thebind(c)
proceedures and pass handles (opaque pointers) asvoid*
.- At the
python
level, these are stored inPyCapsule
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
inf2py
- This includes
void*
handling
- This includes
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 viac_loc
with compatible types
- Either by careful usage of
- Using the
CFI
descriptors to operate efficiently on discontinous memory 6
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.
Pun intended ↩︎
Hence
real(8)
over the more explicit alternatives ↩︎It is at this point (linking, handling dependencies) that
meson
pays off over hand compilation ↩︎This Fortran-Lang discourse thread includes examples from Ivan Pribec demonstrating this ↩︎
This Fortran-Lang discourse thread includes examples from Ivan Pribec demonstrating this ↩︎
A popular NumPy usage pattern, and a visible bottleneck in interactive workflows ↩︎
Series info
Bridging Fortran & Python series
- NumPy, Meson and f2py
- Simple Fortran Derived Types and Python
- Exploring ISO_C_BINDING and type-bound procedures
- Fortran OOP and Python
- Types from Fortran to Python via Opaque Pointers <-- You are here!