Skip to content

Commit e86cb22

Browse files
committed
Add pointer-based variant
1 parent 3b2f018 commit e86cb22

16 files changed

Lines changed: 413 additions & 394 deletions

meson.build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ if pyprojectwheelbuild_enabled
5252
# Injected with `script/inject-srcs-into-meson-build.py`
5353
srcs = files(
5454
'src/example_fgen_basic/error_v/creation_wrapper.f90',
55+
'src/example_fgen_basic/error_v/error_v_ptr_based_wrapper.f90',
5556
'src/example_fgen_basic/error_v/error_v_wrapper.f90',
5657
'src/example_fgen_basic/get_wavelength_wrapper.f90',
5758
)
@@ -81,7 +82,6 @@ if pyprojectwheelbuild_enabled
8182
'src/example_fgen_basic/pyfgen_runtime/base_finalisable.py',
8283
'src/example_fgen_basic/pyfgen_runtime/exceptions.py',
8384
'src/example_fgen_basic/pyfgen_runtime/formatting.py',
84-
'src/example_fgen_basic/runtime_helpers.py',
8585
)
8686

8787
# The ancillary library,

src/example_fgen_basic/error_v/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
Definition of an error value
33
"""
44

5-
from example_fgen_basic.error_v.error_v import ErrorV
5+
from example_fgen_basic.error_v.error_v import ErrorV, ErrorVPtrBased
66

7-
__all__ = ["ErrorV"]
7+
__all__ = ["ErrorV", "ErrorVPtrBased"]

src/example_fgen_basic/error_v/creation.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import annotations
66

7-
from example_fgen_basic.error_v.error_v import ErrorV
7+
from example_fgen_basic.error_v.error_v import ErrorV, ErrorVPtrBased
88
from example_fgen_basic.pyfgen_runtime.exceptions import CompiledExtensionNotFoundError
99

1010
try:
@@ -24,3 +24,16 @@ def create_error(inv: int) -> ErrorV:
2424
error = ErrorV(instance_index)
2525

2626
return error
27+
28+
29+
def create_error_ptr_based(inv: int) -> ErrorVPtrBased:
30+
"""
31+
Create an instance of error (a wrapper around our Fortran derived type)
32+
33+
Uses the pointer based logic
34+
"""
35+
instance_ptr = m_error_v_creation_w.create_error_ptr_based(inv)
36+
37+
error = ErrorVPtrBased(instance_ptr)
38+
39+
return error

src/example_fgen_basic/error_v/creation_wrapper.f90

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
!> Generation to be automated in future (including docstrings of some sort).
55
module m_error_v_creation_w
66

7+
use iso_c_binding, only: c_ptr, c_loc
8+
79
! => allows us to rename on import to avoid clashes
810
use m_error_v_creation, only: o_create_error => create_error
911
use m_error_v, only: ErrorV
@@ -17,7 +19,7 @@ module m_error_v_creation_w
1719
implicit none
1820
private
1921

20-
public :: create_error
22+
public :: create_error, create_error_ptr_based
2123

2224
contains
2325

@@ -51,4 +53,31 @@ subroutine create_error(inv, res_instance_index)
5153

5254
end subroutine create_error
5355

56+
subroutine create_error_ptr_based(inv, res_instance_ptr)
57+
! Needs to be subroutine to have the created instance persist I think
58+
! (we can check)
59+
! function create_error(inv) result(res_instance_index)
60+
61+
integer, intent(in) :: inv
62+
!! Input value to use to create the error
63+
64+
!f2py integer(8), intent(out) :: res_instance_ptr
65+
type(c_ptr), intent(out) :: res_instance_ptr
66+
!! Pointer to the resulting instance
67+
!
68+
! This is the major trick for wrapping.
69+
! We return pointers (passed as integers) to Python rather than the instance itself.
70+
71+
type(ErrorV), pointer :: res
72+
73+
! Question is: when does this get deallocated?
74+
! When we go out of scope?
75+
! If yes, that will be why we had to do this array thing.
76+
allocate(res)
77+
res = o_create_error(inv)
78+
79+
res_instance_ptr = c_loc(res)
80+
81+
end subroutine create_error_ptr_based
82+
5483
end module m_error_v_creation_w

src/example_fgen_basic/error_v/error_v.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010

1111
from example_fgen_basic.pyfgen_runtime.base_finalisable import (
1212
FinalisableWrapperBase,
13+
FinalisableWrapperBasePtrBased,
1314
check_initialised,
15+
check_initialised_ptr_based,
1416
)
1517
from example_fgen_basic.pyfgen_runtime.exceptions import CompiledExtensionNotFoundError
1618

1719
try:
1820
from example_fgen_basic._lib import ( # type: ignore
21+
m_error_v_ptr_based_w,
1922
m_error_v_w,
2023
)
2124
except (ModuleNotFoundError, ImportError) as exc: # pragma: no cover
@@ -77,3 +80,62 @@ def message(self) -> str:
7780
).decode()
7881

7982
return message
83+
84+
85+
@define
86+
class ErrorVPtrBased(FinalisableWrapperBasePtrBased):
87+
"""
88+
TODO: auto docstring e.g. "Wrapper around the Fortran :class:`ErrorV`"
89+
90+
Uses the pointer-based passing logic
91+
"""
92+
93+
# Bug in Ipython pretty hence have to put this on every object?
94+
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
95+
"""
96+
Get pretty representation of self
97+
98+
Used by IPython notebooks and other tools
99+
"""
100+
super()._repr_pretty_(p=p, cycle=cycle)
101+
102+
@property
103+
def exposed_attributes(self) -> tuple[str, ...]:
104+
"""
105+
Attributes exposed by this wrapper
106+
"""
107+
return ("code", "message")
108+
109+
# TODO: from_build_args, from_new_connection, context manager, finalise
110+
111+
@property
112+
@check_initialised_ptr_based
113+
def code(self) -> int:
114+
"""
115+
Error code
116+
117+
Returns
118+
-------
119+
:
120+
Error code, retrieved from Fortran
121+
"""
122+
code: int = m_error_v_ptr_based_w.iget_code(instance_ptr=self.instance_ptr)
123+
124+
return code
125+
126+
@property
127+
@check_initialised_ptr_based
128+
def message(self) -> str:
129+
"""
130+
Error message
131+
132+
Returns
133+
-------
134+
:
135+
Error message, retrieved from Fortran
136+
"""
137+
message: str = m_error_v_ptr_based_w.iget_message(
138+
instance_ptr=self.instance_ptr
139+
).decode()
140+
141+
return message
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
!> Wrapper for interfacing `m_error_v` with Python, using pointer only (no instance array)
2+
!>
3+
!> Written by hand here.
4+
!> Generation to be automated in future (including docstrings of some sort).
5+
module m_error_v_ptr_based_w
6+
7+
use iso_c_binding, only: c_ptr, c_f_pointer
8+
9+
use m_error_v, only: ErrorV
10+
11+
implicit none
12+
private
13+
14+
public :: iget_code, iget_message
15+
16+
contains
17+
18+
! Full set of wrapping strategies to pass different types in e.g.
19+
! https://gitlab.com/magicc/fgen/-/blob/switch-to-uv/tests/test-data/exposed_attrs/src/exposed_attrs/exposed_attrs_wrapped.f90
20+
! (we will do a full re-write of the code which generates this,
21+
! but the strategies will probably stay as they are)
22+
subroutine iget_code( &
23+
instance_ptr, &
24+
code &
25+
)
26+
27+
!f2py integer(8), intent(in) :: instance_ptr
28+
type(c_ptr), intent(in) :: instance_ptr
29+
30+
integer, intent(out) :: code
31+
32+
type(ErrorV), pointer :: instance
33+
34+
call c_f_pointer(instance_ptr, instance)
35+
36+
code = instance % code
37+
38+
end subroutine iget_code
39+
40+
subroutine iget_message( &
41+
instance_ptr, &
42+
message &
43+
)
44+
45+
!f2py integer(8), intent(in) :: instance_ptr
46+
type(c_ptr), intent(in) :: instance_ptr
47+
48+
character(len=128), intent(out) :: message
49+
50+
type(ErrorV), pointer :: instance
51+
52+
call c_f_pointer(instance_ptr, instance)
53+
54+
message = instance % message
55+
56+
end subroutine iget_message
57+
58+
end module m_error_v_ptr_based_w

src/example_fgen_basic/meson.build

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
srcs += files(
2+
'error_v/creation.f90',
3+
'error_v/error_v.f90',
4+
'fpyfgen/base_finalisable.f90',
25
'get_wavelength.f90',
36
'kind_parameters.f90',
47
)

src/example_fgen_basic/pyfgen_runtime/base_finalisable.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,134 @@ def checked(
163163
return method(ref, *args, **kwargs)
164164

165165
return checked # type: ignore
166+
167+
168+
@define
169+
class FinalisableWrapperBasePtrBased(ABC):
170+
"""
171+
Base class for Fortran derived type wrappers using the pointer-based passing
172+
"""
173+
174+
instance_ptr: int | None = None
175+
"""
176+
Pointer to Fortran instance (technically a C pointer)
177+
"""
178+
179+
def __str__(self) -> str:
180+
"""
181+
Get string representation of self
182+
"""
183+
return to_str(
184+
self,
185+
self.exposed_attributes,
186+
)
187+
188+
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
189+
"""
190+
Get pretty representation of self
191+
192+
Used by IPython notebooks and other tools
193+
"""
194+
to_pretty(
195+
self,
196+
self.exposed_attributes,
197+
p=p,
198+
cycle=cycle,
199+
)
200+
201+
def _repr_html_(self) -> str:
202+
"""
203+
Get html representation of self
204+
205+
Used by IPython notebooks and other tools
206+
"""
207+
return to_html(
208+
self,
209+
self.exposed_attributes,
210+
)
211+
212+
@property
213+
def initialized(self) -> bool:
214+
"""
215+
Is the instance initialised, i.e. connected to a Fortran instance?
216+
"""
217+
return self.instance_ptr is not None
218+
219+
@property
220+
@abstractmethod
221+
def exposed_attributes(self) -> tuple[str, ...]:
222+
"""
223+
Attributes exposed by this wrapper
224+
"""
225+
...
226+
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()
255+
256+
def _uninitialise_instance_index(self) -> None:
257+
self.instance_index = None
258+
259+
260+
WrapperPtrBased = TypeVar("WrapperPtrBased", bound=FinalisableWrapperBasePtrBased)
261+
262+
263+
def check_initialised_ptr_based(
264+
method: Callable[Concatenate[WrapperPtrBased, P], T],
265+
) -> Callable[Concatenate[WrapperPtrBased, P], T]:
266+
"""
267+
Check that the wrapper object has been initialised before executing the method
268+
269+
Parameters
270+
----------
271+
method
272+
Method to wrap
273+
274+
Returns
275+
-------
276+
:
277+
Wrapped method
278+
279+
Raises
280+
------
281+
InitialisationError
282+
Wrapper is not initialised
283+
"""
284+
285+
@wraps(method)
286+
def checked(
287+
ref: WrapperPtrBased,
288+
*args: P.args,
289+
**kwargs: P.kwargs,
290+
) -> Any:
291+
if not ref.initialized:
292+
raise NotInitialisedError(ref, method)
293+
294+
return method(ref, *args, **kwargs)
295+
296+
return checked # type: ignore

0 commit comments

Comments
 (0)