I have looked at (and used as inspiration) the following SO threads:
- Using python-ctypes to interface fortran with python
- call functions from a shared fortran library in python
- Interfacing Python with Fortran module by ctypes
- creating a python module using ctypes
- Call fortran function from Python with ctypes
These get me part of the way, but with recent deprecations many answers that you can find on SO are unfortunately outdated.
Problems
- distutils is deprecated with removal planned for Python 3.12.
- numpy.distutils is deprecated, and will be removed for Python >= 3.12.
- setuptools does not provide fortran build support (see e.g. GitHub issue #2372)
What I have
I managed to create a simplified example project (details below) consisting of fortran source code that I interface with python using ctypes. Compiling the fortran code I create an .so
library which I can then read in with ctypes’ CDLL
.
Doing these steps manually one after the other with meson works fine, but I struggle to get this fully automated and packaged up (with pip).
What I want
I would like the whole compilation and linking process to be automated such that a simple pip install .
performs all the necessary compiling and linking, creating a python package that I can import from anywhere.
Ultimately I will want to be able to package everything and release it on PyPI.
Note that I want to achieve this without tools such as cython or f2py, which would probably work very well for my highly simplified example here, but not for the decades old legacy code, for which I am working through this simplified problem…
Example
Project structure
project_root
├── fortran2python
│ ├── example1.py
│ └── __init__.py
├── meson.build
├── pyproject.toml
└── src
├── example1_bindings.f90
└── example1.f90
src/example1.f90
This fortran file provides a toy subroutine called real_sum
adding two real numbers:
module example1
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: real_sum
integer, parameter :: dp = real64
contains
subroutine real_sum(a, b, res)
!! Sum of two reals
real(dp), intent(in) :: a, b
real(dp), intent(out) :: res
res = a + b
end subroutine real_sum
end module example1
src/example1_bindings.f90
This file introduces the c-bindings needed for ctypes.
module example1_bindings
use example1
use iso_c_binding, only: c_double, c_int
implicit none
contains
real(c_double) function real_sum_c(a, b) result(res) bind(c, name='real_sum')
real(c_double), value, intent(in) :: a, b
call real_sum(a, b, res)
end function real_sum_c
end module example1_bindings
fortran2python/example1.py
This file uses ctypes to read in the library libexample1.so
created from the above fortran files, making it available to python. It also performs the necessary type declarations.
from ctypes import CDLL, c_double
import os
# Load shared library with C-bindings.
libpath = os.path.dirname(os.path.realpath(__file__))
libfile = os.path.join(libpath, '../build/libexample1.so')
libexample1 = CDLL(libfile)
# Define the function signature for the Fortran function `real_sum`.
real_sum = libexample1.real_sum
real_sum.argtypes = [c_double, c_double] # allows for automatic type conversion
real_sum.restype = c_double # the default return type would be int
fortran2python/__init__.py
Provide the __init__.py
file that allows me to later import fortran2python
as a module, and make the real_sum
function available.
from fortran2python import example1
real_sum = example1.real_sum
Compilation setup
My go-to for python packaging used to be setuptools, but with the deprecation of the distutils packages and with setuptool’s lack of fortran build support, I guess I need to look elsewhere. That’s why I’ve looked into meson, which works well as long as I do things by hand, but the automation via pip install .
fails.
meson.build
project('fortran2python', 'fortran')
fortran_src = ['src/example1.f90', 'src/example1_bindings.f90']
example1_lib = library('example1', fortran_src, install: true)
With this I can run
meson setup build
meson compile -C build
This produces the following build
folder:
project_root
├── build
│ ├── build.ninja
│ ├── compile_commands.json
│ ├── libexample1.so
│ ├── libexample1.so.p
│ │ ├── depscan.dd
│ │ ├── example1_bindings.mod
│ │ ├── example1.dat
│ │ ├── example1-deps.json
│ │ ├── example1.mod
│ │ ├── src_example1_bindings.f90.o
│ │ └── src_example1.f90.o
│ ├── meson-info
│ │ ├── ...
│ ├── meson-logs
│ │ └── ...
│ └── meson-private
│ ├── ...
...
With the libexample1.so
library compiled, I can then run
python -c "from fortran2python import real_sum; print(real_sum(1, 2))"
from the project root directory and get the correct answer (3.0
in this case). So far this works great.
But how can I pip install the fortran2python
package such that it becomes available from anywhere, and such that I can package it up and distribute to PyPI?
pyproject.toml
I’ve set up a pyproject.toml
file that’s supposed to use meson-python as the build-system.
[build-system]
requires = ["meson-python"]
build-backend = "mesonpy"
[project]
name = "fortran2python"
version = "0.0.1"
description = "Examples for wrapping Fortran code with Python using ctypes."
This isn’t enough, though. While pip install .
or pip install -e .
run through without complaint, I am faced with the error No module named 'fortran2python'
(unless I’m in the project root directory…). What’s more, this doesn’t even invoke the meson commands (meson setup build
and meson compile -C build
) in the first place.
I looked at the python packaging introduction with meson-python, but I struggle to understand how I might be able to adapt that example to mine… I probably need to add an extension_module
to the meson.build
file somehow, but my attempts have failed.
Running on:
- OS: Linux
- compilers: gcc and gfortran (version 13.2.1 20230801)
- python version: 3.12