当前位置: 首页 > news >正文

pybind11绑定C++项目心得

前言

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 filexxx_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 modulem.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 classpy::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 functionbindB(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 Bdelete static_cast<B*>(p);}
    };
    PYBIND11_MODULE(...) {// declare Apy::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 Apy::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 Avoid xxx() override {PYBIND11_OVERRIDE_PURE(void, 	// return typeA, 		// base class typexxx		// 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(); });
    
http://www.dtcms.com/a/328646.html

相关文章:

  • Sentinel 和 Hystrix
  • MySQL 存储过程终止执行的方法
  • 力扣热题100------279.完全平方数
  • XGBoost 的适用场景以及与 CNN、LSTM 的区别
  • AQS的原理与ReentrantLock的关系
  • 基于Rocky Linux 9的容器化部署方案,使用Alpine镜像实现轻量化
  • 企业高性能web服务器(3)
  • Linux学习-应用软件编程(文件IO)
  • 【科研绘图系列】R语言绘制特定区域颜色标记散点图
  • Pytest项目_day13(usefixture方法、params、ids)
  • 【不动依赖】Kali Linux 2025.2 中安装mongosh
  • 【数据结构】二叉树详细解析
  • 安路Anlogic FPGA下载器的驱动安装与测试教程
  • C++联合体的定义
  • 数据结构 二叉树(2)堆
  • 带宽受限信道下的数据传输速率计算:有噪声与无噪声场景
  • C++方向知识汇总(四)
  • PyCATIA高级建模技术:等距平面、点云重命名与棱柱体创建的工业级实现
  • 基于Java与Vue搭建的供应商询报价管理系统,实现询价、报价、比价全流程管理,功能完备,提供完整可运行源码
  • Python训练营打卡Day30-文件的规范拆分和写法
  • 树与二叉树
  • NY198NY203美光固态闪存NY215NY216
  • 串口通信学习
  • Xshell远程连接Ubuntu 24.04.2 LTS虚拟机
  • 模型 霍特林法则
  • 自动驾驶 HIL 测试:构建 “以假乱真” 的实时数据注入系统
  • 【JavaEE】多线程之线程安全(上)
  • 学习嵌入式的第十八天——Linux——文件编程
  • nexus-集成prometheus监控指标
  • 力扣面试150题--爬楼梯 打家劫舍 零钱兑换 最长递增子序列