写点什么

Python 绑定:从 Python 调用 C 或 C++

发布于: 2021 年 07 月 08 日

摘要:您是拥有想要从 Python 中使用的 C 或 C++ 库的 Python 开发人员吗?如果是这样,那么 Python 绑定允许您调用函数并将数据从 Python 传递到 C 或 C++,让您利用这两种语言的优势。


本文分享自华为云社区《Python 绑定:从 Python 调用 C 或 C++ |【生长吧!Python!】》,原文作者:Yuchuan 。


您是拥有想要从 Python 中使用的 C 或 C++ 库的 Python 开发人员吗?如果是这样,那么 Python 绑定允许您调用函数并将数据从 Python 传递到 C 或 C++,让您利用这两种语言的优势。在本教程中,您将看到一些可用于创建 Python 绑定的工具的概述。


在本教程中,您将了解:

  • 为什么要从 Python 调用 C 或 C++

  • 如何在 C 和 Python 之间传递数据

  • 哪些工具和方法可以帮助您创建 Python 绑定


本教程面向中级 Python 开发人员。它假定读者具备 Python 的基本知识,并对 C 或 C++ 中的函数和数据类型有所了解。您可以通过单击下面的链接获取本教程中将看到的所有示例代码:

Python Bindings 概述


在深入研究如何从 Python 调用 C 之前,最好花一些时间了解为什么. 有几种情况下,创建 Python 绑定来调用 C 库是一个好主意:


  1. 您已经拥有一个用 C++ 编写的大型、经过测试的稳定库,您想在 Python 中利用它。这可能是一个通信库或一个与特定硬件对话的库。它做什么并不重要。

  2. 您希望通过将关键部分转换为 C 来加速 Python 代码的特定部分。 C 不仅具有更快的执行速度,而且还允许您摆脱 GIL 的限制,前提是您小心。

  3. 您想使用 Python 测试工具对其系统进行大规模测试。


以上所有都是学习创建 Python 绑定以与 C 库交互的重要原因。


注意:在本教程中,您将创建到 C 和 C++ 的 Python 绑定。大多数通用概念适用于两种语言,因此除非两种语言之间存在特定差异,否则将使用 C。通常,每个工具都支持 C 或 C++,但不能同时支持两者。

让我们开始吧!

编组数据类型


等待!在开始编写 Python 绑定之前,先看看 Python 和 C 如何存储数据以及这会导致哪些类型的问题。首先,让我们定义编组。这个概念由维基百科定义如下:


将对象的内存表示转换为适合存储或传输的数据格式的过程。


出于您的目的,编组是 Python 绑定在准备数据以将其从 Python 移动到 C 或反之亦然时所做的工作。Python 绑定需要进行编组,因为 Python 和 C 以不同的方式存储数据。C 在内存中以最紧凑的形式存储数据。如果您使用 uint8_t,那么它总共将只使用 8 位内存。


另一方面,在 Python 中,一切都是对象。这意味着每个整数在内存中使用几个字节。多少取决于您运行的 Python 版本、操作系统和其他因素。这意味着您的 Python 绑定将需要为每个跨边界传递的整数C 整数转换为 Python 整数


其他数据类型在这两种语言之间具有相似的关系。让我们依次来看看:


  • 整数存储计数数字。Python 以任意精度存储整数,这意味着您可以存储非常非常大的数字。C 指定整数的确切大小。在语言之间移动时需要注意数据大小,以防止 Python 整数值溢出 C 整数变量。

  • 浮点数是带有小数位的数字。Python 可以存储比 C 大得多(和小得多)的浮点数。这意味着您还必须注意这些值以确保它们保持在范围内。

  • 复数是带有虚部的数字。虽然 Python 具有内置复数,而 C 具有复数,但没有用于在它们之间编组的内置方法。要封送复数,您需要在 C 代码中构建 struct 或 class 来管理它们。

  • 字符串是字符序列。作为这样一种常见的数据类型,当您创建 Python 绑定时,字符串将被证明是相当棘手的。与其他数据类型一样,Python 和 C 以完全不同的格式存储字符串。(与其他数据类型不同,这也是 C 和 C++ 不同的领域,这增加了乐趣!)您将研究的每个解决方案都有略微不同的处理字符串的方法。

  • 布尔变量只能有两个值。由于它们在 C 中得到支持,因此将它们编组将被证明是相当简单的。

除了数据类型转换之外,在构建 Python 绑定时还需要考虑其他问题。让我们继续探索它们。

了解可变和不可变值


除了所有这些数据类型之外,您还必须了解 Python 对象如何可变不可变。当谈到传值传引用时,C 有一个类似的函数参数概念。在 C 中,所有参数都是按值传递的。如果要允许函数更改调用方中的变量,则需要传递指向该变量的指针。


