C++擴展Python與Swig工具

python作為一種通用的編程語言,一般而言,是能夠滿足邏輯實現的需求的。只是在日常使用過程中,除了實現一些邏輯之外,

至少還有兩個方面的需求是可能需要尋求其他語言幫助的,第一個是提升運行效率,第二個是複用已有C/C++代碼。python比較接近自然語言這一特性確實對使用者而言很不錯,但是這帶來了一個不良後果--運行速度慢。有時還需要藉助多進程的方式提升運算速度,但是無論如何,python本身速度慢這件事確實改變不了,也就意味著硬件資源的利用率低。而C/C++的運行速度上的優勢讓我們比較願意尋求某種方式在python中調用C/C++的程序。此外,如果並不是白手起家,而是已有大量C/C++代碼實現的核心功能,那麼使用python重新實現一遍是不划算的。這時將這些C/C++代碼編譯成python可調用的庫將非常具有吸引力。本文將使用三種方式將一個簡單的C程序邏輯封裝成python中可調用的形式,以展示python使用C/C++擴展功能的一般方式。分別是:動態共享庫調用、C擴展以及swig工具。本文的所有演示在MacOS 10.14.6操作系統中完成,Linux操作系統下應該比較容易復現,Windows可能會比較麻煩一點。

1. 動態共享庫調用

將C/C++代碼編譯成動態共享庫然後在python中調用是一種比較直觀的方式。在MacOS和Linux中是生成後綴為so的文件,在windows系統成是生成後綴為dll的文件。

第一步:

首先展示一下這個簡單功能的C程序代碼。

閒話python 48: C/C++擴展Python與Swig工具

核心功能函數

第二步:

這裡,我們使用gcc編譯器將源代碼編譯成動態共享庫。查看編譯產出,發現按照預期生成了so文件。

閒話python 48: C/C++擴展Python與Swig工具

編譯動態共享庫

第三步:

接著,就可以在python中加載生成的so文件,然後調用對應的功能函數。不過還是有一點需要說明,即數據類型。使用過C/C++的同學都知道,C/C++是強類型的靜態語言,也就是說在執行功能函數的時候,常常必須要保證傳參的數據類型完全一致,否則程序容易崩潰。python提供了一個庫ctypes來解決調用C/C++程序時的類型問題。通常,我們需要使用ctypes中提供的數據類型和轉換接口,將python中的數據轉換成C/C++程序中的數據類型,然後調用對應的功能函數。複雜的函數參數對於這種調用方式而言是災難性的,因此這種方式一般適用於python與C/C++交互接口簡單的情形。

閒話python 48: C/C++擴展Python與Swig工具

python調用動態共享庫

2. C擴展

為了解決上文中調用動態共享庫所面臨的問題,還可以使用C擴展的方式完成在python中調用C/C++程序的需求。這種方式使用了python本身提供的一組C/C++語言交互接口API,這樣所做成的擴展程序並不需要像動態共享庫那樣顯式加載文件,而是像一般的python模塊那樣導入即可。編程風格更加pythonic,能夠實現的交互接口也更加複雜。

第一步:

下面就展示一下使用C擴展的方式需要提供的C程序源代碼。

閒話python 48: C/C++擴展Python與Swig工具

C擴展源碼

第二步:

C代碼中除了本次演示功能的核心之外,最主要的就是定義調用接口。正是由於在C代碼中實現了一個複雜解析過程,python中調用過程就可以非常簡單,與一般的python程序調用毫無差別。 C代碼編寫完成後還需要編寫一個對應的setup.py文件,以便於編譯。下面展示這個setup.py文件。

閒話python 48: C/C++擴展Python與Swig工具

setup.py編譯描述文件

第三步:

C代碼和setup.py文件準備完畢之後就可以進行編譯了。下面展示編譯的指令,並顯示編譯產出的文件。

閒話python 48: C/C++擴展Python與Swig工具

編譯C擴展

