【问题标题】:Cython and fortran - how to compile together without f2pyCython 和 fortran - 如何在没有 f2py 的情况下一起编译
【发布时间】:2012-10-02 18:59:00
【问题描述】:

最终更新

这个问题是关于如何编写一个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的。

尝试了这个,我的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)。

在雪豹上,以下适用:

这意味着模块编译(万岁!)(虽然似乎没有 --inplace 用于 numpy,所以我不得不在系统范围内安装测试模块:/)但我仍然在 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 -> .o.f90 -> .o 并链接它们。如果我知道如何包含所有相关的头文件,我可以自己编写这个编译器行。

请告诉我,我不需要为此编写自己的 makefile...解决了整个问题。)请注意,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...

【问题讨论】:

标签: python c fortran cython distutils


【解决方案1】:

更新: 我在 github 上创建了一个项目,它手动完成了编译行的生成。它叫做complicated_build

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


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

  • 进口
  • 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 命令 - 高级学生的练习;)

【讨论】:

    【解决方案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 库的链接。

    【讨论】:

    • 这是一个非常优雅的解决方案——干得好!我会看看我能不能让它工作,如果可以的话,改变“正确”的答案。
    【解决方案3】:

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

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

    在命令提示符下:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-06-30
      • 2018-09-03
      • 2020-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-12
      相关资源
      最近更新 更多