如何把C/C++程式編譯成Python模組-超實用

王平發表於2019-05-23

在Python遇到效能瓶頸時怎麼辦?答案是找對應功能的C/C++程式,把它編譯成CPython模組,供Python呼叫來提高效能。

如何把C/C++程式編譯成Python模組

比如Python中做科學計算,資料處理的Numpy模組就是使用C語言編寫的,Numpy處理速度比Pandas快數倍。Numpy的處理速度一點都不比go語言差。

本文就是介紹如何把C/C++程式編譯成Python模組。本文偏技術,需要耐著性質看。

Python 作為一個膠水語言,可以很方便的透過C/C++來進行擴充套件,提高效能。前面我寫了一篇文章介紹如何透過Python的ctypes載入普通的.so庫。

其實,這還不算真正的用C/C++寫Python的擴充套件模組。

本文將介紹如何使用C語言和C++寫Python模組。

 

一、Python的C語言介面

Python語言最初是用C語言實現的一種指令碼語言,後來被稱為CPython,是因為後來又有其它語言實現的Python,比如Python實現的Python——PyPy,Java語言實現的Python——Jython,.Net實現的Python——IronPython。

CPython具有優良的開放性和可擴充套件性,並提供了方便靈活的應用程式介面(API),從而使得C/C++程式設計師能夠對Python直譯器的功能進行擴充套件。

Python的C語言介面很適合封裝C語言實現的各種函式,如果要封裝C++的類,使用boost_python或者SWIG更方便和合適,還有一個類似boost_python的支援C++11的pybind11

 

1 模組封裝

假設我們有一個C函式:

/* 檔名:mylib.c */
int addone(int a) {
    return a+1;
}

如果想在Python直譯器中呼叫該函式,則應該首先將其實現為Python中的一個模組,這需要編寫相應的封裝介面,如下所示:


/* 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); /*這裡呼叫C函式 */
  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);
}

上面就是一個典型的Python擴充套件模組,它至少應該包含三個部分:匯出函式、方法列表和初始化函式。

 

2 匯出函式

要在Python直譯器中呼叫C語言中的某個函式,首先要為它編寫對應的匯出函式,上述例子中的匯出函式為wrap_addone。在Python的C語言擴充套件中,所有的匯出函式都具有相同的函式原型:


PyObject* wrap_method(PyObject* self, PyObject* args);

這個函式是Python直譯器和C函式進行互動的介面,一般以wrap_開頭後面跟上C語言的函式名,這樣命名把匯出函式和C語言函式對應起來使得程式碼更加清晰。它帶有兩個引數:self和args。

引數self 只在C函式被實現為內聯方法(built-in method)時才被用到,通常該引數的值為空(NULL)。
引數args 中包含了Python直譯器要傳遞給C函式的所有引數,通常使用Python的C語言擴充套件介面提供的函式PyArg_ParseTuple()來獲得這些引數值。

所有的匯出函式都返回一個PyObject指標,如果對應的C函式沒有真正的返回值(即返回值型別為void),則應返回一個全域性的None物件(Py_None),並將其引用計數增1,如下所示:


PyObject* wrap_method(PyObject *self, PyObject *args)
{
  Py_INCREF(Py_None);
  return Py_None;
}

 

3 方法列表

方法列表中列出了所有可以被Python直譯器使用的方法,上述例子對應的方法列表為:


static PyMethodDef mylibMethods[] =
{
  {"addone", wrap_addone, METH_VARARGS, "Add one to N"},
  {NULL, NULL}
};

方法列表中的每項由四個部分組成:

  • 方法名
  • 匯出函式
  • 引數傳遞方式
  • 方法描述

方法名是從Python直譯器中呼叫該方法時所使用的名字。
引數傳遞方式則規定了Python向C函式傳遞引數的具體形式,可選的兩種方式是METH_VARARGS和METH_KEYWORDS,其中METH_VARARGS是引數傳遞的標準形式,它透過Python的元組在Python直譯器和C函式之間傳遞引數,若採用METH_KEYWORD方式,則Python直譯器和C函式之間將透過Python的字典型別在兩者之間進行引數傳遞。

 

4 初始化函式

所有的Python擴充套件模組都必須要有一個初始化函式,以便Python直譯器能夠對模組進行正確的初始化。Python直譯器規定所有的初始化函式的函式名都必須以init開頭,並加上模組的名字。對於模組mylib來說,則相應的初始化函式為:


