python调用C++——pybind11

python调用C++——pybind11

官方:https://pybind11.readthedocs.io/en/stable/ ;中文翻译:https://github.com/charlotteLive/pybind11-Chinese-docs

也是一种python调用c++的方式。

具体做法是 在C++代码里要写上要绑定的C++函数,封装、编译成某个python也能用的.so库,供给python代码import进这个库。

编译,可以用cmake。

例子,看官方文档就可以。

模型部署入门教程(四)阅读笔记一之Pybind11 保姆级教程

b站:模型部署入门教程(四)阅读笔记一之Pybind11 保姆级教程

该视频的例子来自youtube:Setting up CMake, Pybind11 and QtCreator [Part 3A, Understand & Code a Kalman Filter]

第0步 放第三方库

新建文件夹,记为thirdparty,里面放第三方库,分别放了 eigenpybind11 (分别是到官网https://eigen.tuxfamily.org/index.php?title=Main_Page 下载 eigen-3.4.0.zip 解压到该路径,和 git clone https://github.com/pybind/pybind11.git

(eigen是一个矩阵代数库)

第1步 创建 CMakeLists.txt,内容为:

1
2
3
4
5
6
7
8
9
10
# 项目名称
project(kf_code)
# cmake最低版本
cmake_minimum_required (VERSION 3.0)
# 添加子目录
add_subdirectory(thirdparty/pybind11)
# 添加库
include_directories(thirdparty/engen-3.4.0)
# 编译成动态库,名字叫 kf_cpp,动态库的源文件的 wrappers.cpp
pybind11_add_module(kf_cpp wrappers.cpp)

(这里kf来自一个kalman filter项目)

第2步 创建 wrapper.cpp 文件,内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <pybind11/pybind11.h>
#include <iostream>

using namespace std;

int add(int i, int j){
std::cout << "start" << std::endl;
return i + j;
}

PYBIND11_MODULE(kf_cpp, m){
m.doc() = "pybind11 example plugin";
m.def("add", &add, "a add funciton");
}

可以看到,这里cpp文件名叫wrapper,编译出的名称叫kf_cpp,不一样,如果一样就省事,如果不一样,则需要cmake编译

第3步 make,生成 kf_cpp 可执行动态库。

1
2
3
4
mkdir build
cd build
cmake ..
make

生成 kf_cpp.cpython-39-x86_64-linux-gnu.so

  • 如果修改了 wrapper.cpp 文件,修改后,重新执行 make 一下就好。

  • 如果一直cmake不好(比如修改过cmakelist.txt),就把build文件夹删除,重新创建一次。

第4步 python里测试

1
2
3
4
5
import kf_cpp

kf_cpp.add(1,2)

#输出 3

Setting up CMake, Pybind11 and QtCreator [Part 3A, Understand & Code a Kalman Filter]

youtube:Setting up CMake, Pybind11 and QtCreator [Part 3A, Understand & Code a Kalman Filter]

youtube:Python Wrappers for C++ with Pybind11 [Part 3B, Understand & Code a Kalman Filter]

youtube:Kalman Filter in C++ (and Pybind11) [Part 3C, Understand & Code a Kalman Filter]

全系列为:https://www.youtube.com/playlist?list=PLvKAPIGzFEr8n7WRx8RptZmC1rXeTzYtA

github:kalman_python_cpp

第一个视频和上面的b站视频基本一致。

  1. set up CMake, Eigen & Pybind11
  2. C++ Skeleton and integrate w/tests
  3. populate KF code in C++ Skeleton

第二、三个视频:

验证python里调用C++执行的结果,和python自己的函数执行的结果是否一致。用了命令行指令 pytest (pytest 自动会测试以 test_开头/结尾的文件,测试类以Test开头,并且不能带有 init 方法,测试函数以test_开头)

用一个wrapper.cpp作为绑定到python的中间人,里面写进其它cpp/h的类、函数,通过这个cpp来建立python和其它cpp/h之间的绑定:

wrapper.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "kf.h"

PYBIND11_MODULE(kf_cpp, m)
{
m.doc() = "C++ KF implementation wrappers"; // optional module docstring

m.def("add", &add, "A function which adds two numbers");

pybind11::class_<KF>(m, "KF")
.def(pybind11::init<double, double, double>())
.def("predict", &KF::predict)
.def("update", &KF::update)
.def_property_readonly("cov", &KF::cov)
.def_property_readonly("mean", &KF::mean)
.def_property_readonly("pos", &KF::pos)
.def_property_readonly("vel", &KF::vel);
}

其中,类KF来自 kf.h

这里参考的是pybind11官方教程里的写法:

https://pybind11.readthedocs.io/en/stable/classes.html

1
2
3
4
5
6
7
8
9
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName)
.def("__repr__",
[](const Pet &a) {
return "<example.Pet named '" + a.name + "'>";
}
);

Python Bindings: Calling C or C++ From Python

https://realpython.com/python-bindings-overview/

翻译:Python 绑定:从 Python 调用 C 或 C++ |【生长吧!Python!】

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

这篇没看很懂,有点乱。

编写绑定

在C++代码中,创建一些代码来告诉该工具如何构建您的 Python 绑定:

1
2
3
4
5
6
7
8
// 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", &add, "A function that multiplies two numbers");
}

注意这里,一般 PYBIND11_MODULE 的第一个参数 模块名,和这个cpp文件的名称相同最好??,就不需要自己写一个init了。

(上面这个代码例子,第一个参数模块名是pybind11_example,但是cpp文件名称是pybind11_wrapper

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

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

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

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

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

  • **cpp_function**是您将在 Python 中使用的函数的导出名称。如本例所示,它不需要匹配 C++ 函数的名称。(后面python调用函数,用的这个名称)
  • &add 获取要导出的函数的地址。(C++函数名的地址)(这里是cppmult.hpp里定义的一个函数add
  • "A function..." 是函数的可选文档字符串。

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

这里cppmult.hpp是:

1
2
3
4
5
6
#ifndef CPPMULT_HPP
#define CPPMULT_HPP

#include <iostream>
int add();
#endif

这里cppmult.cpp是:

1
2
3
4
5
6
7
8
#include <iostream>
#include "cppmult.hpp"
int add(){
return 0;
}
int main(){
return 0;
}

构建 Python 绑定

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

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

1
2
3
4
5
6
# tasks.py
import invoke
invoke.run(
"g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC cppmult.cpp "
"-o libcppmult.so "
)

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

1
2
3
4
$ invoke build-cppmult
==================================================
= Building C++ Library
* Complete

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

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

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

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

1
2
$ python3 -m pybind11 --includes
-I/mnt/k/anaconda3/include/python3.9 -I/mnt/k/anaconda3/lib/python3.9/site-packages/pybind11/include

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

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

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

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

1
2
$ python3-config --extension-suffix
.cpython-39-x86_64-linux-gnu.so

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

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

运行此命令以构建绑定:

1
2
3
4
5
6
7
$ invoke build-pybind11
==================================================
= Building C++ Library
* Complete
==================================================
= Building PyBind11 Module
* Complete

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

调用你的函数

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

1
2
3
4
5
6
7
8
9
# pybind11_test.py
import 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_examplePYBIND11_MODULE宏中用作模块的名称,因此这就是您导入的名称。在m.def()您告诉PyBind11cppmult函数导出为 的调用中cpp_function,这就是您用来从 Python 调用它的方法。

你也可以测试它invoke

1
2
3
4
5
$ 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,这使得它不同于ctypesCFFI。它有几个特性使其对 C++ 库非常有吸引力:

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

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

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