ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

pybind11: C++ 工程提供 Python 接口的实例代码

pybind11: C++ 工程提供 Python 接口的实例代码

前言


把一个已有的 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 pybind11

CMakeLists.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 完全一致。


常见坑点



  1. ❌PYBIND11_MODULE(example, m)的模块名与 CMake 目标名、扩展文件名不一致。


✅ 三者保持一致;导入名来自文件名,不是m.def注册的名字。



  1. ❌ 绑定std::vector参数却没有包含pybind11/stl.h。


✅ 需要 STL 转换时包含对应头文件;否则编译期报无法转换类型。



  1. ❌ 释放 GIL 后继续访问py::object或调用 Python 函数。


✅ 释放期间只做纯 C++ 计算;需要 Python 对象时先转换成本地值,或不要释放 GIL。



  1. ❌ 返回局部变量的引用,并设置reference_internal期待延长生命周期。


✅ 返回值语义的对象;返回成员引用时确保父对象存活,并正确使用keep_alive。



  1. ❌ 对同一个裸指针既让 Python 接管所有权,又在 C++ 侧delete。


✅ 所有权只能有一个归属;需要共享时用std::shared_ptr并明确策略。



  1. ❌ 用不同 Python 版本、不同运行库或不同架构编译扩展,导入时报 DLL 加载失败或符号错误。


✅ 在目标解释器环境中构建,保证版本、架构、运行库一致。



  1. ❌ 把py::arg的顺序写错,Python 关键字参数与 C++ 参数错位。


✅py::arg的顺序必须与函数签名一致;改动签名时同步更新绑定。



  1. ❌ 在 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 环境配置

GILpy::gil_scoped_release释放期间不碰 Python 对象

返回值优先按值返回引用/指针必须明确生命周期



pybind11 让 C++ 与 Python 的边界变得可读:绑定代码本身就是接口文档,参数名、默认值、类的成员都写在 C++ 里。真正需要谨慎的是边界之外的东西——模块命名、ABI 匹配、对象生命周期和 GIL。把这四件事处理好,绑定层通常很薄;处理不好,问题往往不在编译期,而是在import或运行时的某次调用中才暴露。




RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进