|
| 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. |
0 commit comments