void initmylib()
{
  PyObject* m;
  m = Py_InitModule("mylib", mylibMethods);
}

當Python直譯器需要匯入該模組時,將根據該模組的名稱查詢相應的初始化函式,一旦找到則呼叫該函式進行相應的初始化工作,初始化函式則透過呼叫Python的C語言擴充套件介面所提供的函式Py_InitModule(),來向Python直譯器註冊該模組中所有可以用到的方法。

 

5 編譯連結

要在Python直譯器中使用C語言編寫的擴充套件模組,必須將其編譯成動態連結庫的形式。下面以Linux為例,介紹如何將C編寫的Python擴充套件模組編譯成動態連結庫:


$ gcc -fpic -shared -o mylib.so \
             -I/usr/include/python2.7 \
            mylib.c wrap_mylib.c

 

6 在Python中呼叫

上面編譯生成的Python擴充套件模組的動態連結庫,可以在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動態庫和上一篇中不用Python的C語言生成的動態庫是不一樣的,從生成過程和使用方法就可以看出來,這裡的動態庫使用起來感覺就是一個Python模組,直接import就可以了。

 

二、用boost_python庫封裝C++類

安裝boost python庫:


sudo aptitude install libboost-python-dev

示例

下面程式碼簡單實現了一個普通函式maxab()和一個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庫封裝也很簡單,如下程式碼所示:


#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.\n"
       );  

    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的話,會報錯誤:

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的話就要修改為相應的路徑,編譯wrap_student.cpp要指定-fPIC引數,連結(-lboost_python)生成動態庫(-shared)。生成的student.so動態庫就可以被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"])
])

然後執行命令編譯:


python setup.py build
or
sudo python setup.py install

三、SWIG封裝C++類

Python呼叫C/C++程式碼的利器除了boost_python外,還有SWIG(Simplified Wrapper and Interface Generator),它是用來為指令碼語言呼叫C和C++程式的軟體開發工具,它實際上是一個編譯器,獲取C/C++的宣告和定義,用一個殼封裝起來,以便其它指令碼語言訪問這些宣告。所以,SWIG 最大的好處就是將指令碼語言的開發效率和 C/C++ 的執行效率有機的結合起來。

一個雙陣列Trie Tree的實現:cedar在中文分詞、新詞發現等演算法中可以y用於詞典的建立。本文以cedar的SWIG封裝實現來說明SWIG的使用。

 

0. 安裝swig

工欲善其事必先利其器,首先要安裝swig,Ubuntu安裝swig很簡單:


sudo aptitude install swig

 

1. 宣告和定義C/C++程式碼

在cedar的swig目錄下面有cedar的C++宣告和實現程式碼trie.h,但是這個實現裡面沒有遍歷所有key的函式方法,所以我新增了一個實現,首先定義一個資料結構來定義key:


// key-value pair return type for next_key()
class kv_t {
    public:
        std::string key;
        int value;
};

新增一個函式每次返回一個key,當key字串為空時表示遍歷結束,繼續呼叫的話就又從頭開始遍歷:


  // 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的介面檔案的編寫規則:

  1. 首先在 %module  後面宣告模組名稱,這就是Python在import時使用的模組名稱;
  2. 在%{ … %}之間包含相關標頭檔案
  3. 在%include 後面可以宣告對STL的支援
  4. 最後宣告要封裝的函式和變數,也可以之間包含標頭檔案:%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或者單獨執行上面的swig命令,就可以生成cedar.py和cedar_wrap.cxx檔案。

 

4. 編譯生成動態庫

編譯生成的cedar_wrap.cxx使用python distutils的setup,可以參考python/setup.py的編寫。setup.py的build如下:


python setup.py build

就會在當前目錄下面建立目錄build,下面生成lib.linux-x86_64-2.7/cedar.py 和 _cedar.so

 

四、 pybind11封裝C++

從pybind11的名字可以看出,它是用來封裝C++ 11程式碼為Python模組的庫。它的目標和用法都是想Boost_python庫看齊,但是它又比龐大的Boost庫精簡。我知道這個庫的時間不長,也沒有具體實踐過。以前都是寫C++,然後用boost封裝。但是,感覺pybind11更簡潔,所以下一個專案可以試試它。到時候再分享使用心得給大家。

猿人學banner宣傳圖

我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。

***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***

相關文章