直接从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 答案中的建议。那就是
- 使用
ccache
减少重建的成本 - 启用并行编译。这可以通过在 C++ 文件的配置头中设置
cfg['parallel'] = True
来完成。
进一步思考,如果您的扩展有多个源文件,并且您希望进行增量编译,那么这可能表明您已经超出了 cppimport
的范围,应该考虑使用更完整的构建系统,如 CMake。
为什么导入钩子需要在 .cpp 文件的第 一行上包含 "cppimport"?
修改 Python 导入系统是一个全局修改,因此会影响来自任何其他包的所有导入。因此,当我第一次实现 cppimport
时,其他包(例如 scipy
)突然开始出现问题,因为那些包内部的导入语句正在导入 C 或 C++ 文件,而不是它们打算导入的模块。为了避免这种失败模式,导入钩子使用一个“自愿”系统,其中 C 和 C++ 文件可以通过在第一行注释中包含“cppimport”文本来指定它们打算与 cppimport 一起使用。
作为导入钩子的替代方案,您可以使用 imp
或 imp_from_filepath
。cppimport.imp
和 cppimport.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 了解您的可用编译器。请尝试以下建议这里。