使用 pybind11 将 C++ 代码绑定为 Python 扩展

本文介绍如何使用 pybind11 将 C++ 代码封装为 Python 扩展模块,并通过 Conan 管理依赖库,构建项目,最终在 Python 中直接调用 C++ 接口的逻辑。


当前开发环境

  • 操作系统:macOS
  • Python 版本:3.12
  • Conan 版本:2.15
  • CMake 版本:>= 3.15(示例中使用的是 4.0.0)
  • VS Code 作为开发环境

项目结构

my_project/
├── CMakeLists.txt
├── conanfile.py
├── include/
│   ├── math_ops.h
│   └── string_ops.h
├── src/
│   ├── math_ops.cpp
│   └── string_ops.cpp
├── bindings/
│   └── pybind_module.cpp
├── test/
│   └── test.py
├── build.sh
└── clean.sh

第三方依赖,通过 Conan 安装

conanfile.py 中声明依赖:

from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout

class MyProjectConan(ConanFile):
    name = "my_project"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    requires = (
        "pybind11/2.11.1",
        "fmt/10.2.1",
        "spdlog/1.13.0"
    )
    generators = "CMakeDeps", "CMakeToolchain"

    def layout(self):
        cmake_layout(self)

我也不清楚这2个三方库作用,下面是AI 给出的解释:

  • fmt:现代 C++ 的格式化库,类似 Python 的 f-string,适用于高性能的文本格式化。
  • spdlog:基于 fmt 构建的高性能日志库,支持异步日志、日志等级、日志文件分割等。

CMakeLists.txt 示例(直接方式)

cmake_minimum_required(VERSION 3.15)
project(my_project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(pybind11 REQUIRED)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)

file(GLOB_RECURSE SOURCES
    ${PROJECT_SOURCE_DIR}/bindings/*.cpp
    ${PROJECT_SOURCE_DIR}/src/*.cpp
)

pybind11_add_module(${PROJECT_NAME} ${SOURCES})

target_include_directories(${PROJECT_NAME} PUBLIC
    ${PROJECT_SOURCE_DIR}/include
)

target_link_libraries(${PROJECT_NAME} PUBLIC
    fmt::fmt
    spdlog::spdlog
)
# 构建后复制到 test 目录
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${PROJECT_NAME}> ${PROJECT_SOURCE_DIR}/test
)

构建中遇到的问题说明

在 macOS 和 Windows 上测试时发现,如果只写target_include_directories(... ${PROJECT_SOURCE_DIR}/include),即使使用了 Conan 2.0,并启用了 CMakeDepsCMakeToolchain,在构建过程中也无法自动识别三方库的头文件路径,导致编译失败。因此,需要显式添加。

# 设置头文件路径
target_include_directories(${PROJECT_NAME} PUBLIC
    ${CMAKE_SOURCE_DIR}/include
    ${pybind11_INCLUDE_DIRS} # 手动添加
    ${fmt_INCLUDE_DIRS} # 手动添加
    .... # 其他第三方依赖
)

使用静态库封装核心逻辑的替代方式

除了直接将所有源文件传递给 pybind11_add_module,我们也可以先将核心逻辑构建为一个静态库,再由 pybind11 模块链接这个库。

file(GLOB_RECURSE CORE_SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp)
add_library(core STATIC ${CORE_SOURCES})
target_include_directories(core PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(core PRIVATE fmt::fmt spdlog::spdlog)

pybind11_add_module(${PROJECT_NAME} ${PROJECT_SOURCE_DIR}/bindings/pybind_module.cpp)
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME} PRIVATE core)

这种方式的优点(AI的解释):

  • 将业务逻辑与 Python 绑定层解耦,有助于维护与单元测试;
  • 可重用 core 静态库于其他非 Python 项目。

C++ 头文件与实现

include/math_ops.h

#pragma once
int add(int a, int b);
int multiply(int a, int b);

include/string_ops.h

#pragma once
#include <string>
std::string to_upper(const std::string& input);

src/math_ops.cpp

#include "math_ops.h"
#include <fmt/core.h>

int add(int a, int b) {
    fmt::print("Adding {} and {}\n", a, b);
    return a + b;
}

int multiply(int a, int b) {
    fmt::print("Multiplying {} and {}\n", a, b);
    return a * b;
}

src/string_ops.cpp

#include "string_ops.h"
#include <spdlog/spdlog.h>
#include <algorithm>

std::string to_upper(const std::string& input) {
    spdlog::info("Converting string '{}' to uppercase", input);
    std::string result = input;
    std::transform(result.begin(), result.end(), result.begin(), ::toupper);
    return result;
}

Python 绑定代码:bindings/pybind_module.cpp

#include <pybind11/pybind11.h>
#include "math_ops.h"
#include "string_ops.h"

namespace py = pybind11;

PYBIND11_MODULE(my_project, m) {
    m.doc() = "Example Python bindings using pybind11";

    m.def("add", &add, py::arg("a"), py::arg("b"), "Add two integers");
    m.def("multiply", &multiply, py::arg("a"), py::arg("b"), "Multiply two integers");
    m.def("to_upper", &to_upper, py::arg("input"), "Convert string to uppercase");
}


构建脚本:build.sh

#!/bin/bash
set -e

conan install conanfile.py --output-folder=build --build=missing
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release
cd .. && cd test
python3 test.py

清理脚本:clean.sh

#!/bin/bash
rm -rf build CMakeUserPresets.json

Python 端测试:test.py

import my_project

result = my_bind_project.add(2, 3)
print(f"Result of add(2, 3) is: {result}")

result2 = my_bind_project.to_upper("hello")
print(f"Result of to_upper('hello') is: {result2}")

输出:

Adding 2 and 3
Result of add(2, 3) is: 5
[2025-04-23 08:00:58.180] [info] Converting string 'hello' to uppercase
Result of to_upper('hello') is: HELLO

总结

本demo通过 pybind11 将 C++ 功能暴露为 Python 模块,使用 Conan 安装和管理第三方依赖(如 fmt 和 spdlog),结合 CMake 实现跨平台构建。两种模块构建方式(直接 vs. 静态库)可根据项目需求灵活选择,并使用 shell 脚本简化构建流程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容