您可能想知道是否可以通过使用指针将不可变对象简单地传递给 C 来绕过不可变限制。除非你走到丑陋和不可移植的极端,否则 Python 不会给你一个指向 object 的指针,所以这行不通。如果您想用 C 修改 Python 对象,那么您需要采取额外的步骤来实现这一点。这些步骤将取决于您使用的工具,如下所示。

因此,您可以将不变性添加到您创建 Python 绑定时要考虑的项目清单中。在创建此清单的宏伟之旅中,您的最后一站是如何处理 Python 和 C 处理内存管理的不同方式。

管理内存


C 和 Python 管理内存的方式不同。在 C 中,开发人员必须管理所有内存分配并确保它们被释放一次且仅一次。Python 使用垃圾收集器为您处理这个问题。


虽然这些方法中的每一种都有其优点,但它确实为创建 Python 绑定添加了额外的麻烦。您需要知道每个对象的内存分配在哪里,并确保它只在语言障碍的同一侧被释放。


例如,当您设置 x = 3. 用于此的内存在 Python 端分配,需要进行垃圾收集。幸运的是,使用 Python 对象,很难做任何其他事情。看看 C 中的逆向,直接分配一块内存:

int* iPtr = (int*)malloc(sizeof(int));
复制代码

执行此操作时,您需要确保在 C 中释放此指针。这可能意味着手动将代码添加到 Python 绑定中以执行此操作。


这完善了您的一般主题清单。让我们开始设置您的系统,以便您可以编写一些代码!

设置您的环境

在本教程中,您将使用来自 Real Python GitHub 存储库的预先存在的 C 和 C++ 库来展示每个工具的测试。目的是您将能够将这些想法用于任何 C 库。要遵循此处的所有示例,您需要具备以下条件:


  • 安装的 C++ 库和命令行调用路径的知识

  • Python 开发工具:对于 Linux,这是 python3-dev 或 python3-devel 包,具体取决于您的发行版。对于 Windows,有多个选项。

  • Python 3.6 或更高版本

  • 一个虚拟环境(建议,但不要求)

  • invoke 工具


最后一个对你来说可能是新的,所以让我们仔细看看它。

使用 invoke 工具


invoke 是您将在本教程中用于构建和测试 Python 绑定的工具。它具有类似的目的,make 但使用 Python 而不是 Makefiles。您需要 invoke 使用 pip 以下命令在虚拟环境中安装:

$ python3 -m pip install invoke
复制代码

要运行它,请键入 invoke 后跟要执行的任务:

$ invoke build-cmult=================================================== Building C Library* Complete
复制代码

要查看哪些任务可用,请使用以下--list 选项:

$ invoke --listAvailable tasks:
all Build and run all tests build-cffi Build the CFFI Python bindings build-cmult Build the shared library for the sample C code build-cppmult Build the shared library for the sample C++ code build-cython Build the cython extension module build-pybind11 Build the pybind11 wrapper library clean Remove any built objects test-cffi Run the script to test CFFI test-ctypes Run the script to test ctypes test-cython Run the script to test Cython test-pybind11 Run the script to test PyBind11
复制代码

请注意,当您查看定义任务的 tasks.py 文件时 invoke,您会看到列出的第二个任务的名称是 build_cffi. 但是,来自的输出将其--list 显示为 build-cffi. 减号 ( -) 不能用作 Python 名称的一部分,因此该文件使用下划线 ( _) 代替。


对于您将检查的每个工具,都会定义一个 build-和一个 test-任务。例如,要运行 的代码 CFFI,您可以键入 invoke build-cffi test-cffi。一个例外是 ctypes,因为 没有构建阶段 ctypes。此外,为了方便,还添加了两个特殊任务:


  • invoke all 运行所有工具的构建和测试任务。

  • invoke clean 删除任何生成的文件。


既然您已经对如何运行代码有所了解,那么在查看工具概述之前,让我们先看一下您将要包装的 C 代码。

C 或 C++ 源代码


在下面的每个示例部分中,您将为 C 或 C++ 中的相同函数创建 Python 绑定。这些部分旨在让您体验每种方法的外观,而不是有关该工具的深入教程,因此您将封装的函数很小。您将为其创建 Python 绑定的函数将 anint 和 afloat 作为输入参数并返回一个 float 是两个数字的乘积:

// cmult.cfloat cmult(int int_param, float float_param) {    float return_value = int_param * float_param;    printf("    In cmult : int: %d float %.1f returning  %.1f\n", int_param,            float_param, return_value);    return return_value;}
复制代码

C 和 C++ 函数几乎相同,它们之间的名称和字符串略有不同。您可以通过单击以下链接获取所有代码的副本:


现在您已经克隆了 repo 并安装了工具,您可以构建和测试这些工具。因此,让我们深入了解下面的每个部分!

ctypes


