打包二进制扩展¶
- 页面状态:
未完成
- 上次审核时间:
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 扩展的一个特别值得注意的特性是,当它们不需要回调解释器运行时时,它们可以在长时间运行的操作(无论这些操作是 CPU 密集型还是 IO 密集型)期间释放 CPython 的全局解释器锁。
并非所有扩展模块都完全符合上述类别。例如,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,但依赖该 API 的模块会给 PyPy JIT 带来问题,并且仿真层通常会暴露 CPython 当前容忍的扩展模块中的潜在缺陷(通常是引用计数错误——一个对象有一个而不是两个活动引用通常不会破坏任何东西,但是没有引用而不是一个是一个主要问题)。
Cython 是一个成熟的静态编译器,可以将大多数 Python 代码编译为 C 扩展模块。初始编译提供了一些速度提升(通过绕过 CPython 解释器层),并且 Cython 的可选静态类型特性可以提供额外的速度提升机会。使用 Cython 仍然具有与使用二进制扩展相关的缺点,但具有降低 Python 程序员入门门槛的优点(相对于 C 或 C++ 等其他语言)。
Numba 是一个较新的工具,由科学 Python 社区的成员创建,旨在利用 LLVM 允许在运行时将 Python 应用程序的片段选择性编译为本机机器代码。它要求 LLVM 在运行代码的系统上可用,但可以显著提高速度,特别是对于适合向量化操作。
手动编码包装器模块的替代方案¶
C ABI (Application Binary Interface) 是在多个应用程序之间共享功能的通用标准。CPython C API (Application Programming Interface) 的优势之一是允许 Python 用户利用该功能。然而,手动包装模块非常繁琐,因此应考虑许多其他替代方法。
下面描述的方法完全没有简化分发情况,但它们可以显著减少保持包装器模块最新状态的维护负担。
除了对创建加速器模块有用之外,Cython 还广泛用于为 C 或 C++ API 创建包装器模块。它涉及手动包装接口,这在设计和优化包装器代码方面提供了广泛的自由,但可能不适合快速包装非常大的 API。有关使用 Cython 自动包装的第三方工具列表,请参阅。它还支持提供类似 CPython 的 C-API 的面向性能的 Python 实现,例如 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 引入了 Limited API,它是 Python C API 的一个明确定义的子集。Limited API 所需的符号构成了“稳定 ABI”,它保证在所有 Python 3.x 版本之间兼容。包含针对稳定 ABI 构建的扩展的 Wheel 文件使用 abi3 ABI 标签,以反映它们与所有 Python 3.x 版本兼容。
CPython 的C API 稳定性页面提供了有关 API / ABI 稳定性保证、如何使用 Limited API 以及“Limited API”精确内容的详细信息。
构建二进制扩展¶
FIXME:涵盖可用于构建扩展的构建后端。
为多个平台构建扩展¶
如果您打算分发您的扩展,您应该为您打算支持的所有平台提供wheel。这些通常在持续集成 (CI) 系统上构建。有一些工具可以帮助您从 CI 构建高度可分发的二进制文件;其中包括cibuildwheel和multibuild。
对于大多数扩展,您需要为您打算支持的所有平台构建 Wheel 文件。这意味着您需要构建的 Wheel 文件的数量是
count(Python minor versions) * count(OS) * count(architectures)
使用 CPython 的稳定 ABI可以显著减少您需要提供的 Wheel 文件的数量,因为单个平台上的单个 Wheel 文件可以用于所有 Python 次要版本;从而消除矩阵的一个维度。它还消除了为每个新的 Python 次要版本生成新 Wheel 文件的需要。
Windows 的二进制扩展¶
在构建二进制扩展之前,需要确保您有可用的合适编译器。在 Windows 上,Visual C 用于构建官方 CPython 解释器,并且应该用于构建兼容的二进制扩展。要为二进制扩展设置构建环境,请安装 Visual Studio Community Edition - 任何最新版本都可以。
一个注意事项:如果您使用 Visual Studio 2019 或更高版本,您的扩展将除了依赖所有以前版本(追溯到 2015)所依赖的 VCRUNTIME140.dll 之外,还将依赖一个“额外”文件 VCRUNTIME140_1.dll。这将为在不包含此额外文件的 CPython 版本上使用您的扩展增加一个额外的要求。为了避免这种情况,您可以添加编译时参数 /d2FH4-。最新版本的 Python 可能包含此文件。
不鼓励为 Python 3.5 之前的版本构建,因为旧版本的 Visual Studio 已不再从 Microsoft 提供。如果您确实需要为旧版本构建,可以设置 DISTUTILS_USE_SDK=1 和 MSSdk=1 以强制查找当前激活的 MSVC 版本,并且在设计扩展时应小心,避免在不同的库之间进行 malloc/free 内存操作,避免依赖已更改的数据结构等。用于生成扩展模块的工具通常会为您避免这些问题。
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 包相同的上传机制。您使用构建后端为您的扩展构建一个 wheel 文件,并使用 twine 将其上传到 PyPI。
避免仅二进制发布¶
强烈建议您发布二进制扩展以及用于构建它们的源代码。这允许用户在需要时从源代码构建扩展。值得注意的是,某些 Linux 发行版需要这样做,它们在自己的构建系统内从源代码构建发行版包存储库。
弱链接¶
FIXME:本节需要充实。
额外资源¶
扩展模块的跨平台开发和分发是一个复杂的话题,因此本指南主要侧重于提供指向各种工具的指针,这些工具可以自动化处理底层的技术挑战。本节中的额外资源旨在帮助开发人员了解更多关于这些系统在运行时所依赖的底层二进制接口。
使用 scikit-build 进行跨平台 Wheel 文件生成¶
scikit-build 包有助于抽象跨平台构建操作,并在创建二进制扩展包时提供额外功能。有关 Python 二进制扩展模块的 C 运行时、编译器和构建系统生成器的附加文档也可用。
C/C++ 扩展模块简介¶
有关 CPython 在 Debian 系统上如何使用扩展模块的更深入解释,请参阅以下文章
二进制 Wheel 文件的其他注意事项¶
pypackaging-native 网站提供了有关使用原生代码打包 Python 包的更多内容。它旨在概述此类项目最重要的打包问题,并提供深入的解释和参考资料。
涵盖的主题包括非 Python 编译依赖项(“原生依赖项”)、原生代码的 ABI (Application Binary Interface) 的重要性、对 SIMD 代码的依赖以及交叉编译。