如何把C/C++程序編譯成實(shí)用的Python模塊
在Python遇到性能瓶頸時(shí)怎么辦?答案是找對(duì)應(yīng)功能的C/C++程序,把它編譯成CPython模塊,供Python調(diào)用來(lái)提高性能。
如何把C/C++程序編譯成Python模塊
比如Python中做科學(xué)計(jì)算,數(shù)據(jù)處理的Numpy模塊就是使用C語(yǔ)言編寫的,Numpy處理速度比Pandas快數(shù)倍。Numpy的處理速度一點(diǎn)都不比go語(yǔ)言差。
本文就是介紹如何把C/C++程序編譯成Python模塊。本文偏技術(shù),需要耐著性質(zhì)看。
Python 作為一個(gè)膠水語(yǔ)言,可以很方便的通過(guò)C/C++來(lái)進(jìn)行擴(kuò)展,提高性能。前面我寫了一篇文章介紹如何通過(guò)Python的ctypes加載普通的.so庫(kù)。
其實(shí),這還不算真正的用C/C++寫Python的擴(kuò)展模塊。
本文將介紹如何使用C語(yǔ)言和C++寫Python模塊。
一、Python的C語(yǔ)言接口
Python語(yǔ)言最初是用C語(yǔ)言實(shí)現(xiàn)的一種腳本語(yǔ)言,后來(lái)被稱為CPython,是因?yàn)楹髞?lái)它語(yǔ)言實(shí)現(xiàn)的Python,比如Python實(shí)現(xiàn)的Python——PyPy,Java語(yǔ)言實(shí)現(xiàn)的Python——Jython,.Net實(shí)現(xiàn)的Python——IronPython。
CPython具有優(yōu)良的開放性和可擴(kuò)展性,并提供了方便靈活的應(yīng)用程序接口(API),從而使得C/C++程序員能夠?qū)ython解釋器的功能進(jìn)行擴(kuò)展。
Python的C語(yǔ)言接口很適合封裝C語(yǔ)言實(shí)現(xiàn)的各種函數(shù),如果要封裝C++的類,使用boost_python或者SWIG更方便和合適,還有一個(gè)類似boost_python的支持C++11的pybind11。
1 模塊封裝
假設(shè)我們有一個(gè)C函數(shù):
- /* 文件名:mylib.c */
- int addone(int a) {
- return a+1;
- }
如果想在Python解釋器中調(diào)用該函數(shù),則應(yīng)該首先將其實(shí)現(xiàn)為Python中的一個(gè)模塊,這需要編寫相應(yīng)的封裝接口,如下所示:
- /* wrap_mylib.c */
- #include
- #include "mylib.h"
- PyObject* wrap_addone(PyObject* self, PyObject* args)
- {
- int n, result;
- if (! PyArg_ParseTuple(args, "i:fact", &n))
- return NULL;
- result = addone(n); /*這里調(diào)用C函數(shù) */
- return Py_BuildValue("i", result);
- }
- static PyMethodDef mylibMethods[] =
- {
- {"addone", wrap_addone, METH_VARARGS, "Add one to N"},
- {NULL, NULL}
- };
- void initmylib()
- {
- PyObject* m;
- m = Py_InitModule("mylib", mylibMethods);
- }
上面就是一個(gè)典型的Python擴(kuò)展模塊,它至少應(yīng)該包含三個(gè)部分:導(dǎo)出函數(shù)、方法列表和初始化函數(shù)。
2 導(dǎo)出函數(shù)
要在Python解釋器中調(diào)用C語(yǔ)言中的某個(gè)函數(shù),首先要為它編寫對(duì)應(yīng)的導(dǎo)出函數(shù),上述例子中的導(dǎo)出函數(shù)為wrap_addone。在Python的C語(yǔ)言擴(kuò)展中,所有的導(dǎo)出函數(shù)都具有相同的函數(shù)原型:
- PyObject* wrap_method(PyObject* self, PyObject* args);
這個(gè)函數(shù)是Python解釋器和C函數(shù)進(jìn)行交互的接口,一般以wrap_開頭后面跟上C語(yǔ)言的函數(shù)名,這樣命名把導(dǎo)出函數(shù)和C語(yǔ)言函數(shù)對(duì)應(yīng)起來(lái)使得代碼更加清晰。它帶有兩個(gè)參數(shù):self和args。
參數(shù)self 只在C函數(shù)被實(shí)現(xiàn)為內(nèi)聯(lián)方法(built-in method)時(shí)才被用到,通常該參數(shù)的值為空(NULL)。
參數(shù)args 中包含了Python解釋器要傳遞給C函數(shù)的所有參數(shù),通常使用Python的C語(yǔ)言擴(kuò)展接口提供的函數(shù)PyArg_ParseTuple()來(lái)獲得這些參數(shù)值。
所有的導(dǎo)出函數(shù)都返回一個(gè)PyObject指針,如果對(duì)應(yīng)的C函數(shù)沒(méi)有真正的返回值(即返回值類型為void),則應(yīng)返回一個(gè)全局的None對(duì)象(Py_None),并將其引用計(jì)數(shù)增1,如下所示:
- PyObject* wrap_method(PyObject *self, PyObject *args)
- {
- Py_INCREF(Py_None);
- return Py_None;
- }
3 方法列表
方法列表中列出了所有可以被Python解釋器使用的方法,上述例子對(duì)應(yīng)的方法列表為:
- static PyMethodDef mylibMethods[] =
- {
- {"addone", wrap_addone, METH_VARARGS, "Add one to N"},
- {NULL, NULL}
- };
方法列表中的每項(xiàng)由四個(gè)部分組成:
方法名
導(dǎo)出函數(shù)
參數(shù)傳遞方式
方法描述
方法名是從Python解釋器中調(diào)用該方法時(shí)所使用的名字。
參數(shù)傳遞方式則規(guī)定了Python向C函數(shù)傳遞參數(shù)的具體形式,可選的兩種方式是METH_VARARGS和METH_KEYWORDS,其中METH_VARARGS是參數(shù)傳遞的標(biāo)準(zhǔn)形式,它通過(guò)Python的元組在Python解釋器和C函數(shù)之間傳遞參數(shù),若采用METH_KEYWORD方式,則Python解釋器和C函數(shù)之間將通過(guò)Python的字典類型在兩者之間進(jìn)行參數(shù)傳遞。
4 初始化函數(shù)
所有的Python擴(kuò)展模塊都必須要有一個(gè)初始化函數(shù),以便Python解釋器能夠?qū)δK進(jìn)行正確的初始化。Python解釋器規(guī)定所有的初始化函數(shù)的函數(shù)名都必須以init開頭,并加上模塊的名字。對(duì)于模塊mylib來(lái)說(shuō),則相應(yīng)的初始化函數(shù)為:
- void initmylib()
- {
- PyObject* m;
- m = Py_InitModule("mylib", mylibMethods);
- }
當(dāng)Python解釋器需要導(dǎo)入該模塊時(shí),將根據(jù)該模塊的名稱查找相應(yīng)的初始化函數(shù),一旦找到則調(diào)用該函數(shù)進(jìn)行相應(yīng)的初始化工作,初始化函數(shù)則通過(guò)調(diào)用Python的C語(yǔ)言擴(kuò)展接口所提供的函數(shù)Py_InitModule(),來(lái)向Python解釋器注冊(cè)該模塊中所有可以用到的方法。
5 編譯鏈接
要在Python解釋器中使用C語(yǔ)言編寫的擴(kuò)展模塊,必須將其編譯成動(dòng)態(tài)鏈接庫(kù)的形式。下面以Linux為例,介紹如何將C編寫的Python擴(kuò)展模塊編譯成動(dòng)態(tài)鏈接庫(kù):
- $ gcc -fpic -shared -o mylib.so
- -I/usr/include/python2.7
- mylib.c wrap_mylib.c
6 在Python中調(diào)用
上面編譯生成的Python擴(kuò)展模塊的動(dòng)態(tài)鏈接庫(kù),可以在Python中直接import。如下所示:
- veelion@gtx:~$ python
- Python 2.7.12 (default, Nov 19 2016, 06:48:10)
- [GCC 5.4.0 20160609] on linux2
- Type "help", "copyright", "credits" or "license" for more information.
- >>> import example
- >>> example.addone(7)
- 8
- >>>
- >>>
這里生成的.so動(dòng)態(tài)庫(kù)和上一篇中不用Python的C語(yǔ)言生成的動(dòng)態(tài)庫(kù)是不一樣的,從生成過(guò)程和使用方法就可以看出來(lái),這里的動(dòng)態(tài)庫(kù)使用起來(lái)感覺(jué)就是一個(gè)Python模塊,直接import就可以了。
二、用boost_python庫(kù)封裝C++類
安裝boost python庫(kù):
- sudo aptitude install libboost-python-dev
示例
下面代碼簡(jiǎn)單實(shí)現(xiàn)了一個(gè)普通函數(shù)maxab()和一個(gè)Student類:
- #include
- #include
- int maxab(int a, int b) { return a>b?a:b; }
- class Student {
- private:
- int age;
- std::string name;
- public:
- Student() {}
- Student(std::string const& _name, int _age) { name=_name; age=_age; }
- static void myrole() { std::cout << "I'm a student!" << std::endl; }
- void whoami() { std::cout << "I am " << name << std::endl; }
- bool operator==(Student const& s) const { return age == s.age; }
- bool operator!=(Student const& s) const { return age != s.age; }
- };
使用boost.python庫(kù)封裝也很簡(jiǎn)單,如下代碼所示:
- #include
- #include <boost/python.hpp>
- #include <boost/python/suite/indexing/vector_indexing_suite.hpp>
- #include
- #include "student.h"
- using namespace boost::python;
- BOOST_PYTHON_MODULE(student) {
- // This will enable user-defined docstrings and python signatures,
- // while disabling the C++ signatures
- scope().attr("__version__") = "1.0.0";
- scope().attr("__doc__") = "a demo module to use boost_python.";
- docstring_options local_docstring_options(true, false, false);
- def(
- "maxab", &maxab, "return max of two numbers.
- "
- );
- class_("Student", "a class of student")
- .def(init<>())
- .def(init<std::string, int>())
- // methods for Chinese word segmentation
- .def(
- "whoami", &Student::whoami, "method's doc string..."
- )
- .def(
- "myrole", &Student::myrole, "method's doc string..."
- )
- .staticmethod("myrole");
- // 封裝STL
- class_<std::vector >("StudentVec")
- .def(vector_indexing_suite<std::vector >())
- ;
- }
上述代碼還是include了Python.h文件,如果不include的話,會(huì)報(bào)錯(cuò)誤:
- wrap_python.hpp:50:23: fatal error: pyconfig.h: No such file or directory
編譯
編譯以上代碼有兩種方式,一種是在命令行下面直接使用g++編譯:
- g++ -I/usr/include/python2.7 -fPIC wrap_student.cpp -lboost_python -shared -o student.so
首先指定Python.h的路徑,如果是Python 3的話就要修改為相應(yīng)的路徑,編譯wrap_student.cpp要指定-fPIC參數(shù),鏈接(-lboost_python)生成動(dòng)態(tài)庫(kù)(-shared)。生成的student.so動(dòng)態(tài)庫(kù)就可以被python直接import使用了
- In [1]: import student
- In [2]: student.maxab(2, 5)
- Out[2]: 5
- In [3]: s = student.Student('Tom', 12)
- In [4]: s.whoami()
- I am Tom
- In [5]: s.myrole()
- I'm a student!
另外一直方法是用python的setuptools編寫setup.py腳本:
- #!/usr/bin/env python
- from setuptools import setup, Extension
- setup(name="student",
- ext_modules=[
- Extension("student", ["wrap_student.cpp"],
- libraries = ["boost_python"])
- ])
然后執(zhí)行命令編譯:
- python setup.py build
- or
- sudo python setup.py install
三、SWIG封裝C++類
Python調(diào)用C/C++代碼的利器除了boost_python外,還有SWIG(Simplified Wrapper and Interface Generator),它是用來(lái)為腳本語(yǔ)言調(diào)用C和C++程序的軟件開發(fā)工具,它實(shí)際上是一個(gè)編譯器,獲取C/C++的聲明和定義,用一個(gè)殼封裝起來(lái),以便其它腳本語(yǔ)言訪問(wèn)這些聲明。所以,SWIG 最大的好處就是將腳本語(yǔ)言的開發(fā)效率和 C/C++ 的運(yùn)行效率有機(jī)的結(jié)合起來(lái)。
一個(gè)雙數(shù)組Trie Tree的實(shí)現(xiàn):cedar在中文分詞、新詞發(fā)現(xiàn)等算法中可以y用于詞典的創(chuàng)建。本文以cedar的SWIG封裝實(shí)現(xiàn)來(lái)說(shuō)明SWIG的使用。
0. 安裝swig
工欲善其事必先利其器,首先要安裝swig,Ubuntu安裝swig很簡(jiǎn)單:
- sudo aptitude install swig
1. 聲明和定義C/C++代碼
在cedar的swig目錄下面有cedar的C++聲明和實(shí)現(xiàn)代碼trie.h,但是這個(gè)實(shí)現(xiàn)里面沒(méi)有遍歷所有key的函數(shù)方法,所以我添加了一個(gè)實(shí)現(xiàn),首先定義一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)定義key:
- // key-value pair return type for next_key()
- class kv_t {
- public:
- std::string key;
- int value;
- };
添加一個(gè)函數(shù)每次返回一個(gè)key,當(dāng)key字符串為空時(shí)表示遍歷結(jié)束,繼續(xù)調(diào)用的話就又從頭開始遍歷:
- // to iterate all keys
- kv_t next_key() const {
- static size_t from = 0, p = 0;
- union { int i; int x; } b;
- char key[256] = {0};
- kv_t kv;
- if(from == 0) {
- b.i = _t->begin(from, p);
- }else{
- b.i = _t->next(from, p);
- }
- if (b.i == trie_t::CEDAR_NO_PATH) {
- kv.key = "";
- kv.value = 0;
- from = 0;
- p = 0;
- return kv;
- }
- _t->suffix(key, p, from);
- kv.key = key;
- kv.value = b.x;
- return kv;
- }
2. 編寫接口文件.i
查看cedar.i可以看到SWIG的接口文件的編寫規(guī)則:
首先在 %module 后面聲明模塊名稱,這就是Python在import時(shí)使用的模塊名稱;
在%{ … %}之間包含相關(guān)頭文件
在%include 后面可以聲明對(duì)STL的支持
最后聲明要封裝的函數(shù)和變量,也可以之間包含頭文件:%include “trie.h”
3. 封裝代碼
可以在Makefile里面看到python-bindings:
- python-bindings:
- swig -Wall -python -builtin -outdir python -c++ cedar.i
- mv -f cedar_wrap.cxx python
直接make或者單獨(dú)運(yùn)行上面的swig命令,就可以生成cedar.py和cedar_wrap.cxx文件。
4. 編譯生成動(dòng)態(tài)庫(kù)
編譯生成的cedar_wrap.cxx使用python distutils的setup,可以參考python/setup.py的編寫。setup.py的build如下:
- python setup.py build
就會(huì)在當(dāng)前目錄下面創(chuàng)建目錄build,下面生成lib.linux-x86_64-2.7/cedar.py 和 _cedar.so
四、 pybind11封裝C++
從pybind11的名字可以看出,它是用來(lái)封裝C++ 11代碼為Python模塊的庫(kù)。它的目標(biāo)和用法都是想Boost_python庫(kù)看齊,但是它又比龐大的Boost庫(kù)精簡(jiǎn)。我知道這個(gè)庫(kù)的時(shí)間不長(zhǎng),也沒(méi)有具體實(shí)踐過(guò)。以前都是寫C++,然后用boost封裝。但是,感覺(jué)pybind11更簡(jiǎn)潔,所以下一個(gè)項(xiàng)目可以試試它。到時(shí)候再分享使用心得給大家。