您将从 开始 ctypes,它是标准库中用于创建 Python 绑定的工具。它提供了一个低级工具集,用于在 Python 和 C 之间加载共享库和编组数据。

它是如何安装的

的一大优点 ctypes 是它是 Python 标准库的一部分。它是在 Python 2.5 版中添加的,因此您很可能已经拥有它。您可以 import 像使用 sys 或 time 模块一样。

调用函数

加载 C 库和调用函数的所有代码都将在 Python 程序中。这很棒,因为您的过程中没有额外的步骤。您只需运行您的程序,一切都会得到处理。要在 中创建 Python 绑定 ctypes,您需要执行以下步骤:


  1. 加载您的库。

  2. 包装一些输入参数。

  3. 告诉 ctypes 你函数的返回类型。


您将依次查看其中的每一个。


库加载

ctypes 为您提供了多种加载共享库的方法,其中一些是特定于平台的。对于您的示例,您将 ctypes.CDLL 通过传入所需共享库的完整路径来直接创建对象:

# ctypes_test.pyimport ctypesimport pathlib
if __name__ == "__main__": # Load the shared library into ctypes libname = pathlib.Path().absolute() / "libcmult.so" c_lib = ctypes.CDLL(libname)
复制代码

这适用于共享库与 Python 脚本位于同一目录中的情况,但在尝试加载来自 Python 绑定以外的包的库时要小心。在 ctypes 特定于平台和特定情况的文档中,有许多关于加载库和查找路径的详细信息。


注意:在库加载过程中可能会出现许多特定于平台的问题。最好在示例工作后进行增量更改。


现在您已将库加载到 Python 中,您可以尝试调用它!

调用你的函数

请记住,您的 C 函数的函数原型如下:

// cmult.hfloat cmult(int int_param, float float_param);
复制代码

您需要传入一个整数和一个浮点数,并且可以期望得到一个浮点数返回。整数和浮点数在 Python 和 C 中都有本机支持,因此您希望这种情况适用于合理的值。


将库加载到 Python 绑定中后,该函数将成为 的属性 c_lib,即 CDLL 您之前创建的对象。您可以尝试这样称呼它:

x, y = 6, 2.3answer = c_lib.cmult(x, y)
复制代码

哎呀!这不起作用。此行在示例 repo 中被注释掉,因为它失败了。如果您尝试使用该调用运行,那么 Python 会报错:

$ invoke test-ctypesTraceback (most recent call last):  File "ctypes_test.py", line 16, in <module>    answer = c_lib.cmult(x, y)ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
复制代码

看起来您需要说明 ctypes 任何不是整数的参数。ctypes 除非您明确告诉它,否则您对该函数一无所知。任何未以其他方式标记的参数都假定为整数。ctypes 不知道如何将 2.3 存储的值转换为 y 整数,所以它失败了。


要解决此问题,您需要 c_float 从号码中创建一个。您可以在调用函数的行中执行此操作:

# ctypes_test.pyanswer = c_lib.cmult(x, ctypes.c_float(y))print(f"    In Python: int: {x} float {y:.1f} return val {answer:.1f}")
复制代码

现在,当您运行此代码时,它会返回您传入的两个数字的乘积:

$ invoke test-ctypes    In cmult : int: 6 float 2.3 returning  13.8    In Python: int: 6 float 2.3 return val 48.0
复制代码

等一下……6 乘以 2.3 不是 48.0!


事实证明,就像输入参数一样,ctypes 假设您的函数返回一个 int. 实际上,您的函数返回 a float,它被错误地编组。就像输入参数一样,您需要告诉 ctypes 使用不同的类型。这里的语法略有不同:

# ctypes_test.pyc_lib.cmult.restype = ctypes.c_floatanswer = c_lib.cmult(x, ctypes.c_float(y))print(f"    In Python: int: {x} float {y:.1f} return val {answer:.1f}")
复制代码

这应该够了吧。让我们运行整个 test-ctypes 目标,看看你有什么。请记住,输出的第一部分是restype 将函数固定为浮点数之前:

$ invoke test-ctypes=================================================== Building C Library* Complete=================================================== Testing ctypes Module    In cmult : int: 6 float 2.3 returning  13.8    In Python: int: 6 float 2.3 return val 48.0
In cmult : int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 13.8
复制代码

这样更好!虽然第一个未更正的版本返回错误的值,但您的固定版本与 C 函数一致。C 和 Python 都得到相同的结果!现在它可以工作了,看看为什么您可能想或不想使用 ctypes.


长处和短处

ctypes 与您将在此处检查的其他工具相比,最大的优势在于它内置于标准库中。它还不需要额外的步骤,因为所有工作都是作为 Python 程序的一部分完成的。


此外,所使用的概念是低级的,这使得像您刚刚做的那样的练习易于管理。然而,由于缺乏自动化,更复杂的任务变得繁琐。在下一部分中,您将看到一个工具,该工具为流程添加了一些自动化。

