python 通过 ctypes 在 Fortran 中回调

python callback in Fortran via ctypes

考虑这个 C 互操作的 Fortran 子例程,它从 Python 调用并采用 Python 回调函数作为输入参数,然后调用它,

module FortranFunc_mod

    ! C-interoperable interface for the python callback
    abstract interface
        function getSquare_proc( x ) result(xSquared) bind(C)
            use, intrinsic :: iso_c_binding, only: c_double
            real(c_double), intent(in)              :: x
            real(c_double)                          :: xSquared
        end function getSquare_proc
    end interface

contains

    subroutine fortranFunc( getSquareFromPython ) bind(C, name="fortranFunc")
        !DEC$ ATTRIBUTES DLLEXPORT :: fortranFunc
        use, intrinsic :: iso_c_binding, only: c_funptr, c_f_procpointer, c_double
        implicit none
        type(c_funptr), intent(in)          :: getSquareFromPython
        procedure(getSquare_proc), pointer  :: getSquare
        real(c_double)                      :: x = 2._c_double, xSquared

        ! associate the input C procedure pointer to a Fortran procedure pointer
        call c_f_procpointer(cptr=getSquareFromPython, fptr=getSquare)
        xSquared = getSquare(x)
        write(*,*) "xSquared = ", xSquared
    end subroutine fortranFunc

end module FortranFunc_mod

Python 函数可能如下所示,

import numpy as np
import ctypes as ct

# import dll and define result type
ff = ct.CDLL('FortranFunc_mod')
ff.fortranFunc.restype = None

# define and decorate Python callback with propoer ctypes
@ct.CFUNCTYPE( ct.c_double, ct.c_double ) # result type, argument type
def getSquareFromPython(x): return np.double(x**2)

# call Fortran function
ff.fortranFunc( getSquareFromPython )

但是,使用 ifort 编译此代码(成功完成)然后 运行 Python 代码导致以下错误,

        ---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-3-2b798bfb58b5> in <module>
     11
     12 # call Fortran function
---> 13 ff.fortranFunc( getSquareFromPython )

OSError: exception: access violation reading 0xFFFFFFFFFFFFFFFF

我在这个简单的例子中遗漏了什么? Fortran 和 python 代码之间是否需要额外的 C 包装器来定义回调原型?如果您还可以提供 C 等效代码来调用 Python 函数,那也会有所帮助。

你的例子的主要问题是ff.fortranFunc只指定了它的return类型,而不是它的参数 类型。 Fortran 子例程 fortranFunc 有一个输入参数 type(c_funptr),这也应该反映在 Python 一侧。

具体如何实施解决方案,取决于您是只想在 Python 中进行更改,还是也愿意在 Fortran 源代码中进行更改。我将概述这两种解决方案:

仅在 Python

中进行更改

下面是您的 Python 测试例程的更新版本(我称之为 test.py),具有以下具体更改:

  • 指定 ff.fortranFunc.argtypes
  • arg_type 被指定为指向 c_double 的指针 - 如何在 C
  • 中传递标量参数
  • 回调函数 getSquareFromPython 也被修改以反映此 x[0]

(关于最后两点的详细信息,请参阅ctypes documentation - 2.7 版本可能解释得更清楚)

import ctypes as ct

# callback function ctypes specification
return_type = ct.c_double
arg_type = ct.POINTER(ct.c_double)
func_spec = ct.CFUNCTYPE(return_type, arg_type)

# import dll and define result AND argument type
ff = ct.CDLL('FortranFunc_mod')
ff.fortranFunc.restype = None
ff.fortranFunc.argtypes = [ct.POINTER(func_spec),]

# decorate Python callback
@func_spec
def getSquareFromPython(x):
    return x[0]**2

# call Fortran function
ff.fortranFunc( getSquareFromPython )

在 Python 和 Fortran

中进行更改

如果您希望更接近最初的 Python 实现,也可以通过对 test.py 进行以下更改来使其工作:

  • 更改fortranFunc的参数类型:arg_type = ct.c_double
  • 更改 getSquareFromPython 的 return 值:x**2

但是,由于回调函数现在需要 c_double 作为输入参数(而不是指向参数的指针),因此您必须更改 Fortran 抽象接口以反映这一点,方法是添加 value 属性到虚拟参数 x:

abstract interface
    function getSquare_proc( x ) result(xSquared) bind(C)
        use, intrinsic :: iso_c_binding, only: c_double
        real(c_double), intent(in), value       :: x
        real(c_double)                          :: xSquared
    end function getSquare_proc
end interface

编译并运行

编译和 运行ning 代码的任何一个修改版本,在 Windows 上使用 ifort 给我以下结果(在编译命令和库名称中适当更改它也适用Linux 和 OS X 上的 gfortran:

> ifort /DLL FortranFunc_mod.f90 /o FortranFunc_mod.dll
...
> python test.py
 xSquared =    4.00000000000000

注意两种情况的区别

通过查看 getSquareFromPython 的参数 x 的动态类型可以清楚地反映出两种实现之间的差异(它还解释了两种替代方案所需的符号更改)。对于第一个替代方案,您可以将左侧的语句添加到 getSquareFromPython,以获得右侧显示的结果:

print(type(x).__name__)                 :  LP_c_double
print(type(x.contents).__name__)        :  c_double
print(type(x.contents.value).__name__)  :  float
print(type(x[0]).__name__)              :  float
print(x.contents.value == x[0])         :  True

而对于第二种选择:

print(type(x).__name__)                 :  float