尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

pybind11绑定C++项目心得

pybind11绑定C++项目心得 前言pybind11作为轻量级、非侵入式的库用于在C和Python之间创建绑定。基于C11特性设计语法简洁支持自动类型转换和STL容器无需额外依赖。适合高性能科学计算、跨平台通用库模块等绑定但无QT支持所以Qt相关类得手动绑定在此记录一下将C项目中的一些接口绑定到python的经验一、主要用法pybind11 官方介绍及基本用法 doc1. 绑定基础框架大概分为这么几步引入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...)编写绑定文件作为非侵入式的库pybind11可以在不改动源代码的情况下绑定接口所以也可以用于已有动态库的拓展就一般项目而言一个类对应一个绑定文件以简单的打包输出一个库的情况在一个绑定文件中把需要绑定的类、成员、方法等都定义好就可以了具体语法可以看下面的绑定语法对于需要绑定多个类输出多个库它们之间的组织关系怎么处理可以看下面的这里打包链接不同引入方式也就打包代码稍微不同链接代码是一样的反正先设置一下模块名然后打包set(module_name your_module_name)python/gitpybind11_add_module(${module_name}xxx.cpp)仅头文件add_library(${module_name}SHARED xxx_binding.cpp// binding file)注注意SHARED与MODULE的区别如果模块间涉及依赖关系或者想程序启动时就加载这些python模块需要使用SHARED链接如果是只有头文件则不用链接pybind11::module如果这个模块依赖其他python模块也需要把所需模块名加在这里#includepythonpybind11 headers pathtarget_include_directories(${module_name}PRIVATE...)#linkpybind11andother necessary librariestarget_link_libraries(${module_name}PRIVATE pybind11::module...)#changesuffixifneeded,andset output pathset_target_properties(${module_name}PROPERTIES PREFIXSUFFIX.pydLIBRARY_OUTPUT_DIRECTORYpath_to_output)# Windowsor(SUFFIX.so)forLinux/macOS注这里set_target_properties设置的模块名仅为磁盘上的文件名实际模块名以绑定文件中PYBIND11_MODULE定义的为准编译生成动态库.pyd在python中导入即可2. 绑定语法2.1 基础定义绑定文件基本包括以下几个部分// pybind11 header and other headers#includepybind11/pybind11.h#include...// namespace for conveniencenamespacepypybind11;// define your module namePYBIND11_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定义了一个二维点它包含以下信息点的坐标x、y以及私有信息data_一些构造函数重载了一些与坐标相关的操作符一些方法A类的头文件示例方法什么的我就不写了反正就是操作这几个成员classA{private:intdata_;public:staticconstA ORIGIN;// 坐标原点intx;inty;...};2.2.1 类、构造函数// define modulePYBIND11_MODULE(your_module_name,m){// declare your classpy::class_A(m,A)// define constructors.def(py::init())// default constructor.def(py::initint,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__,[](constAp){returnA(std::to_string(p.x), std::to_string(p.y));})注1注意lambda 表达式的参数要与python中对应方法相匹配别忘了self注2类的static方法或成员也可以直接定义为模块级两种方式各有优劣定义到类里更有组织结构后续扩展性更好模块级别用起来更简单直接2.2.4 重载函数一般来说重载函数的参数列表不一致可用以下两种方式进行绑定static_cast通用解决方式.def(add,static_castint(A::*)(int,int)(A::add)).def(add,static_castdouble(A::*)(double,double)(A::add))lambda 表达式通过指定表达式的参数列表达到重载的目的。对于复杂情况提供更灵活的处理.def(add,[](Aself,inta,intb){// check sanity for a/b here...returnself.add(a,b)})如果出错可以使用签名宏进行检查// define signature checking macro#defineCHECK_SIGNATURE(func,expected)\static_assert(std::is_same_vdecltype(func),expected,Signature mismatch for #func)// check functionCHECK_SIGNATURE(A::xxx,double(A::*)(constA)const);2.2.5 模板函数模板函数的绑定相对其他绑定来说有点麻烦了因为它需要显式实例化模板后再绑定对应函数// instantiate templates heretemplateintA::addint(int,int);templatedoubleA::adddouble(double,double);....def(add,static_castint(A::*)(int,int)(A::add)).def(add,static_castdouble(A::*)(double,double)(A::add))2.2.6 重载的运算符除了上面绑定类方法的两种方式外pybind11还提供非常方便的绑定运算符的方式pybind11支持方式需添加头文件pybind11/operators.h.def(py::selfint())调用C类方式.def(__add__,A::operator)lambda 表达式方式.def(__add__,[](Aself,Aother){returnselfother;})注因为操作符的返回值问题有的是返回类对象本身有的是返回新的对象为跟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有两个友元操作符// cclassA{public:friendinlineAoperator(constAa,constAb);friendinlinebooloperator(constAa,constAb);};可以用pybind11支持直接定义为类的成员.def(py::selfpy::self).def(py::selfpy::self)或者定义为模块级别的操作符m.def(__add__,[](constAa,constAb){returnab;});m.def(__eq__,[](constAa,constAb){returnab;});二、注意事项1. CMakeLists.txt检查事项有其他模块依赖时是否链接了该模块模块名是否一致注意由于windows大小写不敏感所以不要让要生成的python模块与已有模块同名检查想暴露的函数签名是否与预期一致待导出的类如果有用 dllexport 进行标记动态库则在编译python模块库时应将该类标记为 dllimport 因为已经在动态库中导出了另外在链接时记得链接上该类所在的动态库如果该类并不会导出到动态库则应将该类的实现文件也加入python模块库2. 多个类打包到同一个模块上面已经说过一个模块的定义只能有一次所以如果一个模块有较多类需要绑定则可以在各个类的绑定文件中定义绑定该类的函数然后用一个单独的文件定义模块并调用各个类的绑定函数假设有两个类A、B都需要绑定到同一个模块中总绑定文件 core_bindings.cpp#includepybind11/pybind11.hvoidbindA(pybind11::modulem);voidbindB(pybind11::modulem);PYBIND11_MODULE(your_module_name,m){bindA(m);// call binding A class functionbindB(m);// call binding B class function}类绑定文件只需要实现对应绑定函数即可// implement binding A class functionvoidbindA(pybind11::modulem){py::class_A...}// implement binding B class functionvoidbindB(pybind11::modulem){py::class_B...}3. 处理一个类中对另一个类的引用如果另一个类是只在方法内部调用或者是该类的基类但不想暴露它则基本可以不用管他基类在定义时说明一下即可具体看下一小节第一个例子否则另一个类是基类且需要一些处理或是作为参数或返回值类型则该类必须要在本类之前进行绑定直接在前面进行定义如果在一个模块则编写它的绑定文件不在一个模块则添加该模块的编译依赖并且在python中该模块需要在本模块之前进行导入因为需要先有该类的定义4. 绑定派生类而不暴露基类pybind11提供对这样绑定的支持但是它需要在绑定时检查相关类的派生关系这里涉及到访问权限的问题如果基类析构函数是public可声明但是不定义基类然后使基类在python中不可见或者仅声明// declare Apy::class_Abase_class(m,A);// B inherits from Apy::class_B,A(m,B)...// cover Am.attr(A)py::none();以上方式A类在python端不可见且isinstance不可用因为没有A类如果基类析构函数不是public则编译时会提示无法访问 protected 成员可自定义一个基类删除器在声明基类的holder时使用它定义派生类后再隐藏基类// define A class deleterstructADeleter{voidoperator()(A*p)const{// access As deconstructor through Bdeletestatic_castB*(p);}};PYBIND11_MODULE(...){// declare Apy::class_A,std::unique_ptrA,ADeleter(m,A);...m.attr(A)py::none();}或者在声明类的holder时使用py::nodelete直接跳过析构函数检验py::class_A,std::unique_ptrA,py::nodelete(m,A);或者使用自定义跳板类暴露基类析构函数详见下节示例5. 跳板类Helper/Trampoline class的利用跳板类就是我们自己定义继承自C类的类通过重写其中的方法达到改变访问权限、实例化虚基类等目的5.1 暴露 protected/private 成员/方法官方doc - binding protected member functions暴露方法示例classPyA:publicA{public:usingA::foo;// As foo is protected};...py::class_A(m,A).def(foo,PyA::foo);// bind PyAs function暴露析构函数示例classPyA:publicA{public:usingA::A;// using As constructor~PyA()overridedefault;// public deconstructor};PYBIND11_MODULE(...){// declare Apy::class_A,PyA(m,A);...}5.2 绑定抽象基类方法官方doc - overriding virtual functions绑定已经有实现的虚函数可以用上面类方法的方式直接声明基类并用.def(, ...)进行绑定绑定无实现的虚函数/纯虚函数或想要支持 python 重写的函数必须用跳板类方法且用PYBIND11_OVERRIDE(_PURE)声明进行转发调用加_PURE为纯虚函数classPyA:publicA{public:usingA::A;// override pure virtual function in Avoidxxx()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 As function (not PyAs)注1定义跳板类后绑定基类时.def()中仍是写基类的方法注2此方法也适用于绑定工厂函数等需要多态支持的场景m.def(createB,[](){returnnewB();});
返回列表