CFFI

CFFI 是 Python 的 C 外来函数接口。生成 Python 绑定需要更自动化的方法。CFFI 有多种方式可以构建和使用 Python 绑定。有两种不同的选项可供选择,为您提供四种可能的模式:


  • ABI vs API: API 模式使用 C 编译器生成完整的 Python 模块,而 ABI 模式加载共享库并直接与其交互。在不运行编译器的情况下,获取正确的结构和参数很容易出错。该文档强烈建议使用 API 模式。

  • 内联 vs 外联:这两种模式的区别在于速度和便利性之间的权衡:每次运行脚本时,内联模式都会编译 Python 绑定。这很方便,因为您不需要额外的构建步骤。但是,它确实会减慢您的程序速度。Out-of-line 模式需要一个额外的步骤来一次性生成 Python 绑定,然后在每次程序运行时使用它们。这要快得多,但这对您的应用程序可能无关紧要。


对于此示例,您将使用 API 外联模式,它生成最快的代码,并且通常看起来类似于您将在本教程后面创建的其他 Python 绑定。


它是如何安装的

由于 CFFI 不是标准库的一部分,您需要在您的机器上安装它。建议您为此创建一个虚拟环境。幸运的是,CFFI 安装有 pip:

$ python3 -m pip install cffi
复制代码

这会将软件包安装到您的虚拟环境中。如果您已经从 安装 requirements.txt,那么应该注意这一点。您可以 requirements.txt 通过访问以下链接中的 repo 来查看:


获取示例代码: 单击此处获取您将用于在本教程中了解 Python 绑定的示例代码。


现在你已经 CFFI 安装好了,是时候试一试了!

调用函数

与 不同的是 ctypes,CFFI 您正在创建一个完整的 Python 模块。您将能够 import 像标准库中的任何其他模块一样使用该模块。您需要做一些额外的工作来构建 Python 模块。要使用 CFFIPython 绑定,您需要执行以下步骤:


  • 编写一些描述绑定的 Python 代码。

  • 运行该代码以生成可加载模块。

  • 修改调用代码以导入和使用新创建的模块。


这可能看起来需要做很多工作,但您将完成这些步骤中的每一个,并了解它是如何工作的。


编写绑定

CFFI 提供读取 C 头文件的方法,以在生成 Python 绑定时完成大部分工作。在 的文档中 CFFI,执行此操作的代码放置在单独的 Python 文件中。对于此示例,您将直接将该代码放入构建工具中 invoke,该工具使用 Python 文件作为输入。要使用 CFFI,您首先要创建一个 cffi.FFI 对象,该对象提供了您需要的三种方法:

# tasks.pyimport cffi...""" Build the CFFI Python bindings """print_banner("Building CFFI Module")ffi = cffi.FFI()
复制代码

拥有 FFI 后,您将使用.cdef()来自动处理头文件的内容。这会为您创建包装函数以从 Python 封送数据:

# tasks.pythis_dir = pathlib.Path().absolute()h_file_name = this_dir / "cmult.h"with open(h_file_name) as h_file:    ffi.cdef(h_file.read())
复制代码

读取和处理头文件是第一步。之后,您需要使用.set_source()来描述 CFFI 将生成的源文件:

# tasks.pyffi.set_source(    "cffi_example",    # Since you're calling a fully-built library directly, no custom source    # is necessary. You need to include the .h files, though, because behind    # the scenes cffi generates a .c file that contains a Python-friendly    # wrapper around each of the functions.    '#include "cmult.h"',    # The important thing is to include the pre-built lib in the list of    # libraries you're linking against:    libraries=["cmult"],    library_dirs=[this_dir.as_posix()],    extra_link_args=["-Wl,-rpath,."],)
复制代码

以下是您传入的参数的细分:


  • "cffi_example"是将在您的文件系统上创建的源文件的基本名称。CFFI 将生成一个.c 文件,将其编译为一个.o 文件,并将其链接到一个.<system-description>.so 或.<system-description>.dll 文件。

  • '#include "cmult.h"'是自定义 C 源代码,它将在编译之前包含在生成的源代码中。在这里,您只需包含.h 要为其生成绑定的文件,但这可用于一些有趣的自定义。

  • libraries=["cmult"]告诉链接器您预先存在的 C 库的名称。这是一个列表,因此您可以根据需要指定多个库。

  • library_dirs=[this_dir.as_posix(),] 是一个目录列表,告诉链接器在何处查找上述库列表。

  • extra_link_args=['-Wl,-rpath,.']是一组生成共享对象的选项,它将在当前路径 ( .) 中查找它需要加载的其他库。

构建 Python 绑定


