Skip to content

Commit bf76bc7

Browse files
committed
Finish mucking around with pointer based option
1 parent e86cb22 commit bf76bc7

8 files changed

Lines changed: 305 additions & 42 deletions

File tree

src/example_fgen_basic/error_v/error_v.f90

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ subroutine finalise(self)
9292
! Hopefully can leave without docstring (like Python)
9393

9494
! If we make message allocatable, deallocate here
95+
self % code = 1
96+
self % message = ""
9597

9698
end subroutine finalise
9799

src/example_fgen_basic/error_v/error_v.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
FinalisableWrapperBasePtrBased,
1414
check_initialised,
1515
check_initialised_ptr_based,
16+
execute_finalise_on_fail_ptr_based,
1617
)
1718
from example_fgen_basic.pyfgen_runtime.exceptions import CompiledExtensionNotFoundError
1819

@@ -106,7 +107,77 @@ def exposed_attributes(self) -> tuple[str, ...]:
106107
"""
107108
return ("code", "message")
108109

110+
@classmethod
111+
def from_new_connection(cls) -> ErrorVPtrBased:
112+
"""
113+
Initialise from a new connection
114+
115+
The user is responsible for releasing this connection
116+
using :attr:`~finalise` when it is no longer needed.
117+
Alternatively a :obj:`~AtmosphereToOceanCarbonFluxCalculatorContext`
118+
can be used to handle the finalisation using a context manager.
119+
120+
Returns
121+
-------
122+
A new instance with a unique instance index
123+
124+
Raises
125+
------
126+
WrapperErrorUnknownCause
127+
If a new instance could not be allocated
128+
129+
This could occur if too many instances are allocated at any one time
130+
"""
131+
instance_ptr = m_error_v_ptr_based_w.get_instance_ptr()
132+
# TODO: result type handling here
133+
134+
return cls(instance_ptr)
135+
109136
# TODO: from_build_args, from_new_connection, context manager, finalise
137+
@classmethod
138+
def from_build_args(
139+
cls,
140+
code: int,
141+
message: str = "",
142+
) -> ErrorVPtrBased:
143+
"""
144+
Build the class (including connecting to Fortran)
145+
"""
146+
out = cls.from_new_connection()
147+
# TODO: remove or update this construct when we have result types
148+
execute_finalise_on_fail_ptr_based(
149+
out,
150+
m_error_v_ptr_based_w.instance_build,
151+
code=code,
152+
message=message,
153+
)
154+
155+
return out
156+
157+
@check_initialised
158+
def finalise(self) -> None:
159+
"""
160+
Close the connection with the Fortran module
161+
"""
162+
m_error_v_ptr_based_w.instance_finalise(self.instance_ptr)
163+
self._uninitialise_instance_ptr()
164+
165+
@property
166+
def is_associated(self) -> bool:
167+
"""
168+
Check whether `self`'s pointer is associated on the Fortran side
169+
170+
Returns
171+
-------
172+
:
173+
Whether `self`'s pointer is associated on the Fortran side
174+
"""
175+
if self.instance_ptr is None:
176+
return False
177+
178+
res: bool = bool(m_error_v_ptr_based_w.is_associated(self.instance_ptr)) # type: ignore
179+
180+
return res
110181

111182
@property
112183
@check_initialised_ptr_based

src/example_fgen_basic/error_v/error_v_ptr_based_wrapper.f90

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,127 @@
44
!> Generation to be automated in future (including docstrings of some sort).
55
module m_error_v_ptr_based_w
66

7-
use iso_c_binding, only: c_ptr, c_f_pointer
7+
use iso_c_binding, only: c_f_pointer, c_loc, c_null_ptr, c_ptr
88

99
use m_error_v, only: ErrorV
1010

1111
implicit none
1212
private
1313

14-
public :: iget_code, iget_message
14+
public :: get_instance_ptr, instance_build, instance_finalise, is_associated, &
15+
iget_code, iget_message
1516

1617
contains
1718

19+
subroutine get_instance_ptr(res_instance_ptr)
20+
!> Get a pointer to a new instance
21+
!
22+
! Needs to be subroutine to have the created instance persist I think
23+
! (we can check)
24+
! function create_error(inv) result(res_instance_index)
25+
26+
!f2py integer(8), intent(out) :: res_instance_ptr
27+
type(c_ptr), intent(out) :: res_instance_ptr
28+
!! Pointer to the resulting instance
29+
!
30+
! This is the major trick for wrapping.
31+
! We return pointers (passed as integers) to Python rather than the instance itself.
32+
33+
type(ErrorV), pointer :: res
34+
! Question is: when does this get deallocated?
35+
! When we go out of scope?
36+
! If yes, that will be why we had to do this array thing.
37+
allocate(res)
38+
39+
res_instance_ptr = c_loc(res)
40+
41+
end subroutine get_instance_ptr
42+
43+
subroutine instance_build(instance_ptr, code, message)
44+
!> Build an instance
45+
46+
!f2py integer(8), intent(in) :: instance_ptr
47+
type(c_ptr), intent(in) :: instance_ptr
48+
!! Pointer to the instance
49+
!
50+
! This is the major trick for wrapping.
51+
! We pass pointers (passed as integers) to Python rather than the instance itself.
52+
53+
integer, intent(in) :: code
54+
character(len=*), optional, intent(in) :: message
55+
56+
type(ErrorV), pointer :: inst
57+
58+
call c_f_pointer(instance_ptr, inst)
59+
60+
call inst % build(code, message)
61+
62+
end subroutine instance_build
63+
64+
subroutine instance_finalise(instance_ptr)
65+
!> Finalise an instance
66+
67+
!f2py integer(8), intent(inout) :: instance_ptr
68+
type(c_ptr), intent(inout) :: instance_ptr
69+
!! Pointer to the instance
70+
!
71+
! This is the major trick for wrapping.
72+
! We pass pointers (passed as integers) to Python rather than the instance itself.
73+
74+
type(ErrorV), pointer :: inst
75+
76+
call c_f_pointer(instance_ptr, inst)
77+
78+
! This may be why we used the array approach.
79+
! The issue here is that, if you call this method twice,
80+
! there is no way to work out that you're the 'second caller'.
81+
! When the first call calls `deallocate(inst)`,
82+
! this puts any other pointers to the instance in an undefined status
83+
! (https://www.ibm.com/docs/en/xl-fortran-aix/16.1.0?topic=attributes-deallocate).
84+
! The result of calling associated on an undefined pointer
85+
! can be anything (https://stackoverflow.com/questions/72140217/can-you-test-for-nullpointers-in-fortran),
86+
! i.e. there is no way to tell that someone else
87+
! has already called finalise before you have.
88+
! This also explains the undefined status issue nicely:
89+
! community.intel.com/t5/Intel-Fortran-Compiler/DEALLOCATING-DATA-TYPE-POINTERS/m-p/982338#M100027
90+
!
91+
! We'd have to introduce some reference counter to make this work I think.
92+
! Probably better advice for now, don't share pointer values
93+
! on the Python side, you have to be super careful about uninitialising if you do.
94+
! Avoiding pointers and using allocatable instead
95+
! was probably the other reason we did it how we did
96+
! community.intel.com/t5/Intel-Fortran-Compiler/how-to-test-if-pointer-array-is-allocated/m-p/1138643#M136486.
97+
if (associated(inst)) then
98+
call inst % finalise()
99+
deallocate(inst)
100+
end if
101+
102+
end subroutine instance_finalise
103+
104+
subroutine is_associated(instance_ptr, res)
105+
!> Check if a pointer is associated with an instance
106+
107+
!f2py integer(8), intent(in) :: instance_ptr
108+
type(c_ptr), intent(in) :: instance_ptr
109+
!! Pointer to the instance
110+
!
111+
! This is the major trick for wrapping.
112+
! We pass pointers (passed as integers) to Python rather than the instance itself.
113+
114+
logical, intent(out) :: res
115+
!! Whether `instance_ptr` is associated or not
116+
117+
type(ErrorV), pointer :: inst
118+
119+
call c_f_pointer(instance_ptr, inst)
120+
121+
print *, instance_ptr
122+
print *, inst
123+
res = associated(inst)
124+
print *, res
125+
126+
end subroutine is_associated
127+
18128
! Full set of wrapping strategies to pass different types in e.g.
19129
! https://gitlab.com/magicc/fgen/-/blob/switch-to-uv/tests/test-data/exposed_attrs/src/exposed_attrs/exposed_attrs_wrapped.f90
20130
! (we will do a full re-write of the code which generates this,

src/example_fgen_basic/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(self, instance: Any, method: Optional[Callable[..., Any]] = None):
5757
if method:
5858
error_msg = f"{instance} must be initialised before {method} is called"
5959
else:
60-
error_msg = f"instance ({instance:r}) is not initialized yet"
60+
error_msg = f"instance ({instance:r}) is not initialised yet"
6161

6262
super().__init__(error_msg)
6363

src/example_fgen_basic/pyfgen_runtime/base_finalisable.py

Lines changed: 84 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _repr_html_(self) -> str:
7777
)
7878

7979
@property
80-
def initialized(self) -> bool:
80+
def initialised(self) -> bool:
8181
"""
8282
Is the instance initialised, i.e. connected to a Fortran instance?
8383
"""
@@ -108,7 +108,7 @@ def exposed_attributes(self) -> tuple[str, ...]:
108108
# ...
109109
#
110110
# @abstractmethod
111-
# def finalize(self) -> None:
111+
# def finalise(self) -> None:
112112
# """
113113
# Finalise the Fortran instance and set self back to being uninitialised
114114
#
@@ -157,7 +157,7 @@ def checked(
157157
*args: P.args,
158158
**kwargs: P.kwargs,
159159
) -> Any:
160-
if not ref.initialized:
160+
if not ref.initialised:
161161
raise NotInitialisedError(ref, method)
162162

