Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@
Python with NumPy arrays. It is based on CFFI and currently assumes the use of
gfortran or Intel Fortran.

## ABI safety and compiler assumptions

`fffi` calls the compiler-generated Fortran ABI. It is meant for controlled
Python/Fortran prototyping where the wrapper and the target library are built
for the same compiler family and compatible compiler version. It is not a
portable replacement for `ISO_C_BINDING`/`BIND(C)`.

For new stable public interfaces, prefer explicit `BIND(C)` entry points. Use
`fffi` for non-`BIND(C)` libraries only when these assumptions hold:

* The Fortran compiler is supported by `fffi` (`gfortran` or Intel Fortran).
* The wrapper is generated for the same compiler ABI as the shared library.
* ABI-changing compiler flags are not mixed between the library and callers.
In particular, do not mix gfortran objects compiled with `-ff2c` and the
default GNU calling convention.
* Scalars are passed as pointers unless the Fortran dummy argument has
`VALUE`.
* Assumed-shape arrays use compiler-specific descriptors. `fffi` generates
those descriptors for the supported compiler layouts; this is why arbitrary
Fortran compilers are not supported.
* Character arguments need hidden length arguments. The hidden length type
changed in gfortran 8, so generated signatures must match the compiler
version.
* Fortran `LOGICAL` values are compiler ABI values, not a cross-compiler
Boolean format. With gfortran, only 0 and 1 are valid.
* Function results follow the compiler ABI. Scalar numeric and C99 complex
results may be returned by value, while arrays and character results use
hidden result arguments. Subroutines return `void`.

These rules follow the GNU Fortran argument-passing and code-generation
documentation:

* <https://gcc.gnu.org/onlinedocs/gfortran/Argument-passing-conventions.html>
* <https://gcc.gnu.org/onlinedocs/gfortran/Code-Gen-Options.html>

The focus of `fffi` is dynamical automatic generation of interfaces directly
within Python with a minimum of extra code generation and files to allow
for the simplest possible workflow for fast prototyping of Python/Fortran codes.
Expand Down
3 changes: 3 additions & 0 deletions fffi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
from .parser import *
from .fortran_wrapper import *
from .fffi import *

fortran_library = FortranLibrary
fortran_module = FortranModule
50 changes: 49 additions & 1 deletion fffi/fortran_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
"""

import numpy as np
import weakref

from cffi import FFI
from .common import libexts, debug

_NUMPY_DESCRIPTOR_CACHES = {}


def arraydims(compiler):
if compiler['name'] == 'gfortran':
Expand Down Expand Up @@ -278,6 +281,10 @@ def numpy2fortran(ffi, arr, compiler):
raise TypeError('needs Fortran order in NumPy arrays')

ndims = len(arr.shape)
cached = _cached_numpy_descriptor(ffi, arr, compiler, ndims)
if cached is not None:
return cached

arrdata = ffi.new('array_{}d*'.format(ndims))
arrdata.base_addr = ffi.cast('void*', arr.ctypes.data)
if compiler['name'] == 'gfortran':
Expand Down Expand Up @@ -316,7 +323,48 @@ def numpy2fortran(ffi, arr, compiler):
arrdata.dim[kd].extent = arr.shape[kd]
distance = distance*arr.shape[kd]

return arrdata
return _cache_numpy_descriptor(ffi, arr, compiler, arrdata)


def _array_cache_key(arr, compiler):
return (id(arr), compiler['name'], compiler['version'])


def _array_cache_signature(arr):
return (arr.ctypes.data, arr.shape, arr.strides, arr.dtype.str)


def _cached_numpy_descriptor(ffi, arr, compiler, ndims):
cache = _NUMPY_DESCRIPTOR_CACHES.get(id(ffi))
if cache is None:
return None

entry = cache.get(_array_cache_key(arr, compiler))
if entry is None:
return None

ref, signature, descriptor = entry
if ref() is arr and signature == _array_cache_signature(arr):
return descriptor

cache.pop(_array_cache_key(arr, compiler), None)
return None


def _cache_numpy_descriptor(ffi, arr, compiler, descriptor):
try:
ref = weakref.ref(arr)
except TypeError:
return descriptor

cache = _NUMPY_DESCRIPTOR_CACHES.get(id(ffi))
if cache is None:
cache = {}
_NUMPY_DESCRIPTOR_CACHES[id(ffi)] = cache

cache[_array_cache_key(arr, compiler)] = (
ref, _array_cache_signature(arr), descriptor)
return descriptor


def call_fortran(ffi, lib, function, compiler, module, *args):
Expand Down
6 changes: 3 additions & 3 deletions fffi/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,15 @@ def __init__(self, **kwargs):
self.namespace = {**self.namespace, **decl.namespace}


class SubprogramHeading(object):
class SubProgramHeading(object):
"""Class representing a Fortran internal subprogram."""

def __init__(self, **kwargs):
self.name = kwargs.pop('name')
self.arguments = kwargs.pop('arguments', []) # optional


class SubprogramEnding(object):
class SubProgramEnding(object):
"""Class representing a Fortran internal subprogram."""

def __init__(self, **kwargs):
Expand Down Expand Up @@ -315,7 +315,7 @@ def parse(inputs, debug=False):
classes = [Fortran,
Module,
InternalSubprogram,
SubprogramHeading,
SubProgramHeading,
Declaration,
Stmt,
DeclarationAttribute,
Expand Down
45 changes: 25 additions & 20 deletions tests/01_arrays/test1_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,34 +109,39 @@ def test_array_2d_multi(self):
then 1000 times. Check for memory leaks via tracemalloc
"""