调用.set_source()不会构建 Python 绑定。它只设置元数据来描述将生成的内容。要构建 Python 绑定,您需要调用.compile():

# tasks.pyffi.compile()
复制代码

这通过生成.c 文件、.o 文件和共享库来完成。在 invoke 你刚走通过任务可以在上运行命令行构建 Python 绑定:

$ invoke build-cffi=================================================== Building C Library* Complete=================================================== Building CFFI Module* Complete
复制代码

你有你的 CFFIPython 绑定,所以是时候运行这段代码了!

调用你的函数

在您为配置和运行 CFFI 编译器所做的所有工作之后,使用生成的 Python 绑定看起来就像使用任何其他 Python 模块一样:

# cffi_test.pyimport cffi_example
if __name__ == "__main__": # Sample data for your call x, y = 6, 2.3
answer = cffi_example.lib.cmult(x, y) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
复制代码

你导入新模块,然后就可以 cmult()直接调用了。要对其进行测试,请使用以下 test-cffi 任务:

$ invoke test-cffi=================================================== Testing CFFI Module    In cmult : int: 6 float 2.3 returning  13.8    In Python: int: 6 float 2.3 return val 13.8
复制代码

这将运行您的 cffi_test.py 程序,该程序会测试您使用 CFFI. 关于编写和使用 CFFIPython 绑定的部分到此结束。

长处和短处

ctypes 与 CFFI 您刚刚看到的示例相比,这似乎需要更少的工作。虽然这对于这个用例来说是正确的,CFFI 但与 ctypes 由于大部分功能包装的自动化相比,它可以更好地扩展到更大的项目。


CFFI 也产生了完全不同的用户体验。ctypes 允许您将预先存在的 C 库直接加载到您的 Python 程序中。CFFI,另一方面,创建一个可以像其他 Python 模块一样加载的新 Python 模块。


更重要的是,使用上面使用的外部 API 方法,创建 Python 绑定的时间损失在您构建它时完成一次,并且不会在每次运行代码时发生。对于小程序来说,这可能不是什么大问题,但也可以通过 CFFI 这种方式更好地扩展到更大的项目。


就像 ctypes, usingCFFI 只允许您直接与 C 库交互。C++ 库需要大量的工作才能使用。在下一节中,您将看到一个专注于 C++ 的 Python 绑定工具。


PyBind11

PyBind11 使用完全不同的方法来创建 Python 绑定。除了将重点从 C 转移到 C++ 之外,它还使用 C++ 来指定和构建模块,使其能够利用 C++ 中的元编程工具。像 一样 CFFI,生成的 Python 绑定 PyBind11 是一个完整的 Python 模块,可以直接导入和使用。


PyBind11 以 Boost::Python 库为蓝本并具有类似的界面。但是,它将其使用限制为 C++11 和更新版本,与支持所有内容的 Boost 相比,这使其能够简化和加快处理速度。

它是如何安装的

文档的“第一步”部分将 PyBind11 引导您了解如何下载和构建 PyBind11. 虽然这似乎不是严格要求,但完成这些步骤将确保您设置了正确的 C++ 和 Python 工具。


注:大部分示例 PyBind11 使用 cmake,是构建 C 和 C++ 项目的好工具。但是,对于此演示,您将继续使用该 invoke 工具,该工具遵循文档的手动构建部分中的说明。


您需要将此工具安装到您的虚拟环境中:

$ python3 -m pip install pybind11
复制代码

PyBind11 是一个全头库,类似于 Boost 的大部分内容。这允许 pip 将库的实际 C++ 源代码直接安装到您的虚拟环境中。

调用函数

在您深入研究之前,请注意您使用的是不同的 C++ 源文件, cppmult.cpp,而不是您用于前面示例的 C 文件。两种语言的功能基本相同。

编写绑定

与 类似 CFFI,您需要创建一些代码来告诉该工具如何构建您的 Python 绑定。与 不同 CFFI,此代码将使用 C++ 而不是 Python。幸运的是,只需要很少的代码:

