Cython和fortran - 如何在没有f2py的情况下一起编译

时间:2012-10-02 18:59:00

标签: python c fortran cython distutils

最终更新

这个问题是关于如何编写一个setup.py来编译一个直接访问FORTRAN代码的cython模块,就像C一样。这是一个相当漫长而艰巨的解决方案之旅,但下面包含完整的混乱。

原始问题

我有一个扩展,它是一个Cython文件,它设置一些堆内存并将其传递给fortran代码,以及一个fortran文件,这是一个古老的模块,我想避免重新实现,如果我可以。

.pyx文件可以很好地编译为C,但是cython编译器在.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文件的位置(没有目录前缀),但是在我修复之后,这种方式完全相同。

我尝试过的事情:

我找到this,并尝试传递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并将其链接起来吗?或is this a bug in Cython,这将迫使我重新实现distutils或破解预处理器?

更新

因此,在检查了numpy.distutils包之后,我对这个问题有了更多了解。看来你必须

  1. 使用cython将.pyx文件转换为cpython .c文件,
  2. 然后使用支持fortran的Extension / setup()组合,例如numpy
  3. 试过这个,我的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上,以下内容适用:

    这意味着该模块编译(欢呼!)(尽管numpy似乎没有--inplace,但似乎我必须在系统范围内安装测试模块:/)但我仍然得到import崩溃如下:

      >>> import delaunay
      Traceback (most recent call last):
        File "<input>", line 1, in <module>
        File "<snip>site-packages/delaunay/__init__.py", line 1, in <module>
          from sphere import delaunay_mesh
      ImportError: dlopen(<snip>site-packages/delaunay/sphere.so, 2): no suitable image found.  Did find:
        <snip>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正在做什么?!我在这个版本中不需要任何f2py功能!我实际上编写了一个cython模块以避免处理f2py的疯狂(我需要有4个或5个输出变量,以及两个都没有输出的参数 - 两者都没有这在f2py中得到了很好的支持。)我只想让它编译.c - &gt; .o.f90 - &gt; .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 ...

3 个答案:

答案 0 :(得分:3)

UPDATE:我在github上创建了一个项目,它手动包装了这个生成的编译行。它被称为complicated_build

更新2:实际上,“手工生成”是一个非常糟糕的主意,因为它是特定于平台的 - 项目现在从distutils.sysconfig模块中读取值,这是设置用于编译python(即我们想要的),唯一猜到的设置是fortran编译器和文件扩展(用户可配置)。我怀疑它现在正在重新实现一些干扰!


这样做的方法是编写自己的编译器行,然后将它们放入setup.py。我在下面展示了一个适用于我(非常简单)案例的例子,它具有以下结构:

  • 进口
  • cythonize()任意.pyx个文件,因此您只有fortran和C文件。
  • 定义一个编译代码的build()函数:
    • 也许是一些容易改变的常量,比如编译器名称和架构
    • 列出fortran和C文件
    • 生成将构建模块的shell命令
    • 添加链接器行
    • 运行shell命令。
  • 如果命令是install且目标尚不存在,请构建它。
  • 运行安装程序(将构建纯python部分)
  • 如果命令为build,请立即运行构建。

我的实现如下所示。它仅设计用于一个扩展模块,并且每次都重新编译所有文件,因此可能需要进一步扩展才能更常用。另请注意,我已对各种unix /进行了硬编码,因此如果您将其移植到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命令 - 高级学生的练习;)

答案 1 :(得分:2)

只需在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的最后一点,因为他们不能有一个即时编译器和一个不同的垃圾收集器。这意味着我们错过了很多潜在的改进。

底线:

  1. 分别构建通用Fortran / C库并在系统范围内安装它们。

  2. 为包装器设置一个单独的构建步骤,应尽可能保持轻量级,这样就可以轻松适应下一个大型语言X.如果有一个安全的假设,那就是X将支持与C库的链接。

答案 2 :(得分:0)

您可以在distutils之外构建目标文件,然后使用Extension的构造函数的extra_objects参数在链接步骤中包含它。在setup.py

...
e = Extension(..., extra_objects = ['holycode.o'])
...

在命令提示符下:

# gfortran -c -fPIC holycode.f
# ./setup.py build_ext ...

只有一个外部对象,这对许多人来说是最简单的方法。