Cython¶
Cython is a mature and heavily used extension of the Python programming language. It allows writing optionally typed Python code, implementing part of a Python module completely in C and many other performance benefits.
It is very mature. For example, a .pyx
Cython file can be compiled
both to a standard CPython C extension as well as providing
basic support for the PyPy emulation layer cpyext
.
Usage¶
The ABI for the Fortran module is provided in a Cython declaration
example_fortran.pxd
which we will reference throughout:
cimport example_fortran
Using this, values can be passed by value into foo()
using typical
C pass by value convention:
def foo(double bar, double baz):
cdef double quux
example_fortran.foo(bar, baz, &quux)
return quux
For the UserDefined
Fortran type, the example_fortran.pxd
defines a matching struct
:
ctypedef struct UserDefined:
double buzz
double broken
int how_many
This can then be used for make_udf()
:
def make_udf(double buzz, double broken, int how_many):
cdef example_fortran.UserDefined made_it
example_fortran.make_udf(&buzz, &broken, &how_many, &made_it)
return made_it
and for udf_ptr()
:
from libc.stdint cimport intptr_t
def udf_ptr():
cdef example_fortran.UserDefined made_it
cdef intptr_t ptr_as_int = <intptr_t> (&made_it)
example_fortran.udf_ptr(&ptr_as_int)
return made_it
In either case, the UserDefined
value is created by each function
(i.e. from Python, not from Fortran) and then a pointer to that memory is
passed along to the relevant Fortran routine:
cdef example_fortran.UserDefined made_it
When calling foo_array()
we allow NumPy arrays and Cython allows
us to specify that the array is 2D and Fortran-contiguous. We also turn
off bounds checking since the only array indices used are 0
:
@cython.boundscheck(False)
@cython.wraparound(False)
def foo_array(np.ndarray[double, ndim=2, mode='fortran'] val not None):
cdef int size
cdef np.ndarray[double, ndim=2, mode='fortran'] two_val
size = np.shape(val)[0]
two_val = np.empty_like(val)
example_fortran.foo_array(
&size,
&val[0, 0],
&two_val[0, 0],
)
return two_val
Calling just_print()
simply requires wrapping a C call in a Python
function (i.e. def
not cdef
):
def just_print():
example_fortran.just_print()
When invoking view_knob()
, we must do a little extra work. The f2py
parser has a bug when a Fortran function
(vs. a subroutine
) has
bind(c, name=...)
. In order to allow f2py
to wrap example.f90
, we
don’t specify the non-mangled name in the ABI, hence must reference the mangled
name from the object file:
int view_knob "__example_MOD_view_knob" ()
Luckily the mangled name can be aliased in the .pxd
declaration and then
calling view_knob()
in Cython is straightforward:
def view_knob():
return example_fortran.view_knob()
Similarly turn_knob()
is also straightforward:
def turn_knob(int new_value):
example_fortran.turn_knob(&new_value)
Output¶
$ python cython/check_cython.py
------------------------------------------------------------
quux = foo(1.0, 16.0) = 61.0
------------------------------------------------------------
quuz = make_udf(1.25, 5.0, 1337)
= {'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. ]]
------------------------------------------------------------
made_it = udf_ptr()
= {'buzz': 3.125, 'broken': -10.5, 'how_many': 101}
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
------------------------------------------------------------
example.get_include() =
.../foreign-fortran/cython/venv/lib/python.../site-packages/example/include
------------------------------------------------------------
view_knob() = 1337
turn_knob(42)
view_knob() = 42
sdist
and installed files¶
On a standard CPython install on Linux, a source dist (sdist
)
contains the following:
.
├── example
│ ├── example.f90
│ ├── example_fortran.pxd
│ ├── fast.c
│ ├── include
│ │ └── example.h
│ └── __init__.py
├── example.egg-info
│ ├── dependency_links.txt
│ ├── PKG-INFO
│ ├── SOURCES.txt
│ └── top_level.txt
├── MANIFEST.in
├── PKG-INFO
├── setup.cfg
└── setup.py
3 directories, 13 files
Once this gets installed, the following files are present:
.
├── example_fortran.pxd
├── fast.cpython-37m-x86_64-linux-gnu.so
├── include
│ └── example.h
├── __init__.py
├── lib
│ └── libexample.a
└── __pycache__
└── __init__.cpython-37.pyc
3 directories, 6 files
cimport
-ing this library¶
This library provides an example/example_fortran.pxd
declaration file
that can be used to cimport
the library without having to worry about
the Python layer:
cimport example.example_fortran
In this case, the library referenced in example_fortran.pxd
is made available in the example
package:
>>> import os
>>> import example
>>>
>>> include_dir = example.get_include()
>>> include_dir
'.../foreign-fortran/cython/venv/lib/python.../site-packages/example/include'
>>> os.listdir(include_dir)
['example.h']
>>>
>>> lib_dir = example.get_lib()
>>> lib_dir
'.../foreign-fortran/cython/venv/lib/python.../site-packages/example/lib'
>>> os.listdir(lib_dir)
['libexample.a']
See cython/use_cimport/setup.py
for an example of how to wrap:
>>> wrapper.morp()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
>>> example.foo(1.5, 2.5)
10.875
>>> wrapper.triple_foo(1.5, 2.5)
32.625
Gotcha¶
If libraries=['gfortran']
is not specified in setup.py
when
building the CPython C extension module (example.so
), then the print
statements in just_print()
(as defined in in example.f90
) cause
$ IGNORE_LIBRARIES=true python setup.py build_ext --inplace
running build_ext
...
$ python -c 'import example'
Traceback (most recent call last):
File "<string>", line 1, in <module>
File ".../cython/package/example/__init__.py", line 5, in <module>
from example import fast
ImportError: .../cython/package/example/fast...so: undefined symbol: _gfortran_transfer_character_write
References¶
- Decently helpful article and pre-article to that one about
using Cython to wrap Fortran. But this article fails to point out
its approach can leave out some symbols (e.g. the
check_cython
example whenlibgfortran
isn’t included) - Description on the uber-useful
fortran90.org
on how to interface with C