pybind11错误书
前言
前面我写过一篇pybind11使用心得,主要记录了编写pybind11绑定文件时的一些小知识
但是在实际项目使用过程中还遇到了一些其他的问题,在此进行记录
一、接口调用的进程/线程问题
这是一个刚开始容易忽略的问题。
包括两个部分:
- 进程/线程间通信
- 如果暴露的 C++ 类/接口是在另一个进程中运行(比如 python 进程),并且涉及其他进程中的对象相关信息(即涉及到进程间通信),那么除了绑定外,还需要利用IPC(进程间通信)将 python 端的请求发送到 C++ 端进行处理,然后将结果再返回给 python
- 对于线程间的通信问题,如果暴露的类/接口均是纯C/C++的话那就没什么问题,如果不是就涉及到下面的部分了
- 绑定Qt相关接口
- 如果你想绑定的接口中涉及Qt代码(比如从Qt主程序中获取某个对象的相关信息),那么这个接口必须是在Qt主线程(GUI线程)中进行调用,否则跨线程/跨进程访问会违反Qt的对象线程亲和性规则
- 即使你的接口参数返回值之类的对外展示的部分跟Qt完全无关,但只要实现文件中涉及了Qt,那么就需要小心处理
二、绑定类的继承顺序
绑定类时指定基类的顺序应与C++中类的继承顺序一致,这样可以为python类的MRO(方法解析顺序)提供基础
如果一个类又有基类,又有跳板类时,应优先指定原始基类,以明确 C++ 继承关系,Trampoline 类通常放在基类之后声明
三、未定义的A类不允许作为编译器内部类型特征 __is_base_of 的参数
这个一般是因为头文件不够所以 pybind11 处理时感觉类不完整
具体原因:
- 在C++中并不一定需要将所有用到的类都
include
进来,可以利用前向声明省略一些类的头文件包含操作(比如指针),然后在实现文件中再include
- 但是在绑定文件中,如果仅包含了目标类的头文件而没有包含其他必要的头文件,pybind11 就会分析不出来,从而判定这个类“未定义”
解决方案:
绑定文件中加上对应类的头文件即可
四、非公开非虚析构函数基类的绑定
我前面写的那篇心得最后提到了,对于具有非公开析构函数的基类需要自定义删除器或是利用跳板类来暴露其析构函数,这样 pybind11 解析时才能检查到
如果基类的析构函数还是非虚构的呢?
那么必须使用自定义删除器
五、特殊参数处理
以下几个问题主要涉及C++与Python的类型差异,以及pybind11在面对这些差异时的处理方式
5.1 特殊参数:引用
作为C++开发者,引用与指针想必都非常熟悉
但是 pybind11 中直接使用引用参数绑定时会有一些限制:比如 python 中有一些是不可变类型(整型int
,字符串str
等基础类型,元组tuple
等),不做特殊处理的话,pybind11 绑定时不会告诉你出错,但是使用时就会发现引用参数根本没变
解决方案
上面官方文档提到了两种解决方案
-
改变参数类型:(实际大概不会采用这个方案)改变原接口参数类型,换成 python 中允许改变的类型
Although inconvenient, one workaround is to encapsulate the immutable types in a custom type that does allow modifications.
-
绑定包装函数:利用一个包装函数,相对简单地在中间做一下类型中转
An other alternative involves binding a small wrapper lambda function that returns a tuple with all output arguments (see the remainder of the documentation for examples on binding lambda functions). An example:
对于想提供更纯粹的 python 编码体验的人来说,这就是你的选择了
- 使用元组返回:较简单的方式,把所有作为出参的参数打包进行返回
下面就是官方给出的包装int foo(int &i) { i++; return 0; }
的例子:
对于STL容器来说,也可以用类似的方式进行包装,并且返回值可以保留而不用// 自己实现包装函数 py::tuple foo_wrapper(int val) {int status = foo(val); // call original functionreturn py::make_tuple(status, val); }// 或者直接用 lambda m.def("foo", [](int i) {auto ret = foo(i);return std::make_tuple(ret, i); });
tuple
,但是多了两次拷贝操作
以包装int foo(vector<int>& lst);
为例:int foo_wrapper(py::list& py_list) {// prepare vectorvector<int> vec;for (auto item : py_list) {int i = item.cast<int>();vec.push_back(i);}// call original functionauto ret = foo(vec);// restore result to python listpy_list.attr("clear")();for (auto& i : vec) {py_list.append(py::cast(i));}return res; }
- 借用列表实现出入参:因为 python 中列表可变,所以可以借用它来实现改变入参(但是这样会改变接口的参数类型)
具体实现可参考上面STL容器的包装
- 使用元组返回:较简单的方式,把所有作为出参的参数打包进行返回
-
透明化处理:对于STL容器来说,pybind11 还提供了一种更“优雅”的方式 —— 透明化容器类型
这种方式相当于把STL容器实例化后暴露给python- 优点:仅需在绑定文件中进行简单包装声明
- 缺点:python 中使用时需要像 C++ 一样定义容器
以绑定
vector<int>
为例:// 绑定文件 // Don't forget this #include <pybind11/stl_bind.h>PYBIND11_MAKE_OPAQUE(std::vector<int>) // ... // later in binding code: py::bind_vector<std::vector<int>>(m, "VectorInt");
后面在 python 中使用时需定义:
lst = VectorInt()
5.2 处理指针的引用:无法将参数从“…* 转为 … *& ”
在 Python 中直接模拟 C++ 的指针引用输出参数是非常棘手的
所以更 pyhonic 的方法是利用包装函数,将该参数加入返回值进行返回,具体方案可看上一小节的解决方案
如果实在是必须要修改传入的 python 对象,考虑用智能指针进行内存管理
5.3 无法将参数从“…* 转为 … ** ”
类似上一个问题,还是因为 pybind11 中无法处理 python 对象到二级指针的转换
可以用包装函数,参数改为更容易从 python 转换过来的类型,包装函数内将 python 对象转为 C++ 二级指针对象
5.4 无法将参数2从"_Ty"转换为"A &&"
(我遇到这个问题是在绑定移动构造函数的时候)
因为纯粹的“移动”语义并不能对应到 python 中,并且 pybind11 中进行参数转换时也不太能直接产生一个可用于移动语义的右值引用
所以如果确实需要绑定移动语义,并且确认绑定文件中参数设置没问题,可以考虑用智能指针,毕竟 pybind11 的智能指针绑定能更好地管理对象的生命周期