代码之家  ›  专栏  ›  技术社区  ›  tehwalrus

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

  •  11
  • tehwalrus  · 技术社区  · 12 年前

    最终更新

    这个问题是关于如何写 setup.py 它将编译一个像C一样直接访问FORTRAN代码的cython模块。这是一个相当漫长和艰巨的解决之旅,但以下是完整的混乱情况。

    原始问题

    我有一个扩展名,它是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. 然后使用 Extension / setup() 支持fortran的组合,如 numpy 的。

    试过之后,我的 设置.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的MacOS X 10.7(Lion)。

    对于雪豹,以下内容适用:

    这意味着模块会编译(万岁!)(尽管没有 --就地 对于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位MacOSX中有很多关于架构冲突的头疼问题;我不得不非常努力地让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

    努皮在干什么?! 在这个版本中我不需要任何f2py功能!我实际上写了一个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
    

    关于如何使这种破解与setup.py兼容,有什么建议吗?我没有人安装这个模块就必须去找 Python.h 手动。。。

    3 回复  |  直到 7 年前
        1
  •  4
  •   tehwalrus    11 年前

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

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


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

    • 进口
    • cythonize() 任何 .pyx 文件,所以您只有fortran和C文件。
    • 定义 build() 用于编译代码的函数:
      • 也许是一些易于更改的常量,如编译器名称和体系结构
      • 列出fortran和C文件
      • 生成将构建模块的shell命令
      • 添加链接器行
      • 运行shell命令。
    • 如果命令是 install 如果目标还不存在,就建立它。
    • 运行setup(它将构建纯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
  •  2
  •   Stefan    11 年前

    只需在Python之外构建并安装您的老式Fortran库,然后在distutils中链接到它。你的问题表明你不打算对这个库进行调整,所以可能会进行一次性安装(使用库的构建和安装说明)。然后将Python扩展链接到已安装的外部库:

    ext_modules = [
        Extension("delaunay",
                  sources = ["delaunay/__init__.pyx"],
                  libraries = ["delaunay"])
    ]
    

    这种方法对于您意识到还需要其他语言的包装器的情况也是安全的,例如Matlab、Octave、IDL。。。

    使现代化

    在某种程度上,如果您最终想要封装多个这样的外部库,那么添加一个顶级构建系统是有利的,该系统可以安装所有这些库,并管理所有封装的构建。我有 cmake 为此,它非常适合处理系统范围内的构建和安装。然而,它不能开箱即用地构建Python,但它 可以 很容易学会在每个子目录中调用“python setup.py install” python ,从而调用distutils。因此,整个构建过程如下所示:

    mkdir build
    cd build
    cmake ..
    make
    make install
    make python
    (make octave)
    (make matlab)
    

    对于特定的前端语言(也适用于您自己的项目!),始终将核心库代码与包装器分离是非常重要的,因为它们的变化往往相当快。其他情况下发生的情况可以从 numpy :而不是编写一个通用C库 libndarray.so 以及为Python创建精简包装,有PythonC API调用 处处 在来源中。这就是现在的阻碍 Pypy 作为CPython的一个严肃的替代方案,因为为了 努皮 他们必须支持CPython API的每一个细节,这是他们无法做到的,因为他们有一个实时编译器和一个不同的垃圾收集器。这意味着我们错过了许多潜在的改进。

    底线:

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

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

        3
  •  0
  •   enigmaticPhysicist    9 年前

    您可以在外部生成对象文件 distutils 然后在链接步骤中使用 extra_objects Extension构造函数的参数。在里面 setup.py 以下为:

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

    在命令提示下:

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

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