pybind11绑定C++项目心得

发布于:2025-08-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

前言

pybind11作为轻量级非侵入式的库,用于在C++和Python之间创建绑定。
基于C++11特性设计,语法简洁,支持自动类型转换和STL容器,无需额外依赖
适合高性能科学计算、跨平台通用库模块等绑定
但无QT支持,所以Qt相关类得手动绑定

在此记录一下将C++项目中的一些接口绑定到python的经验

一、主要用法

pybind11 官方介绍及基本用法 doc

1. 绑定基础框架

大概分为这么几步:

  1. 引入pybind11库

    • 前置准备:准备 pybind11 文件
      • python 导入:pip install ...
      • git 子模块导入: git submodule add ...(从用户负担、管理与后续维护等方面考虑,这个方式更推荐)
      • 如果上述方式都不行(比如测试环境没法联网也安装不了包),还有个办法就是单独准备 pybind11 的头文件(实在不行单独把代码复制过去也行,毕竟也就30+个头文件嘛对吧)
    • 导入包:在项目CMakeLists.txt中配置包路径
      • python:自动搜索包路径
      find_package(pybind11 REQUIRED)
      
      • git 子模块:
      add_subdirectory(${path_to_pybind11})
      
      • 仅头文件:这个配置一下头文件路径就行,放在项目中假装是子模块
      set(PYBIND_INCLUDE_DIR ...)
      
  2. 编写绑定文件
    作为非侵入式的库,pybind11可以在不改动源代码的情况下绑定接口,所以也可以用于已有动态库的拓展
    就一般项目而言,一个类对应一个绑定文件

    • 以简单的打包输出一个库的情况,在一个绑定文件中把需要绑定的类、成员、方法等都定义好就可以了(具体语法可以看下面的绑定语法
    • 对于需要绑定多个类输出多个库,它们之间的组织关系怎么处理,可以看下面的这里
  3. 打包&链接
    不同引入方式也就打包代码稍微不同,链接代码是一样的
    反正先设置一下模块名,然后打包:

    set(module_name your_module_name)
    
    • python/git:
    pybind11_add_module(${module_name} xxx.cpp)
    
    • 仅头文件:添加类的实现文件
    add_library(${module_name} SHARED 
    	xxx.cpp				// include implementation file
    	xxx_binding.cpp		// binding file
    )
    

    :注意SHAREDMODULE的区别,如果模块间涉及依赖关系,或者想程序启动时就加载这些python模块,需要使用SHARED

    • 链接:如果是只有头文件,则不用链接pybind11::module;如果这个模块依赖其他python模块,也需要把所需模块名加在这里
    # include python & pybind11 headers path
    target_include_directories(${module_name} PRIVATE
    	...
    )
    # link pybind11 and other necessary libraries
    target_link_libraries(${module_name} PRIVATE 
    	pybind11::module
    	...
    )
    # change suffix if needed, and set output path
    set_target_properties(${module_name} PROPERTIES 
    	PREFIX "" 
    	SUFFIX ".pyd"
    	LIBRARY_OUTPUT_DIRECTORY "path_to_output"
    )  # Windows or (SUFFIX ".so")  for Linux/macOS
    

    :这里set_target_properties设置的模块名仅为磁盘上的文件名,实际模块名以绑定文件中PYBIND11_MODULE定义的为准

  4. 编译生成动态库.pyd,在python中导入即可

2. 绑定语法

2.1 基础定义

绑定文件基本包括以下几个部分:

// pybind11 header and other headers
#include <pybind11/pybind11.h>
#include "..."

// namespace for convenience
namespace py = pybind11;

// define your module name
PYBIND11_MODULE(your_module_name, m) {
	// helping doc for your module
    m.doc() = "binding doc";

	// define functions/variables/classes in your module (module level)
	m.def("add", &add, "A function which adds two numbers");
}

注1:因为一般是要绑定类,所以需要include对应类的头文件
注2PYBIND11_MODULE用于定义一个模块,一个模块 只可 用其定义一次,否则会出现重复定义的问题
注3:这里的模块名your_module_name就是python代码中import时使用的模块名
注4:直接在PYBIND11_MODULE层使用m.def()定义的对象的级别属于 模块级别 ,像是全局变量、全局函数都这样定义,类的静态方法之类也可以这样定义(但要注意变量名污染的问题)
注5:绑定类相关的代码一般就放在一行(仅一个;),所以下面绑定代码一般以.def...开始而没有;结尾,只要记住最后加上;就行

2.2 C++类及类相关定义

假设有一个类A定义了一个二维点,它包含以下信息:

  • 点的坐标xy,以及私有信息data_
  • 一些构造函数
  • 重载了一些与坐标相关的操作符
  • 一些方法

A类的头文件示例:(方法什么的我就不写了,反正就是操作这几个成员)

class A{
private:
	int data_;
public:
	static const A ORIGIN;	// 坐标原点
	int x;
	int y;
	...
};
2.2.1 类、构造函数
// define module
PYBIND11_MODULE(your_module_name, m) {
	// declare your class
    py::class_<A>(m, "A")
    	// define constructors
    	.def(py::init<>())			// default constructor
        .def(py::init<int, int>())	// constructor with 2 int parameters;
        ...
}
2.2.2 类成员

可读可写用readwrite,只读用readonly,静态则加_static

// public member
.def_readwrite("x", &A::x, "X coordinate of the point")

// public static const member
.def_readonly_static("ORIGIN", &A::ORIGIN)

// protected/private member with getter/setter
.def_property("data", &A::getData, &A::setData)
2.2.3 类方法

指定方法名和对应C++类中调用的方法名即可
另外可通过py::arg设置参数名、添加默认参数等

// normal function
.def("xxx", &A::xxx)
.def("xxx", &A::xxx, py::arg("arg_name") = DEFAULT_ARG_VALUE)

// static function
.def_static("xxx", &A::xxx)

还可以利用lambda 表达式自己实现相关魔法方法:

// define how to print a object
.def("__repr__", [](const A &p) {
    return "A(" + std::to_string(p.x) + ", " + std::to_string(p.y) + ")";
})

注1:注意lambda 表达式的参数要与python中对应方法相匹配(别忘了self)
注2:类的static方法或成员也可以直接定义为模块级,两种方式各有优劣:

  • 定义到类里更有组织结构,后续扩展性更好
  • 模块级别用起来更简单直接
2.2.4 重载函数

一般来说,重载函数的参数列表不一致,可用以下两种方式进行绑定:

  • static_cast:通用解决方式
    .def("add", static_cast<int (A::*)(int, int)>(&A::add))
    .def("add", static_cast<double (A::*)(double, double)>(&A::add))
    
  • lambda 表达式:通过指定表达式的参数列表,达到重载的目的。对于复杂情况提供更灵活的处理
    .def("add", [](A& self, int a, int b) {
    	// check sanity for a/b here
    	...
    	return self.add(a, b)
    })
    

如果出错,可以使用签名宏进行检查:

// define signature checking macro
#define CHECK_SIGNATURE(func, expected) \
    static_assert(std::is_same_v<decltype(func), expected>, "Signature mismatch for " #func)

// check function
CHECK_SIGNATURE(&A::xxx, double (A::*)(const A&) const);
2.2.5 模板函数

模板函数的绑定相对其他绑定来说有点麻烦了
因为它需要显式实例化模板后再绑定对应函数

// instantiate templates here
template int A::add<int>(int, int);
template double A::add<double>(double, double);
...
	.def("add", static_cast<int (A::*)(int, int)>(&A::add))
	.def("add", static_cast<double (A::*)(double, double)>(&A::add))
2.2.6 重载的运算符

除了上面绑定类方法的两种方式外,pybind11还提供非常方便的绑定运算符的方式

  • pybind11支持方式:需添加头文件<pybind11/operators.h>
    .def(py::self + int())
    
  • 调用C++类方式:
    .def("__add__", &A::operator+)
    
  • lambda 表达式方式:
    .def("__add__", [](A& self, A&other) {
    	return self + other;
    })
    

:因为操作符的返回值问题,有的是返回类对象本身,有的是返回新的对象,为跟C++类动作保持一致,可用return-value-policies指定返回值类型

  • 返回本身:可用py::return_value_policy::reference_internal
    .def("__iadd__", &A::operator+=, py::return_value_policy::reference_internal)
    
  • 返回新对象:默认自动处理。也可用py::return_value_policy::automatic
    .def("__add__", &A::operator+, py::return_value_policy::automatic)
    

python遇到二元操作符时一般会先调用前者的__xxx__方法,不成功时再尝试调用后者的__rxxx__方法,所以一般定义一边即可;另外,原地操作符一般是__ixxx__
常用 python magic method:

__neg__(self)	// -self

__add__(self, other)	// self + other
__sub__			// -
__mul__			// *
__truediv__		// /

__eq__			// ==
__ne__			// !=

__getitem__(self, key)
__setitem__(self, key, value)
2.2.7 友元函数

如下,类A有两个友元操作符:

// c++
class A {
public:
	friend inline A operator+(const A& a, const A& b);
	friend inline bool operator==(const A& a, const A& b);
};

可以用pybind11支持直接定义为类的成员

.def(py::self + py::self)
.def(py::self == py::self)

或者定义为模块级别的操作符

m.def("__add__", [](const A& a, const A& b) {
	return a + b;
});
m.def("__eq__", [](const A& a, const A& b) {
	return a == b;
});

二、注意事项

1. CMakeLists.txt

检查事项:

  • 仅使用pybind11头文件,是否添加了实现文件
  • 有其他模块依赖时,是否链接了该模块
  • 模块名是否一致:注意,由于windows大小写不敏感,所以不要让要生成的python模块与已有模块同名
  • 检查想暴露的函数签名是否与预期一致

2. 多个类打包到同一个模块

上面已经说过,一个模块的定义只能有一次,所以如果一个模块有较多类需要绑定,则可以在各个类的绑定文件中定义绑定该类的函数,然后用一个单独的文件定义模块,并调用各个类的绑定函数:
假设有两个类A、B都需要绑定到同一个模块中:

  • 总绑定文件 core_bindings.cpp
    #include <pybind11/pybind11.h>
    
    void bindA(pybind11::module& m);
    void bindB(pybind11::module& m);
    PYBIND11_MODULE(your_module_name, m) {
    	bindA(m);	// call binding A class function
    	bindB(m);	// call binding B class function
    }
    
  • 类绑定文件只需要实现对应绑定函数即可
    // implement binding A class function
    void bindA(pybind11::module& m) {
    	py::class_<A>...
    }
    // implement binding B class function
    void bindB(pybind11::module& m) {
    	py::class_<B>...
    }
    

3. 处理一个类中对另一个类的引用

如果另一个类是作为参数或返回值类型(非继承关系),即只在方法内部调用,则可以不用管它也不用绑定

否则(有继承关系),该类必须要在本类之前进行绑定(直接在前面进行定义,如果在一个模块则编写它的绑定文件,不在一个模块则添加该模块的编译依赖),并且,在python中该模块需要在本模块之前进行导入(因为需要先有该类的定义)

4. 绑定派生类而不暴露基类

pybind11提供对这样绑定的支持,但是它需要在绑定时检查相关类的派生关系,这里涉及到访问权限的问题:

  • 如果基类析构函数public
    可直接在定义类时指明派生关系
    // B 继承自 A
    py::class_<B, A>(m, "B")
    
    或者声明但是不定义基类,然后使基类在python中不可见
    // declare A
    py::class_<A> base_class(m, "A");
    // B inherits from A
    py::class_<B, A>(m, "B")...
    // cover A
    m.attr("A") = py::none();
    
    以上方式A类在python端均不可见,且isinstance不可用(因为没有A类)
  • 如果基类析构函数不是public,则编译时会提示"无法访问 protected 成员"
    可自定义一个基类删除器,在声明基类时使用它,定义派生类后再隐藏基类
    // define A class deleter
    struct ADeleter {
        void operator()(A* p) const {
    		// access A's deconstructor through B
            delete static_cast<B*>(p);
        }
    };
    PYBIND11_MODULE(...) {
    	// declare A
    	py::class_<A, std::unique_ptr<A, ADeleter>>(m, "A");
    	...
    	m.attr("A") = py::none();
    }
    
    或者使用自定义跳板类,暴露基类析构函数,详见下节示例

5. 跳板类(Helper/Trampoline class)的利用

跳板类,就是我们自己定义继承自C++类的类,通过重写其中的方法达到改变访问权限、实例化虚基类等目的

5.1 暴露 protected/private 成员/方法

官方doc - binding protected member functions

  • 暴露方法示例:
    class PyA : public A {
    public:
        using A::foo; // A's foo is protected
    };
    ...
    	py::class_<A>(m, "A")
        	.def("foo", &PyA::foo);	// bind PyA's function
    
  • 暴露析构函数示例:
    class PyA : public A {
    public:
        using A::A;		// using A's constructor
        ~PyA() override = default;		// public deconstructor
    };
    PYBIND11_MODULE(...) {
    	// declare A
    	py::class_<A, PyA>(m, "A");
    	...
    }
    

5.2 绑定抽象基类方法

官方doc - overriding virtual functions

  • 绑定已经有实现的虚函数:可以用上面类方法的方式直接声明基类并用.def("", &...)进行绑定
  • 绑定无实现虚函数/纯虚函数,或想要支持 python 重写的函数:必须用跳板类方法,且用PYBIND11_OVERRIDE(_PURE)声明进行转发调用(加_PURE为纯虚函数)
    class PyA : public A {
    public:
        using A::A;
        // override pure virtual function in A
        void xxx() override {
            PYBIND11_OVERRIDE_PURE(
            	void, 	// return type
            	A, 		// base class type
            	xxx		// function name
            );
        }
    };
    ...
    	py::class_<A, PyA>(m, "A")
    		.def("xxx", &A::xxx)		// we still bind A's function (not PyA's)
    
    注1:定义跳板类后,绑定基类时.def()中仍是写基类的方法
    注2:此方法也适用于绑定工厂函数等需要多态支持的场景
    m.def("createB", []() { return new B(); });
    

网站公告

今日签到

点亮在社区的每一天
去签到