boost.python筆記
標(biāo)簽:boost.python
簡(jiǎn)介
Boost.python是什么?
它是boost庫(kù)的一部分,隨boost一起安裝,用來實(shí)現(xiàn)C++和Python代碼的交互。
使用Boost.python有什么特點(diǎn)?
不需要修改原有的C++代碼,支持比較豐富的C++特性。不會(huì)生成額外的python代碼(像SWIG那樣),但是需要寫一部分C++的封裝代碼。
我只用到了其功能的一部分,把C/C++實(shí)現(xiàn)的功能封裝為可供python直接調(diào)用的.so庫(kù)。具體場(chǎng)景是,有一個(gè)C++模塊通過thrift封裝為RPC,python代碼通過PRC調(diào)用請(qǐng)求服務(wù)。由于調(diào)用頻次較多,RPC調(diào)用開銷成為一個(gè)很耗時(shí)的部分,因此想直接通過python對(duì)原模塊功能進(jìn)行調(diào)用。
以前了解SWIG可以實(shí)現(xiàn)這個(gè)需求。本已開始看SWIG的文檔,但突然又想看一下還有沒有別的方法,于是在stack overflow上搜了一些問題,發(fā)現(xiàn)不少人推薦Boost.python,于是打算拿它來試一試。
如何使用Boost.python
首先,前提是安裝了開發(fā)環(huán)境。
- 安裝了boost開發(fā)環(huán)境。安裝了頭文件和動(dòng)態(tài)庫(kù)。
- 安裝了python開發(fā)環(huán)境。安裝了頭文件和動(dòng)態(tài)庫(kù)。
然后,就是寫代碼了,這是個(gè)沒辦法避免的事情!??!
需要自己動(dòng)手的有兩個(gè)地方。一個(gè)是xxxxxx_wrapper.cpp文件,文件名無所謂,其核心目的是定義導(dǎo)出的python模塊的名稱,以及需要導(dǎo)出的類、函數(shù)等。另一個(gè)是需要修改一下你的Makefile,來編譯、鏈接這個(gè)so庫(kù)。
先來看一下xxxxxx_wrapper.cpp。一般情況下,它的內(nèi)容跟下面的代碼比較接近。
#include <boost/python.hpp>
// 其他需要包含的頭文件,與具體業(yè)務(wù)有關(guān)
namespace py = boost::python;
// 其他函數(shù),可能包括一些用于類型轉(zhuǎn)換和封裝的
BOOST_PYTHON_MODULE(my_module_name)
{
// 導(dǎo)出普通函數(shù)
def("fun_name_in_python", &fun_name_in_c);
// 導(dǎo)出類及部分成員
class_<ClassNameInCpp>("ClassNameInPython", init<std::string>()) //類名,默認(rèn)構(gòu)造函數(shù)
.def(init<double>()) //其他構(gòu)造函數(shù)
.def("memberFunNameInPython", &ClassNameInCpp::memberFunNameInCpp) //成員函數(shù)
.def_readwrite("dataMemberInPython", &ClassNameInCpp::dataMemberInCpp) //普通成員變量
.def_readonly("dataMemberInPython_2", &ClassNameInCpp::dataMemberInCpp_2) //只讀成員變量
;
}
這個(gè)文件的核心目的體現(xiàn)在BOOST_PYTHON_MODULE里,定義需要導(dǎo)出給python的東西。
在Makefile里,需要增加一條用來編譯導(dǎo)出的.so的規(guī)則,編譯命令里通用的部分一般像下面這樣,這里把編譯和鏈接寫在一起了。
g++ -o my_module_name.so -shared -fPIC -I${BOOST_INCLUDE_PATH} -I${PYTHON_INCLUDE_PATH} -L${BOOST_LIB_DIR} -lboost_python ${MY_SRC_FILES}
編譯及鏈接參數(shù)的作用如下,其他參數(shù)由具體項(xiàng)目的業(yè)務(wù)邏輯決定:
-o my_module_name.so,這里的模塊名需要和xxxxxx_wrapper.cpp文件里BOOST_PYTHON_MODULE(my_module_name)一致;-I${BOOST_INCLUDE_PATH} -I${PYTHON_INCLUDE_PATH}是編譯需要的;-L${BOOST_LIB_DIR} -lboost_python -shared -fPIC是鏈接需要的;${MY_SRC_FILES}包含了xxxxxxx_wrapper.cpp以及業(yè)務(wù)邏輯需要的其他.cpp,.c文件;
至此,我們便有了my_module_name.so這個(gè)可以被python調(diào)用的模塊了。測(cè)試一下吧。
>>>import my_moduel_name
>>>help(my_module_name)
可以看到被導(dǎo)出的類及函數(shù),然后可以按照python的習(xí)慣來使用這些類和函數(shù)了。
如何寫wrapper
以一個(gè)實(shí)例為框架來解釋吧,內(nèi)容包括普通函數(shù)、類、數(shù)據(jù)成員、成員函數(shù)、通過參數(shù)傳遞結(jié)果、容器。其他特性沒有用到,也沒有測(cè)試。
先來看一下業(yè)務(wù)邏輯的代碼。包含一個(gè)類,一個(gè)以類對(duì)象為參數(shù)的函數(shù),一個(gè)通過引用修改類對(duì)象的函數(shù)。
//test_class.h
// 定義一個(gè)類
class A
{
public:
A(){privateVal=0;} //默認(rèn)構(gòu)造函數(shù)
A(int val){privateVal=val;} //帶參數(shù)的構(gòu)造函數(shù)
void set(int val){privateVal=val;} //成員函數(shù)
int get() const {return privateVal;}; //成員函數(shù)
int publicVal; //公共數(shù)據(jù)成員
private:
int privateVal; //私有數(shù)據(jù)成員
};
int addA(A &a, int addVal); //普通函數(shù),有返回值,通過引用修改參數(shù)
void printA(const A& a);
//test_class.cpp
#include <stdio.h>
#include "test_class.h"
int addA(A &a, int addVal)
{
int val = a.get();
val += addVal;
a.set(val);
return val;
}
void printA(const A& a)
{
printf("%d\n", a.get());
}
然后是wrapper.cpp文件,這里實(shí)際名為test_class_wrapper.cpp。
//test_class_wrapper.cpp
#include <boost/python.hpp>
#include "test_class.h"
BOOST_PYTHON_MODULE(test_class)
{
using namespace boost::python;
// 導(dǎo)出類
class_<A>("A", init<>()) //如果默認(rèn)構(gòu)造函數(shù)沒有參數(shù),可以省略
.def(init<int>()) //其他構(gòu)造函數(shù)
.def("get", &A::get) //成員函數(shù)
.def("set", &A::set) //成員函數(shù)
.def_readwrite("publicVal", &A::publicVal) //數(shù)據(jù)成員,當(dāng)然是公共的
;
def("printA", &printA);
def("addA", &addA);
}
通過python命令行測(cè)試一下
>>>import test_class
>>>a = test_class.A(5)
>>>ret = addA(a, 10)
>>>print ret
15
>>>print a.get()
15
到目前為止,整個(gè)過程都很順利。需要額外寫的代碼很少,也很規(guī)整,與某種IDL的寫法接近,只需要“聲明”一下,剩下的事情都交給編譯器及庫(kù)完成。但有的時(shí)候,這個(gè)過程就不這么順利了,我們需要額外寫一些轉(zhuǎn)換及封裝。比如在這一部分最開始提到的容器,上面的代碼就沒有涉及。
下面的代碼,我們對(duì)上面的例子做了一些擴(kuò)展。第一,對(duì)類A增加了一個(gè)vector成員,需要在python代碼里引用該成員;第二,增加了一個(gè)函數(shù),以vector為參數(shù),需要在python代碼里直接調(diào)用該函數(shù)。下面我們就來解釋與容器有關(guān)的導(dǎo)出。
#include<vector>
class B;
class A
{
public:
A(){privateVal=0;} //默認(rèn)構(gòu)造函數(shù)
A(int val){privateVal=val;} //帶參數(shù)的構(gòu)造函數(shù)
void set(int val){privateVal=val;} //成員函數(shù)
int get() const {return privateVal;}; //成員函數(shù)
int publicVal; //公共數(shù)據(jù)成員
std::vector<B> m_vB;
private:
int privateVal; //私有數(shù)據(jù)成員
};
class B
{
public:
B(){}
~B(){}
int pos;
int len;
};
int accumulate(const std::vector<A>& v_A);
int addA(A &a, int addVal); //普通函數(shù),有返回值,通過引用修改參數(shù)
void printA(const A& a);
#include <stdio.h>
#include "test_class.h"
int addA(A &a, int addVal)
{
int val = a.get();
val += addVal;
a.set(val);
return val;
}
void printA(const A& a)
{
printf("%d\n", a.get());
}
int accumulate(const std::vector<A>& v_A)
{
int ret = 0;
for (size_t i = 0; i < v_A.size(); i++)
{
ret += v_A[i].get();
}
return ret;
}
首先,需要明白一點(diǎn),c++中的vector不等于python中的list,雖然它們看上去比較相似。Boost.python中有與python的list對(duì)應(yīng)的東西,是boost::python::list,如果在python代碼里以list為參數(shù)調(diào)用某個(gè)方法,則在c++代碼中這個(gè)參數(shù)被自動(dòng)映射為boost::python::list,不是vector。既然這樣,如果我們不打算修改原有的C++代碼,又想調(diào)用以vector為參數(shù)的函數(shù),該怎么辦呢?
目前我了解的方法由兩種:
- 在C++代碼里對(duì)以vector為參數(shù)的函數(shù)進(jìn)行一層封裝,封裝為以boost::python::list為參數(shù)的函數(shù),導(dǎo)出封裝后的函數(shù)。在函數(shù)里通過
boost::python::extract_<T>對(duì)list里的所有成員進(jìn)行提取,將其由boost::python::object對(duì)象變?yōu)門類型的對(duì)象,然后存于vector<T>中,再調(diào)用以vector<T>為參數(shù)的函數(shù)。
如果需要返回list或者原函數(shù)對(duì)vector參數(shù)的內(nèi)容作了修改,需要再將調(diào)用函數(shù)后的vector內(nèi)的每個(gè)元素放回list里。- 直接導(dǎo)出
vector<T>類型。此時(shí)vector<T>本身作為一個(gè)類型被導(dǎo)出給python代碼,與普通的類具有同等地位。但是,與普通類不同的是,它通過模板vector_indexing_suite<std::vector<T> >()導(dǎo)出,自動(dòng)實(shí)現(xiàn)了append,slice,__len__等方法,在python里可以像使用list那樣操作這個(gè)被導(dǎo)出的vector類。而且,以vector<T>為參數(shù)的函數(shù),在通過def導(dǎo)出給python時(shí),其參數(shù)會(huì)被自動(dòng)映射為vector_indexing_suite<std::vector<T> >。同理,python代碼傳入的通過vector_indexing_suite導(dǎo)出的容器對(duì)象,也會(huì)在c++代碼里被自動(dòng)轉(zhuǎn)換為vector,這里無需顯式地寫轉(zhuǎn)換函數(shù)。
下面的代碼是wrapper文件,使用第二種方法,即直接導(dǎo)出vector類型。
為什么這么做呢?因?yàn)锳里有個(gè)vector成員,要導(dǎo)出這個(gè)成員,必須導(dǎo)出這個(gè)vector<B>這個(gè)類型,否則還需要對(duì)類A再做一層封裝,讓它包含一個(gè)boost::python::list成員,這就太麻煩了。
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
#include "test_class.h"
bool operator==(const B& left, const B& right);
bool operator==(const A& left, const A& right)
{
if (left.get() != right.get() || left.publicVal != right.publicVal)
return false;
if (left.m_vB.size() != right.m_vB.size())
return false;
for (size_t i = 0; i < left.m_vB.size(); i ++)
{
if (!(left.m_vB[i] == right.m_vB[i]))
return false;
}
return true;
}
bool operator==(const B& left, const B& right)
{
return (left.pos == right.pos && left.len == right.len);
}
BOOST_PYTHON_MODULE(test_class)
{
using namespace boost::python;
class_<A>("A", init<>()) //如果默認(rèn)構(gòu)造函數(shù)沒有參數(shù),可以省略
.def(init<int>()) //其他構(gòu)造函數(shù)
.def("get", &A::get) //成員函數(shù)
.def("set", &A::set) //成員函數(shù)
.def_readwrite("publicVal", &A::publicVal) //數(shù)據(jù)成員,當(dāng)然是公共的
.def_readwrite("vB", &A::m_vB)
;
class_<std::vector<A> >("VecA")
.def(vector_indexing_suite<std::vector<A> >())
;
class_<B>("B")
.def_readwrite("pos", &B::pos)
.def_readwrite("len", &B::len)
;
class_<std::vector<B> >("VecB")
.def(vector_indexing_suite<std::vector<B> >())
;
def("printA", &printA);
def("addA", &addA);
def("accumulate", &accumulate);
}
我們還是從BOOST_PYTHON_MODULE內(nèi)的代碼開始看。
class_<A>的定義看上去和前面的例子沒有太大差別,只是多導(dǎo)出了一個(gè)成員.def_readwrite("vB", &A::m_vB)。即使這個(gè)成員變量是vector<B>類型的,在這里也不需要特殊對(duì)待;class_<B>就是導(dǎo)出一個(gè)類,包含兩個(gè)共有數(shù)據(jù)成員。這里也沒什么特別的;class_<std::vector<A> >("VecA")和class_<std::vector<B> >("VecB")是本例的重點(diǎn),導(dǎo)出了兩個(gè)不同的vector類型,因?yàn)樵赾++里,vector是一個(gè)類模板,vector<A>和vector<B>才是兩個(gè)具體的類型;printA,addA,accumulate是三個(gè)導(dǎo)出的函數(shù)。即使其參數(shù)是vector<A>類型,也無需特別對(duì)待;
除此之外,注意到為類型A和類型B定義了==操作符,這是boost.python在導(dǎo)出某種類型的vector時(shí)需要的,在內(nèi)部某個(gè)地方用到了==操作符。如果僅導(dǎo)出類型,不導(dǎo)出類型的向量,是不需要==操作符的,如前面的例子所示。
編譯鏈接后,通過python命令行測(cè)試一下:
>>>import test_class
>>>a1 = test_class.A()
>>>b1 = test_class.B() # 實(shí)例化一個(gè)B
>>>b1.pos = 1
>>>b1.len = 1
>>>b2 = test_class.B() # 實(shí)例化另一個(gè)B
>>>b2.pos = 2
>>>b2.len = 2
>>>a1.vB.append(b1) # a1.vB是vector<B>在python中對(duì)應(yīng)類型的對(duì)象,接口類似list,但只能添加B類型的對(duì)象
>>>a1.vB.append(b2)
>>>print a1.vB[-1].len # a1.vB支持list的下標(biāo)引用
2
>>>a1.set(1)
>>>a2 = test_class.A(2)
>>>a3 = test_class.A(3)
>>>vA = test_class.VecA() # vector<A>在python中對(duì)應(yīng)的類型
>>>vA.append(a1)
>>>vA.append(a2)
>>>vA.append(a3)
>>>print accumulate(vA) # 調(diào)用以vector<A>為參數(shù)的函數(shù)
6
對(duì)于導(dǎo)出的python模塊來說,一切在python中會(huì)被引用到的變量,其所屬類型(基本數(shù)據(jù)類型除外)都需要被明確導(dǎo)出,也就是都需要在BOOST_PYTHON_MODULE里被定義。如本例中的vector<B>,盡管沒有函數(shù)以該類型為參數(shù),但如果想要在python代碼里引用A的成員vB,就需要導(dǎo)出它,否則會(huì)拋異常。相反,如果不需要在python代碼里引用這個(gè)成員,則不需要導(dǎo)出vector<B>這個(gè)類型,而且在class_<A>的定義中也應(yīng)把.def_readwrite("vB", &A::m_vB)去掉。如果不導(dǎo)出vector<B>類型,但在class_<A>的定義中通過.def_readwrite("vB", &A::m_vB)導(dǎo)出了該成員,編譯不會(huì)出問題,使用python模塊也不會(huì)出問題,但只要代碼引用到A.vB就會(huì)拋異常,相當(dāng)于埋了一個(gè)坑。
從實(shí)用的角度看,這樣一個(gè)流程可能會(huì)比較有效。首先,確定需要導(dǎo)出的函數(shù)及類型。然后檢查函數(shù)(包括成員函數(shù))參數(shù)及返回值的類型,非基本類型需要被導(dǎo)出;檢查導(dǎo)出的類成員變量,如果不是基本類型,其類型也要導(dǎo)出。如此直到?jīng)]有新的類型需要被添加為止。
總結(jié)
Boost.python的文檔感覺比較少,很多問題和trick都是在stack overflow上看到然后再試驗(yàn)的。據(jù)了解,Boost.python支持更為豐富的c++特性,這里只用到了一小部分。