使用 CMake 和 make 从 Fortan 创建 Python 模块不会产生任何错误,但由于未定义的符号,在 Python 中导入模块失败

问题描述 投票:0回答:1

我收到了

Python 3.12.0 | packaged by conda-forge | (main, Oct  3 2023, 08:43:22) [GCC 12.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: /home/.../myproject/build/foo.cpython-312-x86_64-linux-gnu.so: undefined symbol: f2pywrapfoo_

尝试使用

numpy.f2py
从 Fortran 代码加载使用 CMake 创建的 Python 模块时。


我正在遵循 官方指南 在 CMake 中使用

numpy.f2py
。我正在使用最新版本的 NumPy (1.26)、CMake 3.22 以及 Xubuntu 22.04 存储库中的
gfortran
gcc
编译器(分别为 11.4 和 12.3)。

我的 Fortran 函数比文档中的示例更简单(因为我对该语言非常陌生),即

function foo(a) result(b)
    implicit none
    
    real(kind=8), intent(in)    :: a(:,:)
    complex(kind=8)             :: b(size(a,1),size(a,2))
    
    b = exp((0,1)*a)
    
end function foo

我的 CMake 中处理模块生成的部分是

if(PYTHON_F2PY)
# https://numpy.org/doc/stable/f2py/buildtools/cmake.html
# https://numpy.org/doc/stable/f2py/usage.html
    message("Creating Python module from Fortran code enabled")
    # Example for interfacing with Python using f2py
    # Check if Python with the required version and components is available
    find_package(Python 3.12 REQUIRED
    COMPONENTS Interpreter Development.Module NumPy)

    # Grab the variables from a local Python installation
    # F2PY headers
    execute_process(
    COMMAND "${Python_EXECUTABLE}"
    -c "import numpy.f2py; print(numpy.f2py.get_include())"
    OUTPUT_VARIABLE F2PY_INCLUDE_DIR
    OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    #message("${F2PY_INCLUDE_DIR}")
    # Print out the discovered paths
    include(CMakePrintHelpers)
    cmake_print_variables(Python_INCLUDE_DIRS)
    cmake_print_variables(F2PY_INCLUDE_DIR)
    cmake_print_variables(Python_NumPy_INCLUDE_DIRS)

    # Common variables
    set(f2py_module_name "foo")
    set(fortran_src_file "${CMAKE_SOURCE_DIR}/src/foo.f90")
    set(f2py_module_c "${f2py_module_name}module.c")

    # Generate sources
    add_custom_target(
    genpyf
    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}"
    )
    add_custom_command(
    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}"
    COMMAND ${Python_EXECUTABLE}  -m "numpy.f2py"
                    "${fortran_src_file}"
                    -m "${f2py_module_name}"
                    --lower # Important
    DEPENDS "src/foo.f90" # Fortran source
    )

    # Set up target
    Python_add_library(foo MODULE WITH_SOABI
    "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}" # Generated
    "${F2PY_INCLUDE_DIR}/fortranobject.c" # From NumPy
    "${fortran_src_file}" # Fortran source(s) #"foo-f2pywrappers2.f90"
    )

    # Depend on sources
    target_link_libraries(foo PRIVATE Python::NumPy)
    add_dependencies(foo genpyf)
    target_include_directories(foo PRIVATE "${F2PY_INCLUDE_DIR}")
endif(PYTHON_F2PY)

在我运行的构建目录中

cmake -Wno-dev -DPYTHON_F2PY=1 ..

生成项目,然后

make -j10

构建它。

除其他外,我还得到以下文件:

  • foo.cpython-312-x86_64-linux-gnu.so
    - 我可以在Python中加载的共享库

  • foo-f2pywrappers.f
    - 空的 Fortran 文件

  • foo-f2pywrappers2.f90
    - 包含一些包装器代码的 Fortran 文件

    !     -*- f90 -*-
    !     This file is autogenerated with f2py (version:1.26.4)
    !     It contains Fortran 90 wrappers to fortran functions.
    subroutine f2pywrapfoo (foof2pywrap, a, f2py_a_d0, f2py_a_d1)
     integer f2py_a_d0
     integer f2py_a_d1
     real(kind=8) a(f2py_a_d0,f2py_a_d1)
     complex(kind=8) foof2pywrap(size(a, 1),size(a, 2))
     interface
       function foo(a) result (b) 
         real(kind=8), intent(in),dimension(:,:) :: a
         complex(kind=8), dimension(size(a,1),size(a,2)) :: b
       end function foo
     end interface
     foof2pywrap = foo(a)
    end
    
  • foomodule.c
    - 为我的模块生成的 C 代码将用于构建共享库

