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
考虑这个 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