跳转到主要内容

直接从Python导入C++文件!

项目描述

cppimport - 直接从Python导入C++!

贡献和架构

有关cppimport内部结构和如何参与开发的详细信息,请参阅CONTRIBUTING.md

安装

使用pip install cppimport进行安装。

快速示例

将以下C++代码保存为somecode.cpp

// cppimport
#include <pybind11/pybind11.h>

namespace py = pybind11;

int square(int x) {
    return x * x;
}

PYBIND11_MODULE(somecode, m) {
    m.def("square", &square);
}
/*
<%
setup_pybind11(cfg)
%>
*/

然后在Python解释器中导入C++扩展

>>> import cppimport.import_hook
>>> import somecode #This will pause for a moment to compile the module
>>> somecode.square(9)
81

恭喜,您已使用cppimport和pybind11的组合从Python调用了一些C++代码。

我是这个工作流程的忠实粉丝,您可以编辑C++文件和Python文件,并且重新编译是透明的!这也方便快速构建一个慢速Python函数的优化版本。

解释说明

好吧,现在我已经尽量说服您这是多么令人兴奋了,让我们深入了解如何自己完成这个操作。首先,顶部的注释是至关重要的,以便选择cppimport。别忘了这一点!(以下将解释为什么这是必要的。)

// cppimport

文件的大部分是一个通用的、简单的pybind11扩展。我们包含了pybind11头文件,然后定义了一个简单的函数来计算x的平方,然后将该函数作为名为somecode的Python扩展的一部分导出。

最后,在文件末尾,有一个我称之为“配置块”的部分

<%
setup_pybind11(cfg)
%>

<%%> 包围的这部分区域是一个 Mako 代码块。在构建过程中,该区域被评估为 Python 代码,并向 cppimport 构建系统提供配置信息,如编译器和链接器标志。

请注意,由于 Mako 预处理,配置块周围的注释可能被省略。将配置块放在文件末尾是可选的,但可以确保编译错误信息中的行号保持正确。

生产构建

在生产部署中,通常不希望包含 c/c++ 编译器,所有源文件都在运行时编译。因此,提供了一个简单的 cli 工具来预编译所有源文件。此实用程序可以在 CI/CD 管道中使用。

使用方法很简单

python -m cppimport build