// pybind11_wrapper.cpp#include <pybind11/pybind11.h>#include <cppmult.hpp>
PYBIND11_MODULE(pybind11_example, m) { m.doc() = "pybind11 example plugin"; // Optional module docstring m.def("cpp_function", &cppmult, "A function that multiplies two numbers");}
复制代码

让我们一次一个地看,因为 PyBind11 将大量信息打包成几行。


前两行包括 pybind11.hC++ 库的文件和头文件 cppmult.hpp. 之后,你就有了 PYBIND11_MODULE 宏。这将扩展为 PyBind11 源代码中详细描述的 C++ 代码块:


此宏创建入口点,当 Python 解释器导入扩展模块时将调用该入口点。模块名称作为第一个参数给出,不应用引号引起来。第二个宏参数定义了一个 py::module 可用于初始化模块的类型变量。(来源


这对您来说意味着,在本例中,您正在创建一个名为的模块 pybind11_example,其余代码将 m 用作 py::module 对象的名称。在下一行,在您定义的 C++ 函数中,您为模块创建一个文档字符串。虽然这是可选的,但让您的模块更加 Pythonic 是一个不错的选择。


最后,你有 m.def()电话。这将定义一个由您的新 Python 绑定导出的函数,这意味着它将在 Python 中可见。在此示例中,您将传递三个参数:


  • cpp_function 是您将在 Python 中使用的函数的导出名称。如本例所示,它不需要匹配 C++ 函数的名称。

  • &cppmult 获取要导出的函数的地址。

  • "A function..." 是函数的可选文档字符串。


现在您已经有了 Python 绑定的代码,接下来看看如何将其构建到 Python 模块中。

构建 Python 绑定

用于构建 Python 绑定的工具 PyBind11 是 C++ 编译器本身。您可能需要修改编译器和操作系统的默认值。


首先,您必须构建要为其创建绑定的 C++ 库。对于这么小的示例,您可以将 cppmult 库直接构建到 Python 绑定库中。但是,对于大多数实际示例,您将有一个要包装的预先存在的库,因此您将 cppmult 单独构建该库。构建是对编译器的标准调用以构建共享库:

# tasks.pyinvoke.run(    "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC cppmult.cpp "    "-o libcppmult.so ")
复制代码

运行这个 invoke build-cppmult 产生 libcppmult.so:

$ invoke build-cppmult=================================================== Building C++ Library* Complete
复制代码

另一方面,Python 绑定的构建需要一些特殊的细节:

1# tasks.py 2invoke.run( 3    "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC " 4    "`python3 -m pybind11 --includes` " 5    "-I /usr/include/python3.7 -I .  " 6    "{0} " 7    "-o {1}`python3.7-config --extension-suffix` " 8    "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name) 9)
复制代码

让我们逐行浏览一下。第 3 行包含相当标准的 C++ 编译器标志,指示几个细节,包括您希望捕获所有警告并将其视为错误、您需要共享库以及您使用的是 C++11。


第 4 行是魔法的第一步。它调用 pybind11 模块使其 include 为 PyBind11. 您可以直接在控制台上运行此命令以查看它的作用:

$ python3 -m pybind11 --includes-I/home/jima/.virtualenvs/realpython/include/python3.7m-I/home/jima/.virtualenvs/realpython/include/site/python3.7
复制代码

您的输出应该相似但显示不同的路径。


在编译调用的第 5 行,您可以看到您还添加了 Python dev 的路径 includes。虽然建议您不要链接 Python 库本身,但源代码需要一些代码 Python.h 才能发挥其魔力。幸运的是,它使用的代码在 Python 版本中相当稳定。


第 5 行还用于-I .将当前目录添加到 include 路径列表中。这允许 #include <cppmult.hpp>解析包装器代码中的行。


第 6 行指定源文件的名称,即 pybind11_wrapper.cpp. 然后,在第 7 行,您会看到更多的构建魔法正在发生。此行指定输出文件的名称。Python 在模块命名上有一些特别的想法,包括 Python 版本、机器架构和其他细节。Python 还提供了一个工具来帮助解决这个问题 python3.7-config:

$ python3.7-config --extension-suffix.cpython-37m-x86_64-linux-gnu.so
复制代码

如果您使用的是不同版本的 Python,则可能需要修改该命令。如果您使用不同版本的 Python 或在不同的操作系统上,您的结果可能会发生变化。


构建命令的最后一行,第 8 行,将链接器指向 libcppmult 您之前构建的库。该 rpath 部分告诉链接器向共享库添加信息以帮助操作系统 libcppmult 在运行时查找。最后,您会注意到此字符串的格式为 cpp_name 和 extension_name。Cython 在下一节中构建 Python 绑定模块时,您将再次使用此函数。


运行此命令以构建绑定:

$ invoke build-pybind11=================================================== Building C++ Library* Complete=================================================== Building PyBind11 Module* Complete
复制代码

就是这样!您已经使用 PyBind11. 是时候测试一下了!

调用你的函数

与 CFFI 上面的示例类似,一旦您完成了创建 Python 绑定的繁重工作,调用您的函数看起来就像普通的 Python 代码:

# pybind11_test.pyimport pybind11_example
if __name__ == "__main__": # Sample data for your call x, y = 6, 2.3
answer = pybind11_example.cpp_function(x, y) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
复制代码

由于您 pybind11_example 在 PYBIND11_MODULE 宏中用作模块的名称,因此这就是您导入的名称。在 m.def()您告诉 PyBind11 将 cppmult 函数导出为 的调用中 cpp_function,这就是您用来从 Python 调用它的方法。


你也可以测试它 invoke:

$ invoke test-pybind11=================================================== Testing PyBind11 Module    In cppmul: int: 6 float 2.3 returning  13.8    In Python: int: 6 float 2.3 return val 13.8
复制代码

这就是 PyBind11 看起来的样子。接下来,您将了解何时以及为何 PyBind11 是适合该工作的工具。

长处和短处

PyBind11 专注于 C++ 而不是 C,这使得它不同于 ctypes 和 CFFI。它有几个特性使其对 C++ 库非常有吸引力:


  • 它支持

  • 它处理多态子类化

  • 它允许您从 Python 和许多其他工具向对象添加动态属性,而使用您检查过的基于 C 的工具很难做到这一点。


话虽如此,您需要进行大量设置和配置才能 PyBind11 启动和运行。正确安装和构建可能有点挑剔,但一旦完成,它似乎相当可靠。此外,PyBind11 要求您至少使用 C++11 或更高版本。对于大多数项目来说,这不太可能是一个很大的限制,但它可能是您的一个考虑因素。


最后,创建 Python 绑定需要编写的额外代码是用 C++ 编写的,而不是用 Python 编写的。这可能是也可能不是你的问题,但它是比你在这里看到的其他工具不同。在下一节中,您将继续讨论 Cython,它采用完全不同的方法来解决这个问题。

Cython

该方法 Cython 需要创建 Python 绑定使用类 Python 语言来定义绑定,然后生成的 C 或 C ++代码可被编译成模块。有几种方法可以使用 Cython. 最常见的一种是使用 setupfrom distutils。对于此示例,您将坚持使用该 invoke 工具,它允许您使用运行的确切命令。

它是如何安装的

Cython 是一个 Python 模块,可以从 PyPI 安装到您的虚拟环境中:

$ python3 -m pip install cython
复制代码

同样,如果您已将该 requirements.txt 文件安装到虚拟环境中,则该文件已经存在。您可以 requirements.txt 通过单击以下链接获取副本:


获取示例代码: 单击此处获取您将用于在本教程中了解 Python 绑定的示例代码。


这应该让你准备好与之合作 Cython!

调用函数

要使用 构建 Python 绑定 Cython,您将遵循与用于 CFFI 和 的步骤类似的步骤 PyBind11。您将编写绑定、构建它们,然后运行 ​​Python 代码来调用它们。Cython 可以同时支持 C 和 C++。对于本示例,您将使用 cppmult 您在 PyBind11 上面的示例中使用的库。

编写绑定

声明模块的最常见形式 Cython 是使用.pyx 文件:

1# cython_example.pyx 2""" Example cython interface definition """ 3 4cdef extern from "cppmult.hpp": 5    float cppmult(int int_param, float float_param) 6 7def pymult( int_param, float_param ): 8    return cppmult( int_param, float_param )
复制代码

这里有两个部分:


  1. 线 3 和 4 告诉 Cython 您使用的是 cppmult()从 cppmult.hpp。

  2. 第 6 行和第 7 行创建了一个包装函数 pymult(),以调用 cppmult()。


这里使用的语言是 C、C++ 和 Python 的特殊组合。不过,对于 Python 开发人员来说,它看起来相当熟悉,因为其目标是使过程更容易。


第一部分 withcdef extern...告诉 Cython 下面的函数声明也可以在 cppmult.hpp 文件中找到。这对于确保根据与 C++ 代码相同的声明构建 Python 绑定非常有用。第二部分看起来像一个普通的 Python 函数——因为它是!本节创建一个可以访问 C++ 函数的 Python 函数 cppmult。


现在您已经定义了 Python 绑定,是时候构建它们了!

构建 Python 绑定

构建过程 Cython 与您使用的构建过程相似 PyBind11。您首先 Cython 在.pyx 文件上运行以生成.cpp 文件。完成此操作后,您可以使用用于以下内容的相同函数对其进行编译 PyBind11:

1# tasks.py 2def compile_python_module(cpp_name, extension_name): 3    invoke.run( 4        "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC " 5        "`python3 -m pybind11 --includes` " 6        "-I /usr/include/python3.7 -I .  " 7        "{0} " 8        "-o {1}`python3.7-config --extension-suffix` " 9        "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name)10    )1112def build_cython(c):13    """ Build the cython extension module """14    print_banner("Building Cython Module")15    # Run cython on the pyx file to create a .cpp file16    invoke.run("cython --cplus -3 cython_example.pyx -o cython_wrapper.cpp")1718    # Compile and link the cython wrapper library19    compile_python_module("cython_wrapper.cpp", "cython_example")20    print("* Complete")
复制代码

您首先运行 cython 您的.pyx 文件。您可以在此命令上使用几个选项:

  • --cplus 告诉编译器生成 C++ 文件而不是 C 文件。

  • -3 切换 Cython 到生成 Python 3 语法而不是 Python 2。

  • -o cython_wrapper.cpp 指定要生成的文件的名称。


生成 C++ 文件后,您可以使用 C++ 编译器生成 Python 绑定,就像您为 PyBind11. 请注意,include 使用该 pybind11 工具生成额外路径的调用仍在该函数中。在这里不会有任何伤害,因为您的来源不需要这些。

在 中运行此任务 invoke 会产生以下输出:

$ invoke build-cython=================================================== Building C++ Library* Complete=================================================== Building Cython Module* Complete
复制代码

可以看到它构建了 cppmult 库,然后构建了 cython 模块来包装它。现在你有了 CythonPython 绑定。(试着说的是迅速...)它的时间来测试一下吧!

调用你的函数

调用新 Python 绑定的 Python 代码与用于测试其他模块的代码非常相似:

 1# cython_test.py 2import cython_example 3 4# Sample data for your call 5x, y = 6, 2.3 6 7answer = cython_example.pymult(x, y) 8print(f"    In Python: int: {x} float {y:.1f} return val {answer:.1f}")
复制代码

第 2 行导入新的 Python 绑定模块,并 pymult()在第 7 行调用。请记住,该.pyx 文件提供了一个 Python 包装器 cppmult()并将其重命名为 pymult. 使用 invoke 运行您的测试会产生以下结果:

$ invoke test-cython=================================================== Testing Cython Module    In cppmul: int: 6 float 2.3 returning  13.8    In Python: int: 6 float 2.3 return val 13.8
复制代码

你得到和以前一样的结果!

长处和短处

Cython 是一个相对复杂的工具,可以在为 C 或 C++ 创建 Python 绑定时为您提供更深层次的控制。虽然您没有在此处深入介绍它,但它提供了一种 Python 式的方法来编写手动控制 GIL 的代码,这可以显着加快某些类型的问题的处理速度。


然而,这种 Python 风格的语言并不完全是 Python,因此当您要快速确定 C 和 Python 的哪些部分适合何处时,会有一个轻微的学习曲线。

其他解决方案

在研究本教程时,我遇到了几种用于创建 Python 绑定的不同工具和选项。虽然我将此概述限制为一些更常见的选项,但我偶然发现了其他几种工具。下面的列表并不全面。如果上述工具之一不适合您的项目,这只是其他可能性的一个示例。

PyBindGen

PyBindGen 为 C 或 C++ 生成 Python 绑定并用 Python 编写。它旨在生成可读的 C 或 C++ 代码,这应该可以简化调试问题。目前尚不清楚这是否最近已更新,因为文档将 Python 3.4 列为最新的测试版本。然而,在过去的几年里,每年都有发布。

Boost.Python

Boost.Python 有一个类似于 PyBind11 您在上面看到的界面。这不是巧合,因为 PyBind11 它基于这个库!Boost.Python 是用完整的 C++ 编写的,并且在大多数平台上支持大多数(如果不是全部)C++ 版本。相比之下,PyBind11 仅限于现代 C++。

SIP

SIP 是为 PyQt 项目开发的用于生成 Python 绑定的工具集。wxPython 项目也使用它来生成它们的绑定。它有一个代码生成工具和一个额外的 Python 模块,为生成的代码提供支持功能。

Cppyy

cppyy 是一个有趣的工具,它的设计目标与您目前所见略有不同。用包作者的话来说:

“cppyy 背后的最初想法(追溯到 2001 年)是允许生活在 C++ 世界中的 Python 程序员访问那些 C++ 包,而不必直接接触 C++(或等待 C++ 开发人员过来并提供绑定) 。” (来源)

Shiboken

Shiboken 是为与 Qt 项目关联的 PySide 项目开发的用于生成 Python 绑定的工具。虽然它被设计为该项目的工具,但文档表明它既不是 Qt 也不是 PySide 特定的,可用于其他项目。

SWIG

SWIG 是与此处列出的任何其他工具不同的工具。它是一个通用工具,用于为许多其他语言(而不仅仅是 Python)创建到 C 和 C++ 程序的绑定。这种为不同语言生成绑定的能力在某些项目中非常有用。当然,就复杂性而言,它会带来成本。

结论

恭喜!您现在已经大致了解了用于创建 Python 绑定的几个不同选项。您已经了解了编组数据以及创建绑定时需要考虑的问题。您已经了解了如何使用以下工具从 Python 调用 C 或 C++ 函数:

  • ctypes

  • CFFI

  • PyBind11

  • Cython

您现在知道,虽然 ctypes 允许您直接加载 DLL 或共享库,但其他三个工具需要额外的步骤,但仍会创建完整的 Python 模块。作为奖励,您还使用了 invoke 从 Python 运行命令行任务的工具。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 07 月 08 日阅读数: 12
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
Python 绑定:从 Python 调用 C 或 C++