9.6 使用Python CFFI混合C,C++,Fortran和Python

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-9/recipe-06 中找到,其中有一个C++示例和一个Fortran示例。该示例在CMake 3.11版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

前面的三个示例中,我们使用Cython、Boost.Python和pybind11作为连接Python和C++的工具。之前的示例中,主要连接的是C++接口。然而,可能会遇到这样的情况:将Python与Fortran或其他语言进行接口。

本示例中,我们将使用Python C的外部函数接口(CFFI,参见https://cffi.readthedocs.io)。由于C是通用语言,大多数编程语言(包括Fortran)都能够与C接口进行通信,所以Python CFFI是将Python与大量语言结合在一起的工具。Python CFFI的特性是,生成简单且非侵入性的C接口,这意味着它既不限制语言特性中的Python层,也不会对C层以下的代码有任何限制。

本示例中,将使用前面示例的银行帐户示例,通过C接口将Python CFFI应用于Python和C++。我们的目标是实现一个上下文感知的接口。接口中,我们可以实例化几个银行帐户,每个帐户都带有其内部状态。我们将通过讨论如何使用Python CFFI来连接Python和Fortran来结束本教程。

第11章第3节中,通过PyPI分发一个用CMake/CFFI构建的C/Fortran/Python项目,届时我们将重新讨论这个例子,并展示如何打包它,使它可以用pip安装。

准备工作

我们从C++实现和接口开始,把它们放在名为account/implementation的子目录中。实现文件(cpp_implementation.cpp)类似于之前的示例,但是包含有断言,因为我们将对象的状态保持在一个不透明的句柄中,所以必须确保对象在访问时已经创建:

#include "cpp_implementation.hpp"

#include <cassert>

Account::Account()
{
  balance = 0.0;
  is_initialized = true;
}
Account::~Account()
{
  assert(is_initialized);
  is_initialized = false;
}
void Account::deposit(const double amount)
{
  assert(is_initialized);
  balance += amount;
}
void Account::withdraw(const double amount)
{
  assert(is_initialized);
  balance -= amount;
}
double Account::get_balance() const
{
  assert(is_initialized);
  return balance;
}

接口文件(cpp_implementation.hpp)包含如下内容:

此外,我们隔离了C-C++接口(c_cpp_interface.cpp)。这将是我们与Python CFFI连接的接口:

account目录下,我们声明了C接口(account.h):

我们还描述了Python接口,将在稍后对此进行讨论(__init_ _.py):

我们看到,这个接口的大部分工作是通用的和可重用的,实际的接口相当薄。

项目的布局为:

具体实施

现在使用CMake来组合这些文件,形成一个Python模块:

  1. CMakeLists.txt文件包含一个头文件。此外,根据GNU标准,设置编译库的位置:

  2. 第二步,是在account子目录下包含接口和实现的定义:

  3. CMakeLists.txt文件以测试定义(需要Python解释器)结束:

  4. account/CMakeLists.txt中定义了动态库目标:

  5. 导出一个可移植的头文件:

  6. 使用Python-C接口进行对接:

工作原理

虽然,之前的示例要求我们显式地声明Python-C接口,并将Python名称映射到C(++)符号,但Python CFFI从C头文件(示例中是account.h)推断出这种映射。我们只需要向Python CFFI层提供描述C接口的头文件和包含符号的动态库。在主CMakeLists.txt文件中使用了环境变量集来实现这一点,这些环境变量可以在__init__.py中找到:

get_lib_handle函数打开头文件(使用ffi.cdef)并解析加载库(使用ffi.dlopen)。并返回库对象。前面的文件是通用的,可以在不进行修改的情况下重用,用于与Python和C或使用Python CFFI的其他语言进行接口的其他项目。

_lib库对象可以直接导出,这里有一个额外的步骤,使Python接口在使用时,感觉更像Python:

有了这个变化,可以将例子写成下面的方式:

另一种选择则不那么直观:

需要注意的是,如何使用API来实例化和跟踪上下文:

为了导入account的Python模块,需要提供ACCOUNT_HEADER_FILEACCOUNT_LIBRARY_FILE环境变量,就像测试中那样:

第11章中,将讨论如何创建一个可以用pip安装的Python包,其中头文件和库文件将安装在定义良好的位置,这样就不必定义任何使用Python模块的环境变量。

讨论了Python方面的接口之后,现在看下C的接口。account.h内容为:

黑盒句柄account_context会保存对象的状态。ACCOUNT_API定义在account_export.h中,由account/interface/CMakeLists.txt生成:

account_export.h头文件定义了接口函数的可见性,并确保这是以一种可移植的方式完成的,实现可以在cpp_implementation.cpp中找到。它包含is_initialized布尔变量,可以检查这个布尔值确保API函数按照预期的顺序调用:上下文在创建之前或释放之后都不应该被访问。

更多信息

设计Python-C接口时,必须仔细考虑在哪一端分配数组:数组可以在Python端分配并传递给C(++)实现,也可以在返回指针的C(++)实现上分配。后一种方法适用于缓冲区大小事先未知的情况。但返回到分配给C(++)端的数组指针可能会有问题,因为这可能导致Python垃圾收集导致内存泄漏,而Python垃圾收集不会“查看”分配给它的数组。我们建议设计C API,使数组可以在外部分配并传递给C实现。然后,可以在__init__.py中分配这些数组,如下例所示:

return_array函数返回一个Python列表。因为在Python端完成了所有的分配工作,所以不必担心内存泄漏,可以将清理工作留给垃圾收集。

对于Fortran示例,读者可以参考以下Git库:https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter09/recipe06/Fortran-example 。与C++实现的主要区别在于,account库是由Fortran 90源文件编译而成的,我们在account/CMakeLists.txt中使用了Fortran 90源文件:

上下文保存在用户定义的类型中:

Fortran实现可以使用iso_c_binding模块解析account.h中定义的符号和方法:

这个示例和解决方案的灵感来自Armin Ronacher的帖子“Beautiful Native Libraries”: http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

Last updated

Was this helpful?