从 Python 调用 C++ 基本上有两种方法:一是使用 PyBind11 C++ 库生成 Python 模块,二使用 cytpes Python 包访问已编译的共享库。 使用cytpes还需要额外对c++代码进行处理,而使用 Pybind11能够直接对c++代码进行导出,并且支持c++的许多数据类型如vector的自动转换,十分方便。
安装的教程很多,在此不再赘述。然而,对一个新手来说, 难免在使用的时候会遇到很多坑,而有些坑比较玄学,本教程也只是记录遇到过的一些坑的解决办法,便于自己回顾类似的问题,如果能顺便帮助到其他人,不上荣幸。
注意:本文只适用于ubuntu系统
Example:
将c++代码导出为python接口至少需要两个文件,源文件和cmakelists.txt文件,假设都在example文件下:
1. 源文件,需要包含c++代码和pybind11的接口绑定:
pybind_test.cpp
#include <pybind11/pybind11.h>
#include <string>
#include <iostream>
using namespace std;
namespace py = pybind11;
class Pybindtest
{
public:
virtual ~Pybindtest();
Pybindtest(std::string &inputs)
{
info = inputs;
}
void printInfo()
{
std::cout << "Your input is " << info << std::endl;
}
private:
std::string info;
};
PYBIND11_MODULE(libmy_module, m)
{
m.doc() = "pybind11 Hierarchical Localization cpp backend";
py::class_<Pybindtest>(m, "Pybindtest")
.def(py::init<std::string &>())
.def("printInfo", &Pybindtest::printInfo);
}
2.CMakeLists.txt
cmake_minimum_required(VERSION 3.18.2)
project(HFnet_map)
add_compile_options(-std=c++11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g")
set(CMAKE_BUILD_TYPE "Release")
set(PYBIND11_CPP_STANDARD -std=c++11)
# python
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development)
find_package(pybind11 REQUIRED)
INCLUDE_DIRECTORIES(
${pybind11_INCLUDE_DIRS})
# 创建库和添加宏定义
add_library(my_module MODULE ${cpp_srcs})
target_compile_definitions(my_module PRIVATE PYBIND11_MODULE_NAME=my_module)
target_link_libraries(my_module PRIVATE
pybind11::module)
# 设置共享库生成的路径
set_target_properties(my_module PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
# 禁用共享库的安装
set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR}/install CACHE PATH "Installation Directory" FORCE)
坑1:ModuleNotFoundError: No module named XXXXXX或者是ImportError: dynamic module does not define module export function (PyInit_libxxxx)
这个问题通常是因为绑定的名称不匹配,需要注意三个地方:
// pybind_test.cpp
PYBIND11_MODULE(libmy_module, m)
//CMakeLists.txt
add_library(my_module MODULE ${cpp_srcs})
target_link_libraries(my_module PRIVATE
pybind11::module)
这三个地方的名称一定要一致,如果使用的是add_library(my_module xxx),因为c++编译器会给.so文件添加lib,所以PYBIND11_MODULE(libmy_module, m) 这里面需要加上lib,my_module可以替换成其他任意的名称,这是自己设置的。最后的target_link_libraries中,也一定要添加PRIVATE和pybind11::module,否则编译的时候就会报错。
另外,需要注意CMakeLists.txt如果需要指定python版本,要放在pybind11之前,否则,有时候还是会直接连接到系统自带的python版本。
# python
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development)
message(STATUS "Python Version: ${Python_Version}")
# pybind11
find_package(pybind11 REQUIRED)
坑2:undefined symbol: _ZTV6Logger (./libyolov8_inference.so)
这个问题是最折磨人的问题之一,代码编写了,编译也正常了,也导出了自己定义的.so文件。当觉得万事具备,只欠python的import时,然后突然就冒出了这个问题,瞬间感觉被泼一把冷水!然后到处查也查不出个所以然。
那么为啥会出现这个问题?
这个问题一般出现在编写的项目比较大,源文件比较多,所需的各种库也比较多的时候,原因就是遗漏了某些库或者源文件。可以使用使用ldd -r ./yolov8_inference.so(自己导出的so文件)进行查看:
linux-vdso.so.1 => (0x00007ffc61818000)
/lib64/libstdc++.so (0x00007f49ba1aa000)
libcudart.so.10.2 => /usr/local/cuda-10.2/lib64/libcudart.so.10.2 (0x00007f49b9f2c000)
libnvinfer.so.8 => /home/hx/TensorRT-8.4.2.4/lib/libnvinfer.so.8 (0x00007f49abdf2000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f49aba0e000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f49ab7f6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f49ab42c000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f49ab228000)
/lib64/ld-linux-x86-64.so.2 (0x00007f49ba5c3000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f49ab00b000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f49aae03000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f49aaafa000)
undefined symbol: _ZTV6Logger (./libyolov8_inference.so)
undefined symbol: _Z9compareABfff (./libyolov8_inference.so)
undefined symbol: _Z3NMSRKSt6vectorI10detectionsSaIS0_EERS2_Rf (./libyolov8_inference.so)
undefined symbol: _ZN2cv6String10deallocateEv (./libyolov8_inference.so)
会发现果然出现了undefined symbol问题,再使用c++filt +报错符号查看:
// 解码命令
c++filt _Z3NMSRKSt6vectorI10detectionsSaIS0_EERS2_Rf
// 输出
NMS(std::vector<detections, std::allocator<detections> > const&, std::vector<detections, std::allocator<detections> >&, float&)
很显然,这些都是自己定义的一些函数。
这一类错误编译器不会报出来,但是一使用python进行import的时候就会出错,解决这个问题的方法也很简单粗暴:缺啥补啥!例如我报错的这些函数都是在编写CMakeList.txt时,忘了把这些函数的源文件添加进去,在add_library()中添加进去遗漏的源文件后就正常了。有时候还会出现下面这种情况(只截取了一部分):
undefined symbol: PyInstanceMethod_Type (./libyolov8_inference.so)
undefined symbol: PyExc_ValueError (./libyolov8_inference.so)
undefined symbol: _Py_TrueStruct (./libyolov8_inference.so)
undefined symbol: PyExc_IndexError (./libyolov8_inference.so)
undefined symbol: PyCapsule_Type (./libyolov8_inference.so)
undefined symbol: PyModule_Type (./libyolov8_inference.so)
undefined symbol: _Py_NoneStruct (./libyolov8_inference.so)
undefined symbol: PyExc_MemoryError (./libyolov8_inference.so)
这是缺少某些库导致的,例如上面这个就是缺少了python的相关库,只需要把这部分补充上就行了,在pybind11::module后面加上Python::Python,如果出现的是其他库,继续添加到后面即可。
target_link_libraries(yolov8_inference PRIVATE
pybind11::module
Python::Python)
坑3:还是undefined symbol
这个类型的错误就很玄学了,出现的情景就是:在a.h中定义了类A,而A中的某个方法使用到了pybind11的数据结构例如py::list作为参数,假设这个函数为void test(py::list),然后在a.cpp中去实现了这个方法,那么在另一个文件b.cpp中去绑定这个类的时候,会出现undefined symbol: test这个函数。也就是说,类A中的方法test总是被认为未定义,导致python进行import的时候报错!
解决的办法就是:A的所有方法都不要包含pybind11的数据结构,然后在另一个源文件例如b.cpp中另外去写一个函数或者类去对这个数据结构进行处理,然后再传给类A的函数进行处理,这样就不会报错。
这个情况比较特殊,感觉挺玄学的,但是有可能是本人水平有限的原因,仅供参考。
坑4:段错误
运行代码最可怕的就是段错误了,短短的三个字,也没有指定位置,让人像大海捞针一样去查代码,如果代码量惊人,那简直要命。
如果c++运行没有问题,但python出现了这个问题。那大概率是使用pybind11绑定了类,然后类中刚好有一些指针成员,然后析构函数也没有好好释放。。。。。如果出现了这个问题,建议把c++中自己把握不住的一些指针改成智能指针,让c++去自己进行管理。因为python调用c++代码,中间涉及了啥秘密交易我们也理不清楚,不如交给系统自己处理。
整理记录于2024.01.25!