f2py
¶
Rather than calling into a shared library via ctypes
or
cffi
, the f2py
tool can be used to to wrap Fortran
interfaces in Python functions. It does this by generating
a custom CPython C extension and compiling it with a
Fortran shared library.
f2py
has many limitations, chief of which is absence of support
for user defined types or derived types (equivalent of a C struct
).
The examples below demonstrate a few ways of getting around these
limitations, including manual conversion of a custom typed variable
to raw bytes.
Usage¶
First, the f2py
tool must be used to create an extension module:
$ f2py \
> --verbose \
> -c \
> --opt='-O3' \
> -m example \
> fortran/example.f90 \
> skip: make_container view_knob
$ ls *.so
example.cpython-37m-x86_64-linux-gnu.so*
As we can see, this interacts directly with the Fortran source rather than with an object file or a shared library.
Inside the example.so
module we’ve created, the only attribute is
example
, which represents the Fortran module in example.f90
:
>>> import example
>>> example
<module 'example' from '.../example.cpython-37m-x86_64-linux-gnu.so'>
>>> [name for name in dir(example) if not name.startswith("__")]
['example']
>>> example.example
<fortran object>
It is within this wrapped Fortran module that our actual routines live:
>>> example_ns = example.example
>>> for name in dir(example_ns):
... if not name.startswith("__"):
... print(name)
...
foo
foo_array
foo_by_ref
just_print
make_udf
turn_knob
udf_ptr
The first task we’ll accomplish is to call foo()
and
foo_by_ref()
:
# foo()
bar = 1.0
baz = 16.0
msg_foo = "foo ({}, {}) = {}".format(
bar, baz, example_ns.foo(bar, baz)
)
print(msg_foo)
msg_foo_by_ref = "foo_by_ref({}, {}) = {}".format(
bar, baz, example_ns.foo_by_ref(bar, baz)
)
print(msg_foo_by_ref)
As can be seen in the output below. Calling by reference results in the
correct answer while calling by value (foo()
) does not work correctly
with f2py
.
Next, we invoke the make_udf()
routine to “smuggle” out a
UserDefined
value as raw bytes:
# make_udf()
buzz = 1.25
broken = 5.0
how_many = 1337
quuz_as_bytes = example_ns.make_udf(buzz, broken, how_many)
quuz = np_to_udf(quuz_as_bytes)
msg = MAKE_UDF_TEMPLATE.format(buzz, broken, how_many, quuz)
print(msg, end="")
In particular, this uses the np_to_udf
helper to convert those bytes into
a UserDefined
object as defined in ctypes:
def np_to_udf(arr):
address = arr.ctypes.data
return UserDefined.from_address(address)
For udf_ptr()
, the other routine which deals with a user defined type,
we use the prepare_udf
helper from ctypes. This allocates the memory
for the UserDefined
value in Python and then passes a void*
pointer (as an integer) to the Fortran routine:
# udf_ptr()
made_it, ptr_as_int = prepare_udf()
ptr_as_int = ptr_as_int.value
example_ns.udf_ptr(ptr_as_int)
msg = UDF_PTR_TEMPLATE.format(ptr_as_int, ptr_as_int, made_it)
print(msg, end="")
Since f2py
is included with NumPy, it has nicer support for NumPy arrays
than either ctypes
or cffi
. This means we can call foo_array()
directly with a NumPy array:
# foo_array()
val = np.asfortranarray([[3.0, 4.5], [1.0, 1.25], [9.0, 0.0], [-1.0, 4.0]])
two_val = example_ns.foo_array(val)
print(MSG_FOO_ARRAY.format(val, two_val))
Finally, we call just_print()
to mix Python and Fortran usage of
STDOUT
:
# just_print()
print("just_print()")
example_ns.just_print()
Output¶
$ python f2py/check_f2py.py
------------------------------------------------------------
example: <module 'example' from '.../f2py/example...so'>
dir(example.example): foo, foo_array, foo_by_ref, just_print, make_udf, udf_ptr
------------------------------------------------------------
foo (1.0, 16.0) = 0.0
foo_by_ref(1.0, 16.0) = 61.0
------------------------------------------------------------
quuz = make_udf(1.25, 5.0, 1337)
= UserDefined(buzz=1.25, broken=5.0, how_many=1337)
------------------------------------------------------------
val =
[[ 3. 4.5 ]
[ 1. 1.25]
[ 9. 0. ]
[-1. 4. ]]
two_val = foo_array(val)
two_val =
[[ 6. 9. ]
[ 2. 2.5]
[ 18. 0. ]
[ -2. 8. ]]
------------------------------------------------------------
ptr_as_int = address(made_it) # intptr_t / ssize_t / long
ptr_as_int = 139859191412464 # 0x7f33816c36f0
udf_ptr(ptr_as_int) # Set memory in ``made_it``
made_it = UserDefined(buzz=3.125, broken=-10.5, how_many=101)
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
What is Happening?¶
f2py
actually generates a wrapped {modname}module.c
file (so in our
case examplemodule.c
) and utilizes a fortranobject
C library to create
CPython C extensions:
>>> import os
>>> import numpy.f2py
>>>
>>> f2py_dir = os.path.dirname(numpy.f2py.__file__)
>>> f2py_dir
'.../lib/python3.7/site-packages/numpy/f2py'
>>> os.listdir(os.path.join(f2py_dir, 'src'))
['fortranobject.c', 'fortranobject.h']
It uses a C compiler with flags determined by distutils
to
link against NumPy and Python headers when compiling {modname}module.c
and fortranobject.c
and uses a Fortran compiler for {modname}.f90
.
Then it uses the Fortran compiler as linker:
$ gfortran \
> -Wall \
> -g \
> -shared \
> ${TEMPDIR}/.../{modname}module.o \
> ${TEMPDIR}/.../fortranobject.o \
> ${TEMPDIR}/{modname}.o \
> -lgfortran \
> -o \
> ./{modname}.so
When trying to convert a Fortran subroutine to Python via f2py
, a
problem occurs if the subroutine uses a user defined type. For example, if
we tried to use the make_container()
routine:
$ f2py \
> --verbose \
> -c \
> --opt='-O3' \
> -m example \
> fortran/example.f90 \
> only: make_container
...
Building modules...
Building module "example"...
Constructing F90 module support for "example"...
Skipping type unknown_type
Skipping type unknown_type
Constructing wrapper function "example.make_container"...
getctype: No C-type found in "{'typespec': 'type', 'typename': 'datacontainer', 'attrspec': [], 'intent': ['out']}", assuming void.
getctype: No C-type found in "{'typespec': 'type', 'typename': 'datacontainer', 'attrspec': [], 'intent': ['out']}", assuming void.
getctype: No C-type found in "{'typespec': 'type', 'typename': 'datacontainer', 'attrspec': [], 'intent': ['out']}", assuming void.
Traceback (most recent call last):
...
File ".../numpy/f2py/capi_maps.py", line 412,in getpydocsign
sig = '%s : %s %s%s' % (a, opt, c2py_map[ctype], init)
KeyError: 'void'
This is because make_container()
returns the DataContainer
user defined type.
References¶
- (Lack of) support for user defined types in
f2py
- The
f90wrap
interface generator adds support for user defined types tof2py
. However, the author of this document has no experience withf90wrap
.