Foreign Fortran¶
We’ll build a shared library in Fortran (90 and later) and show how to make foreign calls to the library from other programming languages:
Fortran¶
This is the most vanilla example of all. We are simply
calling the Fortran example
module from a Fortran program.
The module is only “foreign” in the sense that we only interact
with the object file example.o
when creating the executable fortran_example
:
$ gfortran \
> -o fortran_example \
> fortran/main.f90 \
> fortran/example.o
However, this still requires the presence of a module file to build the executable
$ ls fortran/example.mod
fortran/example.mod
$ rm -f fortran/example.mod
$ gfortran \
> -o fortran_example \
> fortran/main.f90 \
> fortran/example.o
fortran/main.f90:4:6:
use example, only: &
1
Fatal Error: Can't open module file 'example.mod' for reading
at (1): No such file or directory
compilation terminated.
Finally, we run fortran_example
to verify the behavior of
several procedures in the public interface:
$ ./fortran_example
------------------------------------------------------------
quux = foo(1.000000, 16.000000) = 61.000000
------------------------------------------------------------
quuz = make_udf(1.250000, 5.000000, 1337)
= UserDefined(1.250000, 5.000000, 1337)
------------------------------------------------------------
foo_array(
4,
[[3.000000, 4.500000],
[1.000000, 1.250000],
[9.000000, 0.000000],
[-1.000000, 4.000000]],
) =
[[6.000000, 9.000000],
[2.000000, 2.500000],
[18.000000, 0.000000],
[-2.000000, 8.000000]]
------------------------------------------------------------
ptr_as_int = c_loc(made_it) ! type(c_ptr)
! integer(c_intptr_t)
! integer(kind=8)
ptr_as_int = 140733727194752 ! 0x7FFF1FD13E80
udf_ptr(ptr_as_int) ! Set memory in ``made_it``
made_it = UserDefined(3.125000, -10.500000, 101)
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
------------------------------------------------------------
view_knob() = 1337
turn_knob(42)
view_knob() = 42
Using the shared library is as simple as declaring the public symbols used:
use example, only: &
dp, foo, foo_array, make_udf, udf_ptr, just_print, &
view_knob, turn_knob, UserDefined
Notice that the view_knob()
subroutine is in the public
Fortran interface even though it doesn’t have a bound
name in the ABI.
C¶
This example interacts with the object file
fortran/example.o
when creating the executable c_example
. Since
the example will use just_print()
, it relies on libgfortran
so we link against it (potentially having used gfortran -print-search-dirs
to determine where it is located):
$ gcc \
> c/example.c \
> fortran/example.o \
> -o c_example \
> -L/usr/lib/gcc/x86_64-linux-gnu/5 \
> -L/usr/lib/x86_64-linux-gnu \
> -lgfortran
Alternatively, gfortran
can consume a c/example.o
object file created
from the C source. This removes the worry of including libgfortran
though
it’s somewhat strange to compile an executable from C code with a Fortran
compiler.
$ gcc \
> -I c/ \
> -c c/example.c \
> -o c/example.o
$ gfortran \
> c/example.o \
> fortran/example.o \
> -o c_example
Finally, we run c_example
to verify the behavior of
several procedures in the public interface:
$ ./c_example
------------------------------------------------------------
quux = foo(1.000000, 16.000000) = 61.000000
------------------------------------------------------------
quuz = make_udf(1.250000, 5.000000, 1337)
= UserDefined(1.250000, 5.000000, 1337)
------------------------------------------------------------
foo_array(
4,
[[3.000000, 4.500000],
[1.000000, 1.250000],
[9.000000, 0.000000],
[-1.000000, 4.000000]],
) =
[[6.000000, 9.000000],
[2.000000, 2.500000],
[18.000000, 0.000000],
[-2.000000, 8.000000]]
------------------------------------------------------------
ptr_as_int = &made_it // intptr_t
// ssize_t
// long
ptr_as_int = 140727221075056 // 0x7ffd9c05bc70
udf_ptr(ptr_as_int) // Set memory in ``made_it``
made_it = UserDefined(3.125000, -10.500000, 101)
------------------------------------------------------------
contained =
[[0.000000, 4.000000],
[1.000000, 9.000000],
[1.000000, 2.000000],
[3.000000, 1.000000]]
container = make_container(contained)
container.data =
[[0.000000, 4.000000],
[1.000000, 9.000000],
[1.000000, 2.000000],
[3.000000, 1.000000]]
&contained = 140727221075216 // 0x7ffd9c05bd10
&container = 140727221075280 // 0x7ffd9c05bd50
&container.data = 140727221075280 // 0x7ffd9c05bd50
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
------------------------------------------------------------
view_knob() = 1337
turn_knob(42)
view_knob() = 42
Note that in order to call view_knob()
, the mangled name must
be used
int view_knob(void) {
return __example_MOD_view_knob();
}
C++¶
This example interacts with the object file
fortran/example.o
when creating the executable cpp_example
. Since
the example will use just_print()
, it relies on libgfortran
so we link against it (potentially having used gfortran -print-search-dirs
to determine where it is located):
$ g++ \
> -std=c++11 \
> -I c/ \
> cpp/example.cpp \
> fortran/example.o \
> -o cpp_example \
> -L/usr/lib/gcc/x86_64-linux-gnu/5 \
> -L/usr/lib/x86_64-linux-gnu \
> -lgfortran
The calling script in C++ is only partially complete:
$ ./cpp_example
------------------------------------------------------------
quux = foo(1.000000, 16.000000) = 61.000000
------------------------------------------------------------
quuz = make_udf(1.250000, 5.000000, 1337)
= UserDefined(1.250000, 5.000000, 1337)
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
Python¶
There are several ways to make foreign calls from within a Python program, each with it’s own pros and cons. Of the methods listed, only f2py is specific to Fortran. The rest can be used for any native code that can be built into the artifacts needed for that particular method.
ctypes
¶
This example interacts with the shared object
file fortran/example.so
. This shared object file can be loaded directly
>>> import ctypes
>>> so_file = "fortran/example.so"
>>> lib_example = ctypes.cdll.LoadLibrary(so_file)
>>> lib_example
<CDLL 'fortran/example.so', handle 1b8d1b0 at 0x7f4f1cf0b128>
Once loaded, each function in the ABI can be accessed as an attribute of the
CDLL
object. For example:
>>> lib_example.make_udf
<_FuncPtr object at 0x7f4f1d15f688>
>>> lib_example.foo
<_FuncPtr object at 0x7f4f1d15f750>
See the ctypes
documentation for more information on making
foreign calls.
Usage¶
Each user defined type can be described by subclasses of
ctypes.Structure
, which are used to describe the fields in
a C struct
:
class UserDefined(ctypes.Structure):
_fields_ = [
("buzz", ctypes.c_double),
("broken", ctypes.c_double),
("how_many", ctypes.c_int),
]
def __repr__(self):
template = (
"UserDefined(buzz={self.buzz}, "
"broken={self.broken}, "
"how_many={self.how_many})"
)
return template.format(self=self)
class DataContainer(ctypes.Structure):
_fields_ = [("data_", ctypes.c_double * 8)]
@property
def data(self):
result = np.ctypeslib.as_array(self.data_)
return result.reshape((4, 2), order="F")
Note in particular that NumPy provides the numpy.ctypeslib
module
for ctypes
-based interoperability.
To go the other direction, i.e. from a NumPy array
to a double*
pointer:
def numpy_pointer(array):
return array.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
In order to call udf_ptr()
, a UserDefined
instance is
created and an opaque intptr_t
value is passed by reference:
made_it, ptr_as_int = prepare_udf()
lib_example.udf_ptr(ctypes.byref(ptr_as_int))
In order to convert the pointer to the data held in the ctypes.Structure
instance to an intptr_t
, the UserDefined*
pointer is first converted
to a void*
pointer:
def prepare_udf():
made_it = UserDefined()
raw_pointer = ctypes.cast(ctypes.pointer(made_it), ctypes.c_void_p)
intptr_t = get_intptr_t()
ptr_as_int = intptr_t(raw_pointer.value)
return made_it, ptr_as_int
To call view_knob()
, the mangled name must be used:
def view_knob(lib_example):
return lib_example.__example_MOD_view_knob()
Output¶
$ python python/check_ctypes.py
------------------------------------------------------------
quux = foo(c_double(1.0), c_double(16.0)) = c_double(61.0)
------------------------------------------------------------
quuz = make_udf(c_double(1.25), c_double(5.0), c_int(1337))
= UserDefined(buzz=1.25, broken=5.0, how_many=1337)
needsfree(quuz) = True
address(quuz) = 139757150344968 # 0x7f1bbf4d1708
*address(quuz) =
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(c_int(4), 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 = c_long(139757150344992) # 0x7f1bbf4d1720
udf_ptr(ptr_as_int) # Set memory in ``made_it``
made_it = UserDefined(buzz=3.125, broken=-10.5, how_many=101)
needsfree(made_it) = True
*ptr_as_int =
UserDefined(buzz=3.125, broken=-10.5, how_many=101)
------------------------------------------------------------
contained =
[[0. 4.]
[1. 9.]
[1. 2.]
[3. 1.]]
container = make_container(contained)
container.data =
[[0. 4.]
[1. 9.]
[1. 2.]
[3. 1.]]
address(contained) = 43439344 # 0x296d4f0
address(container) = 139757150084784 # 0x7f1bbf491eb0
address(container.data) = 139757150084784 # 0x7f1bbf491eb0
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
------------------------------------------------------------
view_knob() = 1337
turn_knob(c_int(42))
view_knob() = 42
cffi
¶
This example interacts with the shared object
file fortran/example.so
. Similar to ctypes
, the cffi
library
enables the creation of a foreign function interface (FFI) via dlopen
:
>>> import cffi
>>> ffi = cffi.FFI()
>>> so_file = "fortran/example.so"
>>> lib_example = ffi.dlopen(so_file)
>>> lib_example
<cffi.api._make_ffi_library.<locals>.FFILibrary object at 0x7fdd0e364ba8>
After dynamically loading the path, we need to manually define each member of the ABI that we’ll use (both the functions and the structs):
ffi.cdef("void foo(double bar, double baz, double *quux);")
ffi.cdef(
"typedef struct UserDefined {\n"
" double buzz;\n"
" double broken;\n"
" int how_many;\n"
"} UserDefined;"
)
ffi.cdef(
"void make_udf(double *buzz, double *broken,\n"
" int *how_many, UserDefined *quux);"
)
ffi.cdef("void foo_array(int *size, double *val, double *two_val);")
ffi.cdef("void udf_ptr(intptr_t *ptr_as_int);")
ffi.cdef("void just_print();")
ffi.cdef("int __example_MOD_view_knob(void);")
ffi.cdef("void turn_knob(int *new_value);")
In order to convert a NumPy array to a type that can be used with cffi
,
we use the existing ctypes
interface:
def numpy_pointer(array, ffi):
if array.dtype != np.float64:
raise TypeError("Unexpected data type", array.dtype)
return ffi.cast("double *", array.ctypes.data)
Output¶
$ python python/check_cffi.py
------------------------------------------------------------
quux = foo(1.0, 16.0) = 61.0
------------------------------------------------------------
quuz = make_udf(1.25, 5.0, 1337)
= UserDefined(1.25, 5.0, 1337)
------------------------------------------------------------
val =
[[ 3. 4.5 ]
[ 1. 1.25]
[ 9. 0. ]
[-1. 4. ]]
two_val = foo_array(4, 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 = 14735136 # 0xe0d720
udf_ptr(ptr_as_int) # Set memory in ``made_it``
made_it = UserDefined(3.125, -10.5, 101)
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
------------------------------------------------------------
view_knob() = 1337
turn_knob(42)
view_knob() = 42
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
.
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
Go¶
Fortran can be invoked directly from Go code by using the cgo
language extension. A simple example is provided in the Golang
source tree, which is quite helpful. If the Fortran source is in the
same directory as a Go library, cgo
automatically builds the Fortran
files and includes them.
We define the Go package example
in the golang/src
directory. The
Fortran source and the C headers are side-by-side with the Go package:
$ tree golang/src/example/
golang/src/example/
├── example.f90 -> ../../../fortran/example.f90
├── example.go
└── example.h -> ../../../c/example.h
0 directories, 3 files
Package¶
Within the package, we first declare the package to use cgo
and
include the relevant header file:
// #include "example.h"
import "C"
We start by defining user-friendly equivalents of C.struct_UserDefined
and
C.struct_DataContainer
type UserDefined struct {
Buzz float64
Broken float64
HowMany int32
}
type DataContainer struct {
Data *[8]float64
}
func (udf *UserDefined) String() string {
return fmt.Sprintf(
"%T(%f, %f, %d)",
udf, udf.Buzz, udf.Broken, udf.HowMany,
)
}
Adding just_print()
to the package interface is just a matter of
making C.just_print
public:
func JustPrint() {
C.just_print()
}
When passing in *float64
(i.e. pointer types), the underlying
values can be passed along to C.make_udf
without having to copy
any data, e.g. via (*C.double)(buzz)
. The foreign call will populate
the fields of a C.struct_UserDefined
, which will need to be converted
to normal Go types when a UserDefined
value is created for the return.
To avoid copying when constructing a UserDefined
object, we dereference
a field value (e.g. &quuz.buzz
), convert the reference to a non-C
pointer type (e.g. (*float64)(&quuz.buzz)
) and then dereference the
value:
func MakeUDF(buzz, broken *float64, howMany *int32) *UserDefined {
var quuz C.struct_UserDefined
C.make_udf(
(*C.double)(buzz),
(*C.double)(broken),
(*C.int)(howMany),
&quuz,
)
return &UserDefined{
*(*float64)(&quuz.buzz),
*(*float64)(&quuz.broken),
*(*int32)(&quuz.how_many),
}
}
When dealing with array types, the first value in a slice is dereferenced and
then converted into a C pointer (e.g. (*C.double)(&val[0])
):
func FooArray(size *int32, val []float64) []float64 {
twoVal := make([]float64, len(val), cap(val))
C.foo_array(
(*C.int)(size),
(*C.double)(&val[0]),
(*C.double)(&twoVal[0]),
)
return twoVal
}
When calling udf_ptr()
, a UserDefined
value must be created,
dereferenced, cast to unsafe.Pointer
and then cast again to uintptr
:
madeIt := example.UserDefined{}
ptrAsInt := uintptr(unsafe.Pointer(&madeIt))
example.UDFPtr(&ptrAsInt)
Only then can the uintptr
be converted to a C.intptr_t
:
func UDFPtr(ptrAsInt *uintptr) {
C.udf_ptr(
(*C.intptr_t)(unsafe.Pointer(ptrAsInt)),
)
}
func MakeContainer(contained []float64) *DataContainer {
var container C.struct_DataContainer
C.make_container(
(*C.double)(&contained[0]),
&container,
)
dataPtr := (*[8]float64)(unsafe.Pointer(&container.data))
return &DataContainer{dataPtr}
}
In the case of view_knob()
, the mangled name must be used:
func ViewKnob() int32 {
knobValue := C.__example_MOD_view_knob()
return (int32)(knobValue)
}
func TurnKnob(newValue *int32) {
C.turn_knob(
(*C.int)(newValue),
)
}
Output¶
The Go example can be run via go run
. As with C and C++, the
gfortran
search path may need to be explicitly provided (with -L
flags). This
can be done with the CGO_LDFLAGS
environment variable.
$ go run golang/main.go
------------------------------------------------------------
quux = foo(1.000000, 16.000000) = 61.000000
------------------------------------------------------------
quuz = make_udf(1.250000, 5.000000, 1337)
= *example.UserDefined(1.250000, 5.000000, 1337)
------------------------------------------------------------
foo_array(
4,
[[3.000000, 4.500000],
[1.000000, 1.250000],
[9.000000, 0.000000],
[-1.000000, 4.000000]],
) =
[[6.000000, 9.000000],
[2.000000, 2.500000],
[18.000000, 0.000000],
[-2.000000, 8.000000]]
------------------------------------------------------------
ptrAsInt = &madeIt
ptrAsInt = 842350544096 // 0xc4200144e0
udf_ptr(&ptrAsInt) // Set memory in ``madeIt``
&madeIt = *example.UserDefined(3.125000, -10.500000, 101)
------------------------------------------------------------
contained =
[[0.000000, 4.000000],
[1.000000, 9.000000],
[1.000000, 2.000000],
[3.000000, 1.000000]]
container = make_container(contained)
container.Data =
[[0.000000, 4.000000],
[1.000000, 9.000000],
[1.000000, 2.000000],
[3.000000, 1.000000]]
&contained = 842350560256 // 0xc420018400
&container = 842350518320 // 0xc42000e030
container.Data = 842350560320 // 0xc420018440
------------------------------------------------------------
just_print()
======== BEGIN FORTRAN ========
just_print() was called
======== END FORTRAN ========
------------------------------------------------------------
view_knob() = 1337
turn_knob(42)
view_knob() = 42