Cython和fortran – 如何在没有f2py的情况下一起编译
最终更新
这个问题是关于如何编写一个setup.py
,它将编译一个直接访问FORTRAN代码的cython模块,就像C一样。 这是一个相当漫长而艰巨的解决方案之旅,但下面包含了完整的混乱。
原始问题
我有一个扩展,它是一个Cython文件,它设置一些堆内存并将其传递给fortran代码,以及一个fortran文件,这是一个古老的模块,我想避免重新实现,如果可以的话。
.pyx
文件可以很好地编译为C,但是cython编译器会对.f90
文件进行.f90
,并出现以下错误:
$ python setup.py build_ext --inplace running build_ext cythoning delaunay/__init__.pyx to delaunay/__init__.c building 'delaunay' extension error: unknown file type '.f90' (from 'delaunay/stripack.f90')
这是我的安装文件(上半部分):
from distutils.core import setup, Extension from Cython.Distutils import build_ext ext_modules = [ Extension("delaunay", sources=["delaunay/__init__.pyx", "delaunay/stripack.f90"]) ] setup( cmdclass = {'build_ext': build_ext}, ext_modules = ext_modules, ... )
注意:我最初错误地指定了fortran文件的位置(没有目录前缀),但是在我修复之后,这种方式完全相同。
我尝试过的事情:
我找到了这个 ,并尝试传递fortran编译器(即gfortran)的名称,如下所示:
$ python setup.py config --fcompiler=gfortran build_ext --inplace usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] or: setup.py --help [cmd1 cmd2 ...] or: setup.py --help-commands or: setup.py cmd --help error: option --fcompiler not recognized
而且我也试过删除--inplace
,以防这是问题(它不是,与顶部错误消息相同)。
那么,我该如何编译这个fortran? 我可以自己将它破解成.o
并将其链接起来吗? 或者这是Cython中的一个错误 ,它会迫使我重新实现distutils或者使用预处理器进行破解?
UPDATE
所以,在检查了numpy.distutils
软件包后,我对这个问题有了更多了解。 看来你必须这样做
- 使用cython将.pyx文件转换为cpython .c文件,
- 然后使用支持fortran的
Extension
/setup()
组合,就像numpy
一样。
尝试过这个,我的setup.py
现在看起来像这样:
from numpy.distutils.core import setup from Cython.Build import cythonize from numpy.distutils.extension import Extension cy_modules = cythonize('delaunay/sphere.pyx') e = cy_modules[0] ext_modules = [ Extension("delaunay.sphere", sources=e.sources + ['delaunay/stripack.f90']) ] setup( ext_modules = ext_modules, name="delaunay", ... )
(请注意,我还重新组织了一下模块,因为看似__init__.pyx
是不允许的……)
现在,事情变得越来越多,并且依赖于平台。 我有两个测试系统 – 一个是使用Macports Python 2.7的Mac OS X 10.6(Snow Leopard),另一个是使用系统python 2.7的Mac OS X 10.7(Lion)。
在Snow Leopard上,以下内容适用:
这意味着该模块编译(欢呼!)(虽然没有--inplace
在numpy的位置,似乎,所以我不得不在系统范围内安装测试模块:/)但我仍然在import
遇到崩溃,如下所示:
>>> import delaunay Traceback (most recent call last): File "", line 1, in File "site-packages/delaunay/__init__.py", line 1, in from sphere import delaunay_mesh ImportError: dlopen(site-packages/delaunay/sphere.so, 2): no suitable image found. Did find: site-packages/delaunay/sphere.so: mach-o, but wrong architecture
在Lion上,我遇到了一个编译错误,在一个相当混乱的编译行之后:
gfortran:f77: build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.f /usr/local/bin/gfortran -Wall -arch i686 -arch x86_64 -Wall -undefined dynamic_lookup -bundle build/temp.macosx-10.7-intel-2.7/delaunay/sphere.o build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/spheremodule.o build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/fortranobject.o build/temp.macosx-10.7-intel-2.7/delaunay/stripack.o build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.o -lgfortran -o build/lib.macosx-10.7-intel-2.7/delaunay/sphere.so ld: duplicate symbol _initsphere in build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/spheremodule.o ldand :build /temp.macosx-10.7-intelduplicate- 2.7symbol/ delaunay/sphere.o _initsphere in forbuild architecture /i386 temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/spheremodule.o and build/temp.macosx-10.7-intel-2.7/delaunay/sphere.o for architecture x86_64
现在让我们回过头来看看这里的细节。 首先,我知道64位Mac OS X中的架构冲突有很多令人头疼的问题; 我必须非常努力地让Macports Python在Snow Leopard机器上工作(只是从系统python 2.6升级)。 我也知道,当你看到gfortran -arch i686 -arch x86_64
你正在向编译器发送混合消息。 在那里埋藏着各种特定于平台的问题,我们不需要在这个问题的背景下担心。
但是让我们看看这一行 : gfortran:f77: build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.f
什么是numpy做什么?! 我在这个版本中不需要任何f2pyfunction! 我实际上编写了一个cython模块,以避免处理f2py的疯狂(我需要有4个或5个输出变量,以及两个都没有输出的参数 – 在f2py中都没有得到很好的支持。)我只是想要它编译.c
– > .o
,和.f90
– > .o
并链接它们。 如果我知道如何包含所有相关的头文件,我可以自己编写这个编译器行。
请告诉我,我不需要为此编写我自己的makefile …或者有一种方法可以将fortran转换为(输出兼容)C所以我可以避免python看到.f90扩展名(它修复了整个问题。)请注意, f2c
不适用于此,因为它只适用于F77,这是一种更现代的方言(因此.f90
文件扩展名)。
更新2以下bash脚本将很乐意编译并链接代码:
PYTHON_H_LOCATION="/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/" cython sphere.pyx gcc -arch x86_64 -c sphere.c -I$PYTHON_H_LOCATION gfortran -arch x86_64 -c stripack.f90 gfortran -arch x86_64 -bundle -undefined dynamic_lookup -L/opt/local/lib *.o -o sphere.so
关于如何使这种hack与setup.py兼容的任何建议? 我没有安装这个模块的人必须手动找到Python.h
…
更新:我在github上创建了一个项目,它手动包装了这个生成的编译行。 它叫做complex_build 。
更新2:事实上,“手动生成”是一个非常糟糕的主意,因为它是特定于平台的 – 项目现在从distutils.sysconfig
模块读取值,这是用于编译python的设置(即我们想要的确切内容)猜测的唯一设置是fortran编译器和文件扩展名(用户可配置)。 我怀疑它现在重新实现了相当多的干扰!
这样做的方法是编写自己的编译器行,并将它们放入setup.py
。 我在下面展示了一个适用于我(非常简单)案例的示例,它具有以下结构:
- import
-
cythonize()
任何.pyx
文件,所以你只有fortran和C文件。 - 定义一个编译代码的
build()
函数:- 也许一些易于更改的常量,如编译器名称和体系结构
- 列出fortran和C文件
- 生成将构建模块的shell命令
- 添加链接器行
- 运行shell命令。
- 如果命令已
install
且目标尚不存在,请构建它。 - 运行安装程序(将构建纯python部分)
- 如果命令是
build
,请立即运行构建。
我的实现如下所示。 它仅针对一个扩展模块而设计,并且每次都重新编译所有文件,因此可能需要进一步扩展才能更常用。 另请注意,我已对各种unix /
s进行了硬编码,因此如果您将其移植到Windows,请确保使用os.path.sep
修改或替换。
from distutils.core import setup from distutils.sysconfig import get_python_inc from Cython.Build import cythonize import sys, os, shutil cythonize('delaunay/sphere.pyx') target = 'build/lib/delaunay/sphere.so' def build(): fortran_compiler = 'gfortran' c_compiler = 'gcc' architecture = 'x86_64' python_h_location = get_python_inc() build_temp = 'build/custom_temp' global target try: shutil.rmtree(build_temp) except OSError: pass os.makedirs(build_temp) # if you get an error here, please ensure the build/ ... # folder is writable by this user. c_files = ['delaunay/sphere.c'] fortran_files = ['delaunay/stripack.f90'] c_compile_commands = [] for cf in c_files: # use the path (sans /s), without the extension, as the object file name: components = os.path.split(cf) name = components[0].replace('/', '') + '.'.join(components[1].split('.')[:-1]) c_compile_commands.append( c_compiler + ' -arch ' + architecture + ' -I' + python_h_location + ' -o ' + build_temp + '/' + name + '.o -c ' + cf ) fortran_compile_commands = [] for ff in fortran_files: # prefix with f in case of name collisions with c files: components = os.path.split(ff) name = components[0].replace('/', '') + 'f' + '.'.join(components[1].split('.')[:-1]) fortran_compile_commands.append( fortran_compiler + ' -arch ' + architecture + ' -o ' + build_temp + '/' + name + '.o -c ' + ff ) commands = c_compile_commands + fortran_compile_commands + [ fortran_compiler + ' -arch ' + architecture + ' -bundle -undefined dynamic_lookup ' + build_temp + '/*.o -o ' + target ] for c in commands: os.system(c) if 'install' in sys.argv and not os.path.exists(target): try: os.makedirs('build/lib/delaunay') except OSError: # we don't care if the containing folder already exists. pass build() setup( name="delaunay", version="0.1", ... packages=["delaunay"] ) if 'build' in sys.argv: build()
这可以包含在一个新的Extension
类中,我猜它有自己的build_ext
命令 – 高级学生的练习;)
只需在Python之外构建和安装您的老式Fortran库,然后在distutils中链接到它。 您的问题表明您不打算使用此库进行调整,因此可能会进行一次又一次的安装(使用库的构建和安装说明)。 然后将Python扩展链接到已安装的外部库:
ext_modules = [ Extension("delaunay", sources = ["delaunay/__init__.pyx"], libraries = ["delaunay"]) ]
对于你意识到你需要其他语言的包装器的情况,这种方法也是安全的,例如Matlab,Octave,IDL,……
更新
在某些时候,如果您最终需要包含多个这样的外部库,那么添加一个安装所有这些库的顶级构建系统并管理所有包装器的构建是有利的。 我有这个目的的cmake
,它非常适合处理系统范围的构建和安装。 但是,它无法构建开箱即用的Python东西,但可以很容易地教它在每个子目录python
调用“python setup.py install”,从而调用distutils。 所以整个构建过程如下所示:
mkdir build cd build cmake .. make make install make python (make octave) (make matlab)
始终将核心库代码与特定前端语言(也适用于您自己的项目)的包装器分开是非常重要的,因为它们往往变化得相当快。 否则可以在numpy
的例子中看到:除了编写一个通用的C库libndarray.so
并为Python创建瘦包装器之外,源代码中的每个地方都有Python C API调用。 这就是现在阻止Pypy
作为CPython的一个重要替代品,因为为了得到numpy
他们必须支持CPython API的最后一点,这是他们不能做的,因为他们有一个即时编译器和一个不同的垃圾收集器。 这意味着我们错过了很多潜在的改进。
底线:
-
分别构建通用Fortran / C库并在系统范围内安装它们。
-
为包装器设置一个单独的构建步骤,应尽可能保持轻量级,这样就可以轻松适应下一个大型语言X. 如果有一个安全的假设,那就是X将支持与C库的链接。
您可以在distutils
之外构建目标文件,然后使用Extension的构造函数的extra_objects
参数在链接步骤中包含它。 在setup.py
:
... e = Extension(..., extra_objects = ['holycode.o']) ...
在命令提示符下:
# gfortran -c -fPIC holycode.f # ./setup.py build_ext ...
只有一个外部对象,这对许多人来说是最简单的方法。