163163
return method(ref, *args, **kwargs)
@@ -210,7 +210,7 @@ def _repr_html_(self) -> str:
210210
)
211211

212212
@property
213-
def initialized(self) -> bool:
213+
def initialised(self) -> bool:
214214
"""
215215
Is the instance initialised, i.e. connected to a Fortran instance?
216216
"""
@@ -224,37 +224,36 @@ def exposed_attributes(self) -> tuple[str, ...]:
224224
"""
225225
...
226226

227-
# TODO: consider whether we need these
228-
# @classmethod
229-
# @abstractmethod
230-
# def from_new_connection(cls) -> FinalisableWrapperBase:
231-
# """
232-
# Initialise by establishing a new connection with the Fortran module
233-
#
234-
# This requests a new model index from the Fortran module and then
235-
# initialises a class instance
236-
#
237-
# Returns
238-
# -------
239-
# New class instance
240-
# """
241-
# ...
242-
#
243-
# @abstractmethod
244-
# def finalize(self) -> None:
245-
# """
246-
# Finalise the Fortran instance and set self back to being uninitialised
247-
#
248-
# This method resets `self.instance_ptr` back to
249-
# `None`
250-
#
251-
# Should be decorated with :func:`check_initialised`
252-
# """
253-
# # call to Fortran module goes here when implementing
254-
# self._uninitialise_instance_index()
227+
@classmethod
228+
@abstractmethod
229+
def from_new_connection(cls) -> FinalisableWrapperBase:
230+
"""
231+
Initialise by establishing a new connection with the Fortran module
255232
256-
def _uninitialise_instance_index(self) -> None:
257-
self.instance_index = None
233+
This requests a new model index from the Fortran module and then
234+
initialises a class instance.
235+
236+
Returns
237+
-------
238+
:
239+
New class instance
240+
"""
241+
...
242+
243+
@abstractmethod
244+
def finalise(self) -> None:
245+
"""
246+
Finalise the Fortran instance and set self back to being uninitialised
247+
248+
This method resets `self.instance_ptr` back to `None`.
249+
250+
Should be decorated with :func:`check_initialised`
251+
"""
252+
# call to Fortran module goes here when implementing
253+
self._uninitialise_instance_ptr()
254+
255+
def _uninitialise_instance_ptr(self) -> None:
256+
self.instance_ptr = None
258257