错误信息

Python 3.12.0 | packaged by conda-forge | (main, Oct  3 2023, 08:43:22) [GCC 12.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: /home/.../myproject/build/foo.cpython-312-x86_64-linux-gnu.so: undefined symbol: f2pywrapfoo_

点为

f2pywrapfoo_
。正如您在上面看到的,
foo-f2pywrappers2.f90
文件包含该函数(尽管没有
_
后缀)。

我所做的就是将该包装器添加到 Fortran 源文件列表中

# Set up target
Python_add_library(foo MODULE WITH_SOABI
"${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}" # Generated
"${F2PY_INCLUDE_DIR}/fortranobject.c" # From NumPy
"${fortran_src_file}" "foo-f2pywrappers2.f90" # Fortran source(s)
)

我再次运行 CMake 和

make
。当我重复导入模块的步骤时,现在它可以工作了:

Python 3.12.0 | packaged by conda-forge | (main, Oct  3 2023, 08:43:22) [GCC 12.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from foo import foo
>>> import numpy as np
>>> a = np.array([[1,2,3,4], [5,6,7,8]], order='F')
>>> foo(a)
array([[ 0.54030231+0.84147098j, -0.41614684+0.90929743j,
        -0.9899925 +0.14112001j, -0.65364362-0.7568025j ],
       [ 0.28366219-0.95892427j,  0.96017029-0.2794155j ,
         0.75390225+0.6569866j , -0.14550003+0.98935825j]])

问题在于,在首次运行 CMake 和

foo-f2pywrappers2.f90
期间,
make
不可用。它仅在执行
make
后创建。所以我真的无法将其添加为
CMakeLists.txt
中库构建阶段的依赖项。

有什么想法需要改变才能使这项工作成功吗?

python c cmake fortran f2py
1个回答
0
投票

您可以通过简单的

Makefile
来做到这一点:

f2py_module_name = foo

all: foo.pyf foo.cpython-311-x86_64-linux-gnu.so

clean:
    rm -f *.pyf *.so *wrappers* *module.c

foo.pyf: src/foo.f90
    f2py -m $(f2py_module_name) $< -h $@ --overwrite-signature

foo.cpython-311-x86_64-linux-gnu.so: src/foo.f90
    f2py -m $(f2py_module_name) -c $<

foo-f2pywrappers.f foomodule.c foo-f2pywrappers2.f90: src/foo.f90
    f2py -m $(f2py_module_name) $< --lower

test:
    python3 test.py

或者,

CMakeLists.txt
可能看起来像:

cmake_minimum_required(VERSION 3.12)
project(foo_f2py)

find_package(Python 3.12 REQUIRED COMPONENTS Interpreter Development.Module NumPy)

set(f2py_module_name foo)

add_custom_target(generate_foo_pyf
    COMMAND f2py -m ${f2py_module_name} src/foo.f90 -h foo.pyf --overwrite-signature
    COMMENT "Generate foo.pyf."
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    VERBATIM
)

add_custom_target(generate_foo_so
    COMMAND f2py -m ${f2py_module_name} -c src/foo.f90
    COMMENT "Generate foo.cpython-311-x86_64-linux-gnu.so."
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    VERBATIM
)

add_custom_target(clean_files
    COMMAND rm -f *.pyf *.so *wrappers* *module.c
    COMMENT "Clean files."
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    VERBATIM
)

add_custom_target(run_tests
    COMMAND python3 test.py
    COMMENT "Run test."
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    VERBATIM
)

add_dependencies(generate_foo_so generate_foo_pyf)

# Set default target.
add_custom_target(default_target ALL DEPENDS generate_foo_so)

构建:

mkdir build
cd build
cmake ..
make
# Return to directory containing built package.
cd ..

为了使用您的 Python 版本(3.12.0)评估这种方法,我制作了一个小 Docker 镜像。

🗎

Dockerfile

FROM python:3.12.0

RUN apt-get update -qq && \
    apt-get install -y -qq cmake gfortran

WORKDIR /app

COPY requirements.txt .

RUN pip3 install -r requirements.txt

COPY . .

RUN rm -rf build && \
    mkdir build && \
    cd build && \
    cmake .. && \
    # make
    true

🗎

requirements.txt

numpy==1.26.4
meson==1.3.2
ninja==1.11.1.1

© www.soinside.com 2019 - 2024. All rights reserved.