Skip to content

Commit 63ed6b0

Browse files
committed
Update implementation notes
1 parent 7f46bb2 commit 63ed6b0

2 files changed

Lines changed: 115 additions & 1 deletion

File tree

IMPLEMENTATION-NOTES.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
@Marco please move these into `docs/further-background/wrapping-derived-types.md` (and do any other clean up and formatting fixes you'd like)
2+
3+
Wrapping derived types is tricky.
4+
Notably, [f2py](@Marco please add link) does not provide direct support for it.
5+
As a result, we need to come up with our own solution.
6+
7+
## Our solution
8+
9+
To pass derived types back and forth across the Python-Fortran interface,
10+
we introduce a 'manager' module for all derived types.
11+
This manager module is responsible for managing derived type instances
12+
that are passed across the Python-Fortran interface
13+
and is needed because we can't pass them directly using f2py.
14+
15+
The manager module has two key components:
16+
17+
1. an allocatable array of instances of the derived type it manages
18+
(@Marco note that this isn't how it is implemented now,
19+
but this is how we will end up implementing it)
20+
1. an allocatable array of logical (boolean) values
21+
22+
The array of instances are instances which the manager owns.
23+
It holds onto these: can instantiate them, can make them have the same values
24+
as results from Fortran functions etc.
25+
(@Marco I think we need to decide whether this is an array of instances
26+
or an array of pointers to instances (although I don't think that's a thing https://fortran-lang.discourse.group/t/arrays-of-pointers/4851/6,
27+
so doing something like this might require yet another layer of abstraction).
28+
Array of instances means we have to do quite some data copying
29+
and be careful about changes made on the Fortran side propagating to the Python side,
30+
I think (although we have to test as I don't know enough about whether Fortran is pass by reference or pass by value by default),
31+
array of pointers would mean change propagation should be more automatic.
32+
We're going to have to define our requirements and tests quite carefully then see what works and what doesn't.
33+
I think this is also why I introduced the 'no setters' views,
34+
as some changes just can't be propagated back to Fortran in a 'permanent' way.
35+
We should probably read this and do some thinking: https://stackoverflow.com/questions/4730065/why-does-a-fortran-pointer-require-a-target).
36+
37+
Whenever we need to return a derived type to Python,
38+
we follow a recipe like the below:
39+
40+
1. we firstly ask the manager to give us an index (i.e. an integer) such that `logical_array(index)` is `.false.`.
41+
The convention is that `logical_array(index)` is `.false.` means that `instance_array(index)` is available for use.
42+
1. We set `logical_array(index)` equal to `.true.`, making clear that we are now using `instance_array(index)`
43+
1. We set the value of `instance_array(index)` to match the the derived type that we want to return
44+
1. We return the index value (i.e. an integer) to Python
45+
1. The Python side just holds onto this integer
46+
1. When we want to get attributes (i.e. values) of the derived type,
47+
we pass the index value (i.e. an integer) of interest from Python back to Fortran
48+
1. The manager gets the derived type at `instance_array(index)` and then can return the atribute of interest back to Python
49+
1. When we want to set attributes (i.e. values) of the derived type,
50+
we pass the index value (i.e. an integer) of interest and the value to set from Python back to Fortran
51+
1. The manager gets the derived type at `instance_array(index)` and then sets the desired atribute of interest on the Fortran side
52+
1. When we finalise an instance from Python,
53+
we pass the index value (i.e. an integer) of interest from Python back to Fortran
54+
and then call any finalisation routines on `instance_array(index)` on the Fortran side,
55+
while also setting `logical_array(index)` back to `.false.`, marking `instance_array(index)`
56+
as being available for use for another purpose
57+
58+
Doing it this means that ownership is easier to manage.
59+
Let's assume we have two Python instances backed by the same Fortran instance,
60+
call them `PythonObjA` and `PythonObjB`.
61+
If we finalise the Fortran instance via `PythonObjA`, then `logical_array(index)` will now be marked as `.false.`.
62+
Then, if we try and use this instance via `PythonObjB`,
63+
we will see that `logical_array(index)` is `.false.`,
64+
hence we know that the object has been finalised already hence the view that `PythonObjB` has is no longer valid.
65+
(I can see an edge case where, we finalise via `PythonObjA`,
66+
then initialise a new object that gets the (now free) instance index
67+
used by `PythonObjB`, so when we look again via `PythonObjB`,
68+
we see the new object, which could be very confusing.
69+
We should a) test this to see if we can re-create such an edge case
70+
then b) consider a fix (maybe we need an extra array which counts how many times
71+
this index has been initialised and finalised so we can tell if we're still
72+
looking at the same initialisation or a new one that has happened since we last looked).)
73+
74+
This solution allows us to a) only pass integers across the Python-Fortran interface
75+
(so we can use f2py) and b) keep track of ownership.
76+
The tradeoff is that we use more memory (because we have arrays of instances and logicals),
77+
are slightly slower (as we have extra layers of lookup to do)
78+
and have slow reallocation calls sometimes (when we need to increase the number of available instances dynamically).
79+
There is no perfect solution, and we think this way strikes the right balance of
80+
'just works' for most users while also offering access to fine-grained memory control for 'power users'.
81+
82+
## Other solutions we rejected
83+
84+
### Pass pointers back and forth
85+
86+
Example repository: https://github.com/Nicholaswogan/f2py-with-derived-types
87+
88+
Another option is to pass pointers to objects back and forth.
89+
We tried this initially.
90+
Where this falls over is in ownership.
91+
Basically, the situation that doesn't work is this.
92+
93+
From Python, I create an object which is backed by a Fortran derived type.
94+
Call this `PythonObjA`.
95+
From Python, I create another object which is backed by the same Fortran derived type instance i.e. I get a pointer to the same Fortran derived type instance.
96+
Call this `PythonObjB`.
97+
If I now finalise `PythonObjA` from Python, this causes the following to happen.
98+
The pointer that was used by `PythonObjA` is now pointing to `null`.
99+
This is fine.
100+
However, the pointer that is being used by `PythonObjB` is now in an undefined state
101+
(see e.g. community.intel.com/t5/Intel-Fortran-Compiler/DEALLOCATING-DATA-TYPE-POINTERS/m-p/982338#M100027
102+
or https://www.ibm.com/docs/en/xl-fortran-aix/16.1.0?topic=attributes-deallocate).
103+
As a result, whenever I try to do anything with `PythonObjB`,
104+
the result cannot be predicted and there is no way to check
105+
(see e.g. https://stackoverflow.com/questions/72140217/can-you-test-for-nullpointers-in-fortran),
106+
either from Python or Fortran, what the state of the pointer used by `PythonObjB` is
107+
(it is undefined).
108+
109+
This unresolvable problem is why we don't use the purely pointer-based solution
110+
and instead go for a slightly more involved solution with a much clearer ownership model/logic.
111+
We could do something like add a reference counter or some other solution to make this work.
112+
This feels very complicated though.
113+
General advice also seems to be to avoid pointers where possible
114+
(community.intel.com/t5/Intel-Fortran-Compiler/how-to-test-if-pointer-array-is-allocated/m-p/1138643#M136486),
115+
prefering allocatable instead, which has also helped shape our current solution.

NOTES.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)