第四步:

可以看到,編譯產出順利生成,接下來就可以在python中調用了。由於C擴展的so文件並不在當前的搜索目錄下,因此需要修改一下sys.path這個變量。除了執行核心的功能函數之外,還可以查看模塊的文檔,如果需要也可以設置對應的版本號。這樣就與一般的python模塊一致了,用起來也比較順手。

閒話python 48: C/C++擴展Python與Swig工具

python調用C擴展

3. Swig工具

從上文的C擴展可以看出,C/C++的源碼中需要包含python交互的解析,因此顯得非常複雜。而這個過程似乎是形式化的,那麼有沒有什麼工具可以輔助完成這個過程呢?這就是這裡提到的swig工具。當然,好可以用boost中的wrap接口,但是boost本身比較沉重,在發佈和共享時不太方便。

第一步:

下面我們先看一下實現所需功能的C/C++源代碼。

閒話python 48: C/C++擴展Python與Swig工具

功能源碼文件內容

第二步:

然後,需要定義一個後綴為i的描述文件,用於swig工具處理源代碼。由於本文所演示的功能中需要使用int類型的數組,所以在描述文件中也添加了一個相關的定義,就可以在python中定義int數組,便於調用。

閒話python 48: C/C++擴展Python與Swig工具

swig描述文件

第三步:

使用swig指令調用上面所編寫的描述文件,就可以生成針對源碼的wrap接口代碼。從以下的演示可以看出,這條指令生成了swig_example.py和swig_example_wrap.c兩個文件。

閒話python 48: C/C++擴展Python與Swig工具

swig生成接口代碼

第四步:

接下來的步驟就跟上文中的C擴展步驟一致了,編寫setup.py文件,然後編譯。下面展示一個詳細一點的setup.py文件,其中可以指定很多與模塊相關的信息,便於形成標準的python第三方庫。

閒話python 48: C/C++擴展Python與Swig工具

setup.py編譯描述文件

第五步:

編譯生成對應的so文件之後,就可以在python程序中調用了。

閒話python 48: C/C++擴展Python與Swig工具

編譯swig產生的接口和源碼文件

第六步:

同樣的,需要先設置以下搜索路徑,以便於找到該模塊。這裡遇到的一個問題是需要保持python調用C/C++函數的傳入參數與程序中定義的參數類型一致。由於本文所演示的例子中使用了int類型的指針,那麼在描述文件中定義的intp就派上了用場。下面演示了參數轉換和調用的過程。

從這個調用來看,似乎並不簡單,其原因是參數類型的轉換代碼較多,而且還是隻能服務於單一的數據類型轉換。但這並不是一個大問題。通常在設計C/C++程序時,會提供大量簡單易用的類型轉換的接口,在python中也會封裝一些接口,從而避免在實際調用過程中頻繁編寫代碼進行轉換。在封裝完類型轉換接口之後,其調用形式與C/C++源碼中定義的是一致的,因此我們並不需要專門地閱讀swig所生成的代碼,只需要掌握一點swig描述配置即可。

閒話python 48: C/C++擴展Python與Swig工具

調用swig方式的C擴展

到此,使用C/C++擴展python就討論完畢。上面所描述的三種方法各有利弊,一般而言,交互接口簡單的就直接使用動態共享庫完成,如果調用接口比較複雜那麼可以使用C擴展的方式完成,如果需要封裝的C代碼比較複雜,那麼可以考慮使用swig工具實現,然後配合一些類型轉換接口。從實際的開發經驗來看,擴展python還是屬於比較高階的用法,一般的日常開發中是很少用到的。在圖像處理領域,由於過去曾經積累了大量的C/C++代碼,而且對計算效率的要求比較高,相對而言可能需要使用擴展python的地方稍多一點。本文的notebook版本文件在github的cnbluegeek/notebook倉庫中共享,歡迎感興趣的朋友前往下載。


分享到:


相關文章: