前言
把一个已有的 C++ 库暴露给 Python,常见做法有三种:手写 CPython C API、用 SWIG 生成包装、用 pybind11 这类现代 C++ 绑定库。pybind11 是 header-only 的第三方库,用模板和 C++ 语法描述绑定关系,编译后得到一个 Python 扩展模块(Windows 上是.pyd,Linux 上是.so)。它支持函数、类、继承、STL 容器转换、异常翻译和 GIL 管理,代码量比裸 C API 少很多。
常见的误解包括:以为 pybind11 是标准库的一部分(它不是,需要单独安装并配置 CMake);以为写了PYBIND11_MODULE就能直接import(模块名必须与编译产物名一致);以为 C++ 的std::vector会自动变成 Python 列表(需要包含对应的转换头文件);以为释放 GIL 后还能随便访问 Python 对象(释放期间不能碰任何 Python 对象)。
本文以 C++17 为准,给出一个包含普通函数、STL 容器和自定义类的完整模块,并说明 CMake 构建、模块命名、GIL 和返回值策略。pybind11 的版本迭代较快,具体 API 细节以 pybind11 官方文档为准;本文只使用长期稳定的核心接口。
一、最小模块:函数绑定
一个 pybind11 模块的基本结构是:
// 适用于 C++17(需要 pybind11)
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int a, int b)
{
return a + b;
}
PYBIND11_MODULE(example, m)
{
m.doc() = "pybind11 example module";
m.def("add", &add, "Add two integers");
}PYBIND11_MODULE(example, m)展开后定义 Python 扩展的入口函数,第一个参数是模块名,必须与编译出的扩展文件名一致;第二个参数m是模块对象。m.def把 C++ 函数注册为 Python 函数,第一个参数是 Python 侧的名字,第二个是函数指针,第三个是文档字符串。
编译完成后,Python 侧这样使用:
import example
print(example.add(2, 3))import的名字来自扩展文件名,不是m.def里注册的函数名。如果 CMake 目标叫example,生成的产物是example.cpython-3xx-x86_64-linux-gnu.so或example.cp3xx-win_amd64.pyd,导入时写import example。模块名和文件名不一致时,导入会失败或找不到符号。
二、绑定类与 STL 容器
绑定自定义类需要pybind11/pybind11.h;要让std::vector、std::string、std::map等自动转换,需要额外包含pybind11/stl.h。没有这个头文件,编译器会抱怨无法转换参数类型。
// 适用于 C++17(需要 pybind11)
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <string>
#include <vector>
namespace py = pybind11;
struct Pet
{
Pet(const std::string& name, int age) : name(name), age(age) {}
std::string name;
int age;
std::string greet() const
{
return "I am " + name;
}
};
std::vector<int> scale(const std::vector<int>& values, int factor)
{
std::vector<int> result;
result.reserve(values.size());
for (int value : values)
result.push_back(value * factor);
return result;
}
PYBIND11_MODULE(example, m)
{
m.doc() = "pybind11 example module";
m.def("scale", &scale,
py::arg("values"), py::arg("factor"),
"Multiply every element by factor");
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string&, int>(),
py::arg("name"), py::arg("age"))
.def_readwrite("name", &Pet::name)
.def_readwrite("age", &Pet::age)
.def("greet", &Pet::greet);
}py::class_<Pet>(m, "Pet")注册类,构造函数的绑定用py::init<...>()指定参数列表。def_readwrite直接暴露公有数据成员,读写都会作用到 C++ 对象;只读成员用def_readonly。成员函数用def绑定,greet的const限定会被保留,Python 侧调用不会修改对象。
py::arg("values")让 Python 可以用关键字参数调用:
import example
print(example.scale([1, 2, 3], factor=2))
pet = example.Pet("Tom", 3)
print(pet.greet(), pet.name, pet.age)STL 转换是“值拷贝”语义:Python 列表传入时被转换成std::vector<int>,函数返回的std::vector<int>再被转换成新的 Python 列表。修改 C++ 容器不会影响原列表,反之亦然。需要共享底层数据时,应考虑py::array、缓冲区协议或自定义包装,而不是依赖 STL 转换。
三、构建:CMake 与 pybind11
pybind11 是 header-only 的,但推荐通过 CMake 的pybind11_add_module构建,它会处理 Python 头文件路径、扩展名、链接选项等细节。先安装:
python -m pip install pybind11CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(pybind_example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(example example.cpp)配置时告诉 CMake pybind11 的位置:
cmake -S . -B build -Dpybind11_DIR="$(python -m pybind11 --cmakedir)"
cmake --build build第二行命令在 Windows 的 PowerShell 或 cmd 下语法不同,应分别使用对应的环境变量写法;关键是让find_package(pybind11 CONFIG)找到pybind11Config.cmake。构建完成后,把生成的扩展放在 Python 的搜索路径中,或从构建目录启动 Python。
模块名、CMake 目标名、PYBIND11_MODULE的第一个参数三者应保持一致。改其中一个而忘记另外两个,是初次使用最常见的失败原因。
四、GIL、异常与返回值策略
GIL(global interpreter lock)是 CPython 的全局锁。Python 调用 C++ 函数时默认持有 GIL;如果 C++ 函数执行较长的纯计算,可以释放 GIL,让其他 Python 线程运行:
// 适用于 C++17(需要 pybind11)
m.def("heavy_compute", [](int n) {
py::gil_scoped_release release;
// 这里只能做纯 C++ 计算,不能访问任何 Python 对象
long long sum = 0;
for (int i = 0; i < n; ++i)
sum += i;
return sum;
});py::gil_scoped_release在构造时释放 GIL,析构时重新获取。它适合“输入已经转换成 C++ 值、输出也是 C++ 值”的函数。释放期间如果访问py::object、调用 Python 函数、抛出需要 Python 对象参与构造的异常,都会出问题。不要把释放 GIL 当成默认优化;只有确实存在长时间计算且需要并发时才使用。
异常翻译:C++ 异常穿过绑定层时,pybind11 会把它转换成 Python 异常。常见的std::invalid_argument、std::out_of_range、std::runtime_error有对应映射,具体对应关系以官方文档为准。未注册的自定义异常通常会被转换成RuntimeError。如果库对外承诺特定的 Python 异常类型,应显式注册异常翻译器。
返回值策略:默认策略对值返回是拷贝/移动,对指针返回可能涉及所有权转移。返回引用或指针时,必须明确生命周期:
- 返回函数内局部对象的引用是悬垂,任何策略都救不了;
- 返回成员对象的引用,可用
py::return_value_policy::reference_internal并配合keep_alive,让 Python 对象持有父对象; - 返回裸指针且希望 Python 接管所有权时,用
take_ownership,并确保指针来自new且只被接管一次; - 不确定时按值返回,让 pybind11 复制,语义最安全。
五、ABI 与部署
Python 扩展是二进制模块,必须与目标 Python 的 ABI 匹配。需要注意:
- Python 版本:用 Python 3.10 的头文件编译,通常不能直接拿到 3.11 下导入;
- 编译器与运行库:Windows 上 MSVC 的 debug/release、
/MD与/MT必须与 Python 解释器一致;混用可能崩溃; - 架构:32 位 Python 需要 32 位扩展,64 位同理;
- 平台:Linux 与 Windows 的扩展格式不同,不能直接拷贝。
部署时应把扩展放进与目标解释器匹配的环境,或用pip的构建流程在目标平台上编译。不要在开发机上编译后直接复制到另一台机器,除非确认 ABI 完全一致。
常见坑点
- ❌
PYBIND11_MODULE(example, m)的模块名与 CMake 目标名、扩展文件名不一致。
✅ 三者保持一致;导入名来自文件名,不是m.def注册的名字。
- ❌ 绑定
std::vector参数却没有包含pybind11/stl.h。
✅ 需要 STL 转换时包含对应头文件;否则编译期报无法转换类型。
- ❌ 释放 GIL 后继续访问
py::object或调用 Python 函数。
✅ 释放期间只做纯 C++ 计算;需要 Python 对象时先转换成本地值,或不要释放 GIL。
- ❌ 返回局部变量的引用,并设置
reference_internal期待延长生命周期。
✅ 返回值语义的对象;返回成员引用时确保父对象存活,并正确使用keep_alive。
- ❌ 对同一个裸指针既让 Python 接管所有权,又在 C++ 侧
delete。
✅ 所有权只能有一个归属;需要共享时用std::shared_ptr并明确策略。
- ❌ 用不同 Python 版本、不同运行库或不同架构编译扩展,导入时报 DLL 加载失败或符号错误。
✅ 在目标解释器环境中构建,保证版本、架构、运行库一致。
- ❌ 把
py::arg的顺序写错,Python 关键字参数与 C++ 参数错位。
✅py::arg的顺序必须与函数签名一致;改动签名时同步更新绑定。
- ❌ 在 C++ 全局对象构造阶段调用 Python API,解释器尚未初始化。
✅ Python C API 调用应在模块初始化函数或持有 GIL 的运行时路径中进行。
总结
| 环节 | 做法 | 关键点 |
|---|
| 模块入口 | PYBIND11_MODULE(name, m) | 名字与扩展文件名一致 |
| 函数 | m.def("name", &func, py::arg(...)) | 参数名与签名顺序一致 |
| 类 | py::class_<T>(m, "T") | py::init指定构造参数 |
| STL | 包含pybind11/stl.h | 转换是值拷贝语义 |
| 构建 | pybind11_add_module | 用目标 Python 环境配置 |
| GIL | py::gil_scoped_release | 释放期间不碰 Python 对象 |
| 返回值 | 优先按值返回 | 引用/指针必须明确生命周期 |
pybind11 让 C++ 与 Python 的边界变得可读:绑定代码本身就是接口文档,参数名、默认值、类的成员都写在 C++ 里。真正需要谨慎的是边界之外的东西——模块命名、ABI 匹配、对象生命周期和 GIL。把这四件事处理好,绑定层通常很薄;处理不好,问题往往不在编译期,而是在import或运行时的某次调用中才暴露。