打包二进制扩展#
- 页面状态:
不完整
- 上次审阅:
2013-12-08
CPython 参考解释器的功能之一是,除了允许执行 Python 代码之外,它还公开了一个丰富的 C API 供其他软件使用。此 C API 最常见的用途之一是创建可导入的 C 扩展,允许在纯 Python 代码中并不总是容易实现的操作。
二进制扩展概述#
用例#
二进制扩展的典型用例仅分为三个常规类别
加速器模块:这些模块是完全自包含的,并且仅创建为比 CPython 中运行的等效纯 Python 代码运行得更快。理想情况下,加速器模块始终具有纯 Python 等效项,如果给定系统上没有加速版本,则可以使用该等效项作为后备。CPython 标准库广泛使用加速器模块。示例:导入
datetime
时,如果 C 实现( _datetimemodule.c)不可用,Python 会回退到 datetime.py 模块。包装器模块:这些模块创建为将现有 C 接口公开给 Python 代码。它们可以直接公开底层 C 接口,也可以公开更“Pythonic”的 API,利用 Python 语言功能使 API 更易于使用。CPython 标准库广泛使用包装器模块。示例: functools.py 是 _functoolsmodule.c 的 Python 模块包装器。
低级系统访问:这些模块创建为访问 CPython 运行时、操作系统或底层硬件的低级功能。通过特定于平台的代码,扩展模块可以实现纯 Python 代码中无法实现的操作。许多 CPython 标准库模块是用 C 编写的,以便访问语言级别未公开的解释器内部。示例:
sys
,它来自 sysmodule.c。C 扩展的一个特别值得注意的功能是,当它们不需要回调到解释器运行时时,它们可以在长时间运行的操作周围释放 CPython 的全局解释器锁(无论这些操作是 CPU 还是 IO 绑定的)。
并非所有扩展模块都能完全归入上述类别。例如,NumPy 附带的扩展模块涵盖了所有三个用例 - 它们出于速度原因将内部循环移至 C,包装用 C、FORTRAN 和其他语言编写的外部库,并使用 CPython 和底层操作系统的低级系统接口来支持矢量化操作的并发执行,并严格控制创建的对象的确切内存布局。
缺点#
使用二进制扩展的主要缺点是它使得软件的后续分发变得更加困难。使用 Python 的一个优点是它在很大程度上是跨平台的,而用于编写扩展模块的语言(通常是 C 或 C++,但实际上任何可以绑定到 CPython C API 的语言)通常要求为不同的平台创建自定义二进制文件。
这意味着二进制扩展
要求最终用户能够从源代码构建它们,或者要求其他人为常用平台发布预构建的二进制文件
可能与 CPython 参考解释器的不同版本不兼容
通常无法与 PyPy、IronPython 或 Jython 等备用解释器正确配合使用
如果手工编写,则会增加维护难度,因为维护人员不仅需要熟悉 Python,还需要熟悉用于创建二进制扩展的语言以及 CPython C API 的详细信息。
如果提供了纯 Python 回退实现,则会增加维护难度,因为要求在两个地方实施更改,并在测试套件中引入额外的复杂性以确保始终执行两个版本。
依赖二进制扩展的另一个缺点是备用导入机制(例如直接从 zip 文件导入模块的能力)通常不适用于扩展模块(因为大多数平台上的动态加载机制只能从磁盘加载库)。
手工加速器模块的替代方案#
当扩展模块仅用于使代码运行得更快(在分析确定速度提升值得额外的维护工作之后),还应考虑许多其他替代方案
寻找现有的优化替代方案。CPython 标准库包含许多经过优化的数据结构和算法(尤其是在内置函数和
collections
和itertools
模块中)。Python 包索引还提供了其他替代方案。有时,选择合适的标准库或第三方模块可以避免创建自己的加速器模块的需要。对于长时间运行的应用程序,JIT 编译的 PyPy 解释器 可能为标准 CPython 运行时提供合适的替代方案。采用 PyPy 的主要障碍通常是依赖于其他二进制扩展模块 - 虽然 PyPy 确实模拟了 CPython C API,但依赖于它的模块会给 PyPy JIT 带来问题,并且模拟层通常会暴露出 CPython 当前容忍的扩展模块中的潜在缺陷(通常围绕引用计数错误 - 一个对象有一个活动引用而不是两个通常不会破坏任何东西,但没有一个引用而不是一个引用是一个主要问题)。
Cython 是一个成熟的静态编译器,可以将大多数 Python 代码编译为 C 扩展模块。初始编译提供了一些速度提升(通过绕过 CPython 解释器层),并且 Cython 的可选静态类型化功能可以提供额外的速度提升机会。使用 Cython 仍然存在与使用二进制扩展相关的 缺点,但它具有降低 Python 程序员进入门槛的优势(相对于 C 或 C++ 等其他语言)。
Numba 是一个较新的工具,由科学 Python 社区的成员创建,旨在利用 LLVM 允许在运行时将 Python 应用程序的部分内容选择性编译为本机机器代码。它要求在运行代码的系统上可以使用 LLVM,但可以提供显著的速度提升,尤其是对于适合矢量化的操作。
手工包装器模块的替代方案#
C ABI(应用程序二进制接口)是用于在多个应用程序之间共享功能的通用标准。CPython C API(应用程序编程接口)的优势之一是允许 Python 用户利用该功能。但是,手工包装模块非常繁琐,因此应考虑许多其他替代方法。
下面描述的方法根本不会简化分发案例,但它们可以显著减少维护包装器模块以使其保持最新的负担。
除了可用于创建加速器模块之外,Cython 还广泛用于为 C 或 C++ API 创建包装器模块。它涉及手工包装接口,这在设计和优化包装器代码时提供了广泛的自由度,但对于快速包装非常大的 API 可能不是一个好选择。请参阅 第三方工具列表,了解如何使用 Cython 自动包装。它还支持面向性能的 Python 实现,这些实现提供了类似 CPython 的 C-API,例如 PyPy 和 Pyston。
pybind11 是一个纯 C++11 库,它为 CPython(和 PyPy)C API 提供了一个干净的 C++ 接口。它不需要预处理步骤;它完全用模板化 C++ 编写。其中包含了用于 Setuptools 或 CMake 构建的帮助程序。它基于 Boost.Python,但不需要 Boost 库或 BJam。
cffi 是 PyPy 开发者创建的一个项目,它旨在让既了解 Python 又了解 C 的开发者可以轻松地将他们的 C 模块公开给 Python 应用程序。即使您自己不了解 C,它还可以相对轻松地基于其头文件包装一个 C 模块。
cffi
的一个主要优点是它与 PyPy JIT 兼容,允许 CFFI 包装器模块充分参与 PyPy 的跟踪 JIT 优化。SWIG 是一个包装器接口生成器,它允许包括 Python 在内的各种编程语言与 C 和 C++ 代码进行交互。
标准库的
ctypes
模块在没有头信息时可用于获取 C 级别接口,但它的缺点在于它仅在 C ABI 级别操作,因此在库实际导出的接口和 Python 代码中声明的接口之间没有自动一致性检查。相比之下,上述替代方案都可以在 C API 级别操作,使用 C 头文件来确保被包装库导出的接口和 Python 包装器模块预期的接口之间的一致性。虽然cffi
可以 直接在 C ABI 级别操作,但当以这种方式使用它时,它会遇到与ctypes
相同的接口不一致问题。
低级别系统访问的替代方案#
对于需要低级别系统访问(无论出于何种原因)的应用程序,二进制扩展模块通常是实现此目的的最佳方式。对于对 CPython 运行时本身的低级别访问尤其如此,因为某些操作(如释放全局解释器锁)在解释器运行代码时根本无效,即使使用 ctypes
或 cffi
等模块来获取对相关 C API 接口的访问权限也是如此。
对于扩展模块操作底层操作系统或硬件(而不是 CPython 运行时)的情况,有时最好只编写一个普通的 C 库(或另一个系统编程语言(如 C++ 或 Rust)中的库,该库可以导出兼容 C 的 ABI),然后使用上面描述的包装技术之一将该接口作为可导入的 Python 模块提供。
实现二进制扩展#
CPython 扩展和嵌入 指南包括编写 C 中的自定义扩展模块 的简介。
FIXME:详细说明所有这些都是您可能不希望手工编码扩展模块的原因之一 :)
扩展模块生命周期#
FIXME:此部分需要充实。
GIL 的含义#
FIXME:此部分需要充实。
内存分配 API#
FIXME:此部分需要充实。
ABI 兼容性#
CPython C API 无法保证次要版本(3.2、3.3、3.4 等)之间的 ABI 稳定性。这意味着,通常情况下,如果您针对某个 Python 版本构建扩展模块,则只能保证它与相同次要版本的 Python 配合使用,而不能与任何其他次要版本配合使用。
Python 3.2 引入了受限 API,它是 Python C API 中定义良好的子集。受限 API 所需的符号形成了“稳定 ABI”,保证与所有 Python 3.x 版本兼容。包含针对稳定 ABI 构建的扩展的轮子使用 abi3
ABI 标记,以反映它们与所有 Python 3.x 版本兼容。
CPython 的 C API 稳定性 页面提供了有关 API/ABI 稳定性保证、如何使用受限 API 以及“受限 API”的准确内容的详细信息。
构建二进制扩展#
FIXME:介绍用于构建扩展的构建后端。
为多个平台构建扩展#
如果您计划分发您的扩展,则应为所有您打算支持的平台提供 轮子。这些通常在持续集成 (CI) 系统上构建。有一些工具可以帮助您从 CI 构建高度可再分发的二进制文件;其中包括 cibuildwheel 和 multibuild。
对于大多数扩展,您需要为所有您打算支持的平台构建轮子。这意味着您需要构建的轮子数量是
count(Python minor versions) * count(OS) * count(architectures)
使用 CPython 的 稳定 ABI 可以显著减少您需要提供的轮子数量,因为平台上的单个轮子可与所有 Python 次要版本配合使用;消除了矩阵的一个维度。它还消除了为每个新次要版本的 Python 生成新轮子的需要。
Windows 的二进制扩展#
在构建二进制扩展之前,必须确保您有合适的编译器可用。在 Windows 上,Visual C 用于构建官方 CPython 解释器,并且应该用于构建兼容的二进制扩展。要为二进制扩展设置构建环境,请安装 Visual Studio Community Edition - 任何最新版本都可以。
一个警告:如果您使用 Visual Studio 2019 或更高版本,则您的扩展将依赖于一个“额外”文件 VCRUNTIME140_1.dll
,除了所有以前版本(可追溯到 2015 年)依赖的 VCRUNTIME140.dll
之外。这将为在不包含此额外文件的 CPython 版本上使用您的扩展增加一个额外要求。为避免这种情况,您可以添加编译时参数 /d2FH4-
。最新版本的 Python 可能包含此文件。
不建议为 3.5 之前的 Python 构建,因为 Microsoft 不再提供较旧版本的 Visual Studio。如果您确实需要为较旧版本构建,则可以设置 DISTUTILS_USE_SDK=1
和 MSSdk=1
以强制找到当前激活的 MSVC 版本,并且您在设计扩展时应小心,不要跨不同库分配/释放内存,避免依赖已更改的数据结构,等等。用于生成扩展模块的工具通常会为您避免这些问题。
Linux 的二进制扩展#
Linux 二进制文件必须使用足够旧的 glibc 才能与较旧的发行版兼容。 manylinux Docker 镜像提供了一个构建环境,其中包含足够旧的 glibc,以支持常见架构上的大多数当前 Linux 发行版。
macOS 的二进制扩展#
macOS 上的二进制兼容性由目标最低部署系统(例如 10.9)决定,通常在 macOS 上构建二进制文件时使用 MACOSX_DEPLOYMENT_TARGET
环境变量指定。使用 setuptools/distutils 构建时,使用标志 --plat-name
指定部署目标,例如 macosx-10.9-x86_64
。有关 macOS Python 发行版的常见部署目标,请参阅 MacPython Spinning Wheels wiki。
发布二进制扩展#
通过 PyPI 发布二进制扩展使用与发布纯 Python 包相同的上传机制。使用 build-backend 为扩展构建一个轮文件,并使用 twine 将其上传到 PyPI。
避免仅二进制版本#
强烈建议您发布二进制扩展以及用于构建它们的源代码。如果需要,这允许用户从源代码构建扩展。值得注意的是,某些 Linux 发行版需要从源代码构建,因为它们在自己的构建系统中为发行版包存储库构建源代码。
弱链接#
FIXME:此部分需要充实。
其他资源#
扩展模块的跨平台开发和分发是一个复杂的话题,因此本指南主要侧重于提供指向各种工具的指针,这些工具可以自动处理底层技术挑战。本节中的其他资源则面向希望更多了解这些系统在运行时依赖的底层二进制接口的开发人员。
使用 scikit-build 进行跨平台轮生成#
scikit-build 包有助于抽象跨平台构建操作,并在创建二进制扩展包时提供其他功能。有关 Python 二进制扩展模块的 C 运行时、编译器和构建系统生成器 的其他文档也可用。
C/C++ 扩展模块简介#
有关 CPython 在 Debian 系统上如何使用扩展模块的更深入解释,请参阅以下文章