用pybind11连接c++和python

一. 第一步先安装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 的繁琐和内存管理的风险。

下面是实现这个目标的完整步骤和最佳实践。


## 总体策略

我们将分四步走,确保每一步都清晰可靠:

  1. 准备 Python 类: 编写您想在 C++ 中使用的 Python 类。
  2. 集成 pybind11: 修改您现有 C++ 工程的 CMakeLists.txt,使其能够支持 pybind11 的嵌入功能。
  3. 编写 C++ 封装层: 创建一个 C++ 类来专门负责与 Python 类的交互,将 Python 的复杂性封装起来,只对 C++ 工程的其他部分暴露简单的接口。
  4. 配置运行时环境: 确保最终生成的可执行文件 (.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)

  1. 下载并解压官方可嵌入包到您的输出目录(例如 build/Release)。
  2. 确保解压后修改了 pythonXX._pth 文件,启用了 import site
  3. 将您编译好的 C++ 主程序 .exe 和所有它依赖的 .dll 放到这个目录里。
  4. 将您的 my_processor.py 文件也放到这个目录里(上文的 add_custom_command 会自动完成这一步)。

通过以上四个步骤,您就可以非常可靠、方便地将一个 Python 类作为新功能集成到任何现有的 C++ 工程中。

四. 多线程c++程序中,调用pybind11引发的线程问题

以上第三章中设计的改动方法,部分情况下有效,是将run_new_feature()放到publisher进程初始化时可以,但是放到subscriber中接收到数据后执行,就不行,编译可以,但是运行过程中会卡在processor类初始化那一句,然后自动退出。后来发现是线程问题。

  1. 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)在多线程环境下的工作方式。


## 死锁原因分析

让我们来梳理一下程序中两个关键线程的行为:

  1. 主线程 (Main Thread)

    • 在您的 main 函数中,第一行代码 py::scoped_interpreter guard{}; 不仅初始化了 Python 解释器,还为当前线程(也就是主线程)获取并持有了 GIL
    • 接着,您的主线程调用了 mysub->run(),并最终阻塞(停在)了 std::cin.ignore() 这一行,等待您从键盘按回车。
    • 关键点:在主线程等待您按键的整个过程中,它一直占有着 GIL,没有释放。
  2. 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 里的代码,程序就不会再卡住了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容