这将构建当前目录(及其子目录)中所有符合条件的 *.c*.cpp 文件(即包含第一行的 // cppimport 注释)。

或者,您可以指定一个或多个要构建的根目录或源文件

python -m cppimport build ./my/directory/ ./my/single/file.cpp

注意:当指定文件路径时,将跳过该文件的标题检查(// cppimport)。

生产构建的微调

为了进一步提高生产构建的启动性能,您可以选择在导入过程中跳过校验和和编译二进制存在检查,方法是将环境变量 CPPIMPORT_RELEASE_MODE 设置为 true 或在 Python 中设置配置

cppimport.settings['release_mode'] = True

警告:确保在发布模式下预编译所有二进制文件,因为导入任何缺失的二进制文件将导致异常。

常见问题解答

实际上发生了什么?

有时 Python 简直不够快。或者您有一个现有的 C 或 C++ 库中的代码。因此,您编写了一个 Python 扩展模块,一个编译代码的库。我推荐使用 pybind11 进行 C++ 到 Python 绑定或 cffi 进行 C 到 Python 绑定。我多年来一直这样做。但我发现,当我的开发过程从 Python 的 编辑 -> 测试 转变为 编辑 -> 编译 -> 测试 时,我的生产力降低了。因此,cppimport 将编译和导入扩展模块的过程结合起来,以便您只需运行 import foobar 而不必担心多个步骤。内部,cppimport 查找 foobar.cpp 文件。假设找到了一个,它将通过 Mako 模板系统来收集编译器选项,然后将其编译并作为扩展模块加载。

cppimport 是否每次导入模块时都重新编译?

不!编译仅在模块第一次导入时发生。C++ 源文件与每次导入时的校验和进行比较,以确定是否有相关文件已更改。可以通过向 Mako 头部添加内容来跟踪附加依赖项(例如头文件)

cfg['dependencies'] = ['file1.h', 'file2.h']

校验和通过简单地将扩展 C++ 文件的内容与 cfg['sources']cfg['dependencies'] 中的文件内容连接起来来计算。

如何设置编译器或链接器参数?

标准 distutils 配置选项是有效的

cfg['extra_link_args'] = ['...']
cfg['extra_compile_args'] = ['...']
cfg['libraries'] = ['...']
cfg['include_dirs'] = ['...']

例如,要使用 C++11,请添加

cfg['extra_compile_args'] = ['-std=c++11']

如何将扩展模块拆分为多个源文件?

在配置块中

cfg['sources'] = ['extra_source1.cpp', 'extra_source2.cpp']

cppimport 没有按预期工作,我可以获取更详细的输出吗?

cppimport 使用标准的 Python 日志工具。请将日志处理程序添加到根日志记录器或 "cppimport" 日志记录器。例如,要输出所有调试级别的日志消息

root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
root_logger.addHandler(handler)

如何强制重新构建,即使校验和匹配?

设置

cppimport.settings['force_rebuild'] = True

如果这是一个常见的现象,我非常愿意听听您的用例以及为什么校验和、cfg['dependencies']cfg['sources'] 的组合是不够的!

请注意,在并发导入模块时,force_rebuild 不起作用。

我可以并发导入我的模型吗?

使用 cppimport 并发地使用多线程、进程甚至机器导入模块是安全的!

在构建模块之前,cppimport 获取一个锁文件,防止其他处理器同时构建它 - 这防止了可能导致失败的冲突。其他进程将等待最多 10 分钟,直到第一个进程构建了模块并加载它。如果您的模块在 10 分钟内没有构建,则将超时。您可以在设置中增加超时时间。

cppimport.settings['lock_timeout'] = 10*60 # 10 mins

在并发导入时,您不应使用 force_rebuild

我如何获取配置块中文件路径的信息?

模块名称作为 fullname 变量可用,C++ 模块文件作为 filepath 可用。例如,

<%
module_dir = os.path.dirname(filepath)
%>

我如何使编译更快?

在单文件扩展中,这是 C++ 的一个基本问题。高度模板化的代码通常编译很慢。

如果您的扩展有多个源文件,并使用 cfg['sources'] 功能,那么您可能希望进行某种增量编译。对于初学者来说,增量编译仅涉及重新编译已更改的源文件。遗憾的是,这是不可能的,因为 cppimport 是建立在 setuptools 和 distutils 之上的,这些标准库组件不支持增量编译。

我建议遵循这个 SO 答案中的建议。那就是

  1. 使用 ccache 减少重建的成本
  2. 启用并行编译。这可以通过在 C++ 文件的配置头中设置 cfg['parallel'] = True 来完成。

进一步思考,如果您的扩展有多个源文件,并且您希望进行增量编译,那么这可能表明您已经超出了 cppimport 的范围,应该考虑使用更完整的构建系统,如 CMake。

为什么导入钩子需要在 .cpp 文件的第 一行上包含 "cppimport"?

修改 Python 导入系统是一个全局修改,因此会影响来自任何其他包的所有导入。因此,当我第一次实现 cppimport 时,其他包(例如 scipy)突然开始出现问题,因为那些包内部的导入语句正在导入 C 或 C++ 文件,而不是它们打算导入的模块。为了避免这种失败模式,导入钩子使用一个“自愿”系统,其中 C 和 C++ 文件可以通过在第一行注释中包含“cppimport”文本来指定它们打算与 cppimport 一起使用。

作为导入钩子的替代方案,您可以使用 impimp_from_filepathcppimport.impcppimport.imp_from_filepath 执行与导入钩子完全相同的操作,但方式更明确。

foobar = cppimport.imp("foobar")
foobar = cppimport.imp_from_filepath("src/foobar.cpp")

默认情况下,这些显式函数不需要在 C++ 源文件的第一行上包含“cppimport”关键字。

Windows?

CI 系统不在 Windows 上运行。欢迎添加更多 Windows 支持的 PR。我已使用 MinGW-w64 和 Python 3.6 使用 cppimport 并取得了成功。我还收到报告,称 cppimport 在 Windows 上与 Python 3.6 和 Visual C++ 2015 Build Tools 一起工作。主要挑战是确保 distutils 了解您的可用编译器。请尝试以下建议这里

cppimport 使用 MIT 许可证

项目详情


下载文件

下载您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源代码发行版

cppimport-22.8.2.tar.gz (26.6 kB 查看哈希值)

上传时间 源代码