259258

260259
WrapperPtrBased = TypeVar("WrapperPtrBased", bound=FinalisableWrapperBasePtrBased)
@@ -288,9 +287,59 @@ def checked(
288287
*args: P.args,
289288
**kwargs: P.kwargs,
290289
) -> Any:
291-
if not ref.initialized:
290+
if not ref.initialised:
292291
raise NotInitialisedError(ref, method)
293292

294293
return method(ref, *args, **kwargs)
295294

296295
return checked # type: ignore
296+
297+
298+
# Thank you for type hints info
299+
# https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-kwargs/
300+
def execute_finalise_on_fail_ptr_based(
301+
inst: FinalisableWrapperBasePtrBased,
302+
func_to_try: Callable[Concatenate[int, P], T],
303+
*args: P.args,
304+
**kwargs: P.kwargs,
305+
) -> T:
306+
"""
307+
Execute a function, finalising the instance before raising if any error occurs
308+
309+
This function is most useful in factory functions where it provides a
310+
clean way of ensuring that any Fortran is freed up in the event of an
311+
initialisation failure for any reason
312+
313+
Parameters
314+
----------
315+
inst
316+
Instance whose model index we will use when executing the functin
317+
318+
func_to_try
319+
Function to try executing, must take `inst`'s instance pointer as its
320+
first argument
321+
322+
*args
323+
Passed to `func_to_try`
324+
325+
**kwargs
326+
Passed to `func_to_try`
327+
328+
Returns
329+
-------
330+
:
331+
Result of calling `func_to_try(inst.instance_ptr, *args, **kwargs)`
332+
333+
Raises
334+
------
335+
Exception
336+
Any exception which occurs when calling `func_to_try`. Before the
337+
exception is raised, `inst.finalise()` is called.
338+
"""
339+
try:
340+
return func_to_try(inst.instance_ptr, *args, **kwargs)
341+
except RuntimeError:
342+
# finalise the instance before raising
343+
inst.finalise()
344+
345+
raise

0 commit comments

Comments
 (0)