if tracemalloc.is_tracing():
tracemalloc.stop()
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
arr = np.ones((self.m, self.n), order='F') # correct array order
try:
snapshot1 = tracemalloc.take_snapshot()
arr = np.ones((self.m, self.n), order='F') # correct array order

for k in range(10):
arr[:, :] = 1.0
self.mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, self.refarr)
for k in range(10):
arr[:, :] = 1.0
self.mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, self.refarr)

gc.collect()
snapshot2 = tracemalloc.take_snapshot()
gc.collect()
snapshot2 = tracemalloc.take_snapshot()

stats = snapshot2.compare_to(snapshot1, 'filename')
statsum = sum(stat.count_diff for stat in stats)
stats = snapshot2.compare_to(snapshot1, 'filename')
statsum = sum(stat.count_diff for stat in stats)

snapshot1 = tracemalloc.take_snapshot()
for k in range(1000):
arr[:, :] = 1.0
self.mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, self.refarr)
snapshot1 = tracemalloc.take_snapshot()
for k in range(1000):
arr[:, :] = 1.0
self.mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, self.refarr)

gc.collect()
snapshot2 = tracemalloc.take_snapshot()
gc.collect()
snapshot2 = tracemalloc.take_snapshot()

stats = snapshot2.compare_to(snapshot1, 'filename')
self.assertLessEqual(sum(stat.count_diff for stat in stats), statsum)
stats = snapshot2.compare_to(snapshot1, 'filename')
self.assertLessEqual(sum(stat.count_diff for stat in stats), statsum)
finally:
tracemalloc.stop()


if __name__ == "__main__":
test = TestArrays()
test.setUpClass()
test.setUpClass()
55 changes: 30 additions & 25 deletions tests/01_arrays/test_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,29 +123,34 @@ def test_array_2d_multi(mod_arrays, refarr):
then 1000 times. Check for memory leaks via tracemalloc
"""

if tracemalloc.is_tracing():
tracemalloc.stop()
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
arr = np.ones((m, n), order='F') # correct array order

for _ in range(10):
arr[:, :] = 1.0
mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, refarr)

gc.collect()
snapshot2 = tracemalloc.take_snapshot()

stats = snapshot2.compare_to(snapshot1, 'filename')
statsum = sum(stat.count_diff for stat in stats)

snapshot1 = tracemalloc.take_snapshot()
for _ in range(1000):
arr[:, :] = 1.0
mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, refarr)

gc.collect()
snapshot2 = tracemalloc.take_snapshot()

stats = snapshot2.compare_to(snapshot1, 'filename')
assert sum(stat.count_diff for stat in stats) <= statsum + 16
try:
snapshot1 = tracemalloc.take_snapshot()
arr = np.ones((m, n), order='F') # correct array order

for _ in range(10):
arr[:, :] = 1.0
mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, refarr)

gc.collect()
snapshot2 = tracemalloc.take_snapshot()

stats = snapshot2.compare_to(snapshot1, 'filename')
statsum = sum(stat.count_diff for stat in stats)

snapshot1 = tracemalloc.take_snapshot()
for _ in range(1000):
arr[:, :] = 1.0
mod_arrays.test_array_2d(arr)
np.testing.assert_almost_equal(arr, refarr)

gc.collect()
snapshot2 = tracemalloc.take_snapshot()

stats = snapshot2.compare_to(snapshot1, 'filename')
assert sum(stat.count_diff for stat in stats) <= statsum + 16
finally:
tracemalloc.stop()