本文介绍如何使用 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,并启用了 CMakeDeps 和 CMakeToolchain,在构建过程中也无法自动识别三方库的头文件路径,导致编译失败。因此,需要显式添加。
# 设置头文件路径
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 脚本简化构建流程。