diff --git a/README.md b/README.md index c0ca2b0..ff184a4 100644 --- a/README.md +++ b/README.md @@ -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: + +* +* + 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. diff --git a/fffi/__init__.py b/fffi/__init__.py index 80a2c95..273b48e 100644 --- a/fffi/__init__.py +++ b/fffi/__init__.py @@ -7,3 +7,6 @@ from .parser import * from .fortran_wrapper import * from .fffi import * + +fortran_library = FortranLibrary +fortran_module = FortranModule diff --git a/fffi/fortran_wrapper.py b/fffi/fortran_wrapper.py index 845c3fe..5b062df 100644 --- a/fffi/fortran_wrapper.py +++ b/fffi/fortran_wrapper.py @@ -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': @@ -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': @@ -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): diff --git a/fffi/parser/parser.py b/fffi/parser/parser.py index eef2606..e088078 100644 --- a/fffi/parser/parser.py +++ b/fffi/parser/parser.py @@ -116,7 +116,7 @@ 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): @@ -124,7 +124,7 @@ def __init__(self, **kwargs): self.arguments = kwargs.pop('arguments', []) # optional -class SubprogramEnding(object): +class SubProgramEnding(object): """Class representing a Fortran internal subprogram.""" def __init__(self, **kwargs): @@ -315,7 +315,7 @@ def parse(inputs, debug=False): classes = [Fortran, Module, InternalSubprogram, - SubprogramHeading, + SubProgramHeading, Declaration, Stmt, DeclarationAttribute, diff --git a/tests/01_arrays/test1_arrays.py b/tests/01_arrays/test1_arrays.py index 4074eff..21aee46 100644 --- a/tests/01_arrays/test1_arrays.py +++ b/tests/01_arrays/test1_arrays.py @@ -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() \ No newline at end of file + test.setUpClass() diff --git a/tests/01_arrays/test_arrays.py b/tests/01_arrays/test_arrays.py index a7a5450..f3096b0 100644 --- a/tests/01_arrays/test_arrays.py +++ b/tests/01_arrays/test_arrays.py @@ -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()