一. 第一步先安装pybind11
看教程有两种,一种直接pip install,另一种下载源码之后编译,目前还没确定,这两种只用一个就可以,还是需要两个都操作之后才行。
测试了一下,只用pip安装也是可以的,在用cmake时候,find_package(pybind11)需要给出指定寻找位置,不然会提示找不到,可以在cmake时候指定cmake .. -DCMAKE_PREFIX_PATH="D:\03_ClangPrac\pybind\ZYformation\.venv\Lib\site-packages"这就是在构建了虚拟环境情况下,指定虚拟环境的pybind11位置为寻找路径,这样cmake就可以找到pybind11了。
pip方式简单明了,只会提示你安装pytest包,没什么问题。我们讲一下第二种自行编译遇到的问题。
下载源码后,编译过程先提示Catch not detected. Interpreter tests will be skipped.,是catch没有安装。一种解决方法
# -S . 指的是源代码目录,-B build 指的是构建目录
cmake -S . -B build -DDOWNLOAD_CATCH=ON
在cmake时候让cmake自己下载安装catch,我试了后发现下载不动,换成手动安装方式。需要手动下载catch.h,放到pybind11源码文件夹中,例如/third_party/include文件夹,然后cmake时候,指定catch文件位置:
# 假设你已经进入了空的 build 目录
cmake .. -DCATCH_INCLUDE_DIR="../third_party/include"
cmake --build . --config Release
有时候重新cmake时候,需要删掉之前的build文件夹内容,防止缓存影响新的configure过程。
应该就是编译成功了,可以在其他项目中用pybind11了。
二. 项目中应用pybind11,c调用python
对于项目的cmakelists.txt文件,需要修改以加入python解释器和加入pybind11包引用,典型的改动如下:
# 1. 查找 Python (这是嵌入所必需的)
# 确保找到 Interpreter 和 Development 组件
find_package(Python3 3.6 COMPONENTS Interpreter Development REQUIRED)
# 2. 包含 pybind11 的源码树
# 确保路径正确,这里假设 pybind11 和你的项目是同级目录
add_subdirectory(../pybind11 ${CMAKE_CURRENT_BINARY_DIR}/pybind11-build)
注意,如果pybind11已经安装过,可以直接用find_package(pybind11),如果没装过,就得用以上add_subdirectory()来手动添加pybind11工程的位置,此处直接是将pybind11的文件夹放到工程附近了。
注意,其中后一个地址${CMAKE_CURRENT_BINARY_DIR}/pybind11-build是用来告诉cmake将外部的pybind11配置后文件放到哪个位置,因为pybind11属于工程外部的内容,cmake会疑惑将这个外部引用的配置后的文件放到pybind11目录中(会污染源文件),还是放到c++工程中,所以需要有一个参数来明确配置后的工程文件的放置位置。
# 3. 将 pybind11::embed 链接到你的主目标
# 这是让你的 C++ 工程能调用 Python 的关键
target_link_libraries(routeDesignTest
PRIVATE pybind11::embed
PUBLIC fastcdr fastrtps routeDesignTest_lib serializer_lib
)
这里主要添加了PRIVATE pybind11::embed这一句,将pybind加入了工程中。
# 4. (推荐) 添加自定义命令,在构建后自动复制 Python 脚本
# 这样能确保你的 .exe 总能找到 .py 文件
add_custom_command(
TARGET routeDesignTest
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/my_processor.py"
"$<TARGET_FILE_DIR:routeDesignTest>/my_processor.py"
COMMENT "Copying Python script to output directory..."
)
第4步是额外写了脚本将pyhon文件放到最后编译完成的文件夹中,也可以手动复制完成,但是如果忘记把pyhon文件放到编译文件同一个文件夹中,就会报错。
三. 在fastDDS工程中内嵌调用python,验证pybind11用法
要“可靠方便”地实现这个改动,最佳选择是使用 pybind11 的嵌入功能 (pybind11/embed.h)。这能让您用现代、安全的 C++ 风格来调用 Python,避免了原生 C API 的繁琐和内存管理的风险。
下面是实现这个目标的完整步骤和最佳实践。
## 总体策略
我们将分四步走,确保每一步都清晰可靠:
- 准备 Python 类: 编写您想在 C++ 中使用的 Python 类。
-
集成 pybind11: 修改您现有 C++ 工程的
CMakeLists.txt,使其能够支持pybind11的嵌入功能。 - 编写 C++ 封装层: 创建一个 C++ 类来专门负责与 Python 类的交互,将 Python 的复杂性封装起来,只对 C++ 工程的其他部分暴露简单的接口。
-
配置运行时环境: 确保最终生成的可执行文件 (
.exe) 在运行时能找到 Python 解释器和所有需要的模块。
## 步骤一:准备您的 Python 类
假设我们有一个 Python 类,负责处理一些数据。
my_processor.py:
# my_processor.py
class DataProcessor:
"""一个用于演示的简单数据处理器。"""
def __init__(self, name: str):
self.name = name
self.processed_items = 0
print(f"Python 端: DataProcessor '{self.name}' 已被初始化。")
def process_data(self, data: str) -> str:
"""接收一个字符串,处理后返回一个新的字符串。"""
print(f"Python 端: 正在处理数据 '{data}'...")
self.processed_items += 1
# 假设处理逻辑是转换成大写并加上前缀
return f"PROCESSED: {data.upper()}"
@property
def items_count(self) -> int:
"""一个只读属性,返回已处理的数据项数量。"""
return self.processed_items
## 步骤二:修改现有的 CMakeLists.txt
您需要告诉您现有的 CMake 工程如何找到并链接 pybind11 和 Python。
假设您的主目标名为 MyAwesomeApp (add_executable(MyAwesomeApp ...)):
# 在你的 CMakeLists.txt 中找到合适的位置,添加以下内容
# 1. 查找 Python (这是嵌入所必需的)
# 确保找到 Interpreter 和 Development 组件
find_package(Python3 3.6 COMPONENTS Interpreter Development REQUIRED)
# 2. 包含 pybind11 的源码树
# 确保路径正确,这里假设 pybind11 和你的项目是同级目录
add_subdirectory(../pybind11 ${CMAKE_CURRENT_BINARY_DIR}/pybind11-build)
# 3. 将 pybind11::embed 链接到你的主目标
# 这是让你的 C++ 工程能调用 Python 的关键
target_link_libraries(MyAwesomeApp PRIVATE pybind11::embed)
# 4. (推荐) 添加自定义命令,在构建后自动复制 Python 脚本
# 这样能确保你的 .exe 总能找到 .py 文件
add_custom_command(
TARGET MyAwesomeApp
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/my_processor.py"
"$<TARGET_FILE_DIR:MyAwesomeApp>/my_processor.py"
COMMENT "Copying Python script to output directory..."
)
## 步骤三:编写 C++ 封装类
这是一个非常推荐的设计模式。不要在您 C++ 工程的业务逻辑中到处写 Python 调用代码,而是创建一个专门的 C++ 封装类。
PythonProcessor.h (头文件):
#pragma once
#include <string>
#include <pybind11/pybind11.h> // 包含 pybind11 的核心头文件
namespace py = pybind11;
class PythonProcessor {
public:
PythonProcessor(const std::string& name);
~PythonProcessor(); // 确保资源被正确管理
std::string processData(const std::string& data);
int getItemsCount();
private:
// py::scoped_interpreter 会管理 Python 解释器的生命周期
// 确保它只被初始化一次
static py::scoped_interpreter guard;
// 用于持有 Python DataProcessor 类的实例
py::object processor_instance;
};
PythonProcessor.cpp (实现文件):
#include "PythonProcessor.h"
#include <pybind11/embed.h> // 包含嵌入所需的头文件
#include <iostream>
// 静态成员初始化
py::scoped_interpreter PythonProcessor::guard{};
PythonProcessor::PythonProcessor(const std::string& name) {
try {
// 将当前目录添加到 Python 的搜索路径
py::module_::import("sys").attr("path").attr("append")(".");
// 导入包含我们类的 Python 模块
auto processor_module = py::module_::import("my_processor");
// 创建 Python 类的实例,并传入构造函数参数 name
// .attr("DataProcessor") 获取类,后面的 () 调用构造函数
processor_instance = processor_module.attr("DataProcessor")(name);
} catch (const py::error_already_set &e) {
std::cerr << "Python initialization failed:\n" << e.what() << std::endl;
// 在实际项目中,这里应该有更健壮的错误处理
}
}
PythonProcessor::~PythonProcessor() {
// py::object 会自动处理引用计数,当 processor_instance 被销毁时,
// Python 对象的引用会减少,最终被垃圾回收。
}
std::string PythonProcessor::processData(const std::string& data) {
if (!processor_instance) {
return "Error: Python instance not created.";
}
// 调用实例的方法 .attr("process_data"),并传入参数 data
auto result = processor_instance.attr("process_data")(data);
return result.cast<std::string>();
}
int PythonProcessor::getItemsCount() {
if (!processor_instance) {
return -1;
}
// 访问实例的属性 .attr("items_count")
auto result = processor_instance.attr("items_count");
return result.cast<int>();
}
现在,在您 C++ 工程的任何其他地方,您只需要 #include "PythonProcessor.h" 并像使用普通 C++ 对象一样使用它即可:
// 在你的 C++ 主逻辑代码中
#include "PythonProcessor.h"
#include <iostream>
void run_new_feature() {
PythonProcessor proc("C++ Processor");
std::string result = proc.processData("some important data");
std::cout << "Result from Python: " << result << std::endl;
std::cout << "Items processed so far: " << proc.getItemsCount() << std::endl;
}
## 步骤四:配置运行时环境
这是我们之前反复解决的问题。为了让 main.exe 最终能运行,您需要为其准备一个完整的、自洽的 Python 环境。
最可靠的方法是使用我们之前讨论过的 Python 官方可嵌入包 (Embeddable Package)。
- 下载并解压官方可嵌入包到您的输出目录(例如
build/Release)。 - 确保解压后修改了
pythonXX._pth文件,启用了import site。 - 将您编译好的 C++ 主程序
.exe和所有它依赖的.dll放到这个目录里。 - 将您的
my_processor.py文件也放到这个目录里(上文的add_custom_command会自动完成这一步)。
通过以上四个步骤,您就可以非常可靠、方便地将一个 Python 类作为新功能集成到任何现有的 C++ 工程中。
四. 多线程c++程序中,调用pybind11引发的线程问题
以上第三章中设计的改动方法,部分情况下有效,是将run_new_feature()放到publisher进程初始化时可以,但是放到subscriber中接收到数据后执行,就不行,编译可以,但是运行过程中会卡在processor类初始化那一句,然后自动退出。后来发现是线程问题。
- Python 初始化时机和线程问题 (主要原因)
on_data_available 是一个异步回调:这个函数不是由您的主线程调用的,而是由 Fast DDS 的一个内部工作线程在接收到数据时触发的。
Python 解释器初始化不是线程安全的:Py_Initialize(py::scoped_interpreter 内部会调用它)这个函数非常“娇气”,它必须在程序生命周期中只被调用一次,并且应该在主线程中、在任何其他可能与 Python 交互的线程启动之前调用。
您的当前代码违反了这些原则:您在 run_new_feature 中创建了一个局部的 PythonProcessor 对象。这意味着每一次 on_data_available 被 DDS 的工作线程调用时,您的代码都会重复地尝试去初始化 Python 解释器,并且是在一个非主线程中进行。这必然会导致未定义的行为,通常就是直接崩溃或退出。
所以要将线程初始化放到主程序中,py::scoped_interpreter guard{};,这样确保程序全生命周期只执行一次初始化,删除掉PythonProcessor.cpp和h文件中的初始化语句,然后主程序初始化完成后要释放。
以下是大模型解答:
您好,这个问题非常经典,程序停在
py::gil_scoped_acquire acquire;是一个典型的**多线程死锁(Deadlock)**问题。您已经非常接近成功了,这最后一步是理解 Python 全局解释器锁(GIL)在多线程环境下的工作方式。
## 死锁原因分析
让我们来梳理一下程序中两个关键线程的行为:
主线程 (Main Thread):
- 在您的
main函数中,第一行代码py::scoped_interpreter guard{};不仅初始化了 Python 解释器,还为当前线程(也就是主线程)获取并持有了 GIL。- 接着,您的主线程调用了
mysub->run(),并最终阻塞(停在)了std::cin.ignore()这一行,等待您从键盘按回车。- 关键点:在主线程等待您按键的整个过程中,它一直占有着 GIL,没有释放。
DDS 工作线程 (Worker Thread):
- 与此同时,Fast DDS 的一个后台工作线程在网络上接收到了数据。
- 这个工作线程调用了您的回调函数
on_data_available,然后进一步调用run_new_feature。- 在
run_new_feature函数中,工作线程执行到py::gil_scoped_acquire acquire;,它尝试去获取 GIL,因为这是安全调用 Python API 的前提。- 关键点:但是,GIL 此刻正被您的主线程牢牢占有。
死锁发生:
- DDS 工作线程永远地等待主线程释放 GIL。
- 主线程永远地等待您按下回车键。
两个线程互相等待对方,导致整个程序逻辑“卡住”,看起来就像是停止执行了。
## 解决方案:在主线程中及时释放 GIL
解决方案是在主线程完成 Python 初始化后,立即释放 GIL,让它空闲出来,以便其他工作线程可以随时获取并使用它。
pybind11同样为此提供了一个 RAII 工具:py::gil_scoped_release。修改您的
main函数请找到您的主程序入口
main函数(很可能在routeDesignTest_main.cpp文件中),并在py::scoped_interpreter guard{};之后添加一行py::gil_scoped_release release;。
routeDesignTest_main.cpp(修改后):#include "routeDesignTestSubscriber.h" #include <fastdds/dds/domain/DomainParticipantFactory.hpp> #include <pybind11/embed.h> // 包含 embed.h namespace py = pybind11; int main(int argc, char** argv) { // 1. 初始化 Python 解释器,并为当前(主)线程获取 GIL py::scoped_interpreter guard{}; // 2. 关键!主线程在进入阻塞等待前,应立即释放 GIL, // 以便其他 DDS 工作线程可以使用 Python。 py::gil_scoped_release release; // ------------------------------------------------------------- // 现在主线程在没有持有 GIL 的情况下运行下面的代码 std::cout << "Starting subscriber." << std::endl; routeDesignTestSubscriber* mysub = new routeDesignTestSubscriber(); if (mysub->init()) { mysub->run(); // 主线程在这里阻塞,但不再持有 GIL } delete mysub; return 0; // main 函数结束时,guard 的析构函数会自动重新获取 GIL 并安全地关闭解释器 }## 总结
这个 “初始化后立即释放 GIL” 的模式,是在 C++ 主线程中启动 Python,然后在其他工作线程中执行 Python 代码的标准做法。
修改完
main函数后,请重新编译并运行。这次,当您的 DDS 回调线程尝试获取 GIL 时,会发现 GIL 是可用的,于是它就能成功获取并继续执行PythonProcessor里的代码,程序就不会再卡住了。