近幾天使用 python 與 c/c++ 程序交互,網上有推薦swig但效果都不理想,所以琢磨琢磨了 python 的 ctypes 模塊。同時,雖然網上有這方面的內容,但是感覺還是沒說清楚。這里記錄下來做備用,同時也給廣大 python with c/c++ 派留給方便。如果你覺得我寫的不好,可以參考官方文檔里對 ctypes 的介紹,那里說不一定有你想要的。
如有錯誤,請指正:)。
測試環境: win 8.1, Visual Studio 2010, Python 3.5
一、介紹
python 與 c/c++ 交互的主要目的一是為了速度,二大概就是用做腳本了。
說是 python 與 c/c++ 交互,但實際上是 python 與 c 交互, 因為 python 本身只支持 C API。但是我們可以通過調整達到 python 與 c++ 工程協作的目的。下面主要說明 python 使用 ctypes 模塊與 c 交互的要點和疑難點。
二、使用 ctypes 可以做到什么?
python 可以通過使用 ctypes 模塊調用 c 函數,這其中必定包括可以定義 c 的變量類型(包括結構體類型、指針類型)。
官方給的定義是 “ctypes
is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python.” —— 引自 Python 3.5 chm 文檔。其大意就是——ctypes 是一個為 Python 准備的外部函數庫。它提供兼容C的數據類型,並允許調用DLL或共享庫中的函數。通過它,可以使用純粹的 Python 包裝這些函數庫(這樣你就可以直接 import xxx 來使用這些函數庫了)。
口說無憑,我們需要一個具體的例子,下面我們引入一個 cpp 文件來說明以下所有問題:
現有 test.cpp 文件如下:
#if 1 #define DLL_API __declspec(dllexport) #else #define DLL_API __declspec(dllimport) #endif #include <stdio.h> #include <stdlib.h> // Point 結構體 struct Point { float x, y; }; static Point* position = NULL; extern "C" { DLL_API int add(int a, int b) { return a + b; } DLL_API float addf(float a, float b) { return a + b; } DLL_API void print_point(Point* p) { if (p) printf("position x %f y %f", p->x, p->y); } }
可以看見這里有三個函數,包括一個形參帶指針的函數。學會用 Python 成功調用上面的三個函數就是我的本文的目標了。對於windows平台把他生成為 dll 文件就行(其他平台為 .so)。下面我們在解釋器中寫出出測試用的 Python 代碼。
如果你不理解上面的 cpp 文件,那還是先看看其他關於 dll 的文章吧:
1. Dll的分析與編寫(一) http://www.cnblogs.com/hicjiajia/archive/2010/08/27/1809997.html
2. extern "C"的用法解析 http://www.cnblogs.com/rollenholt/archive/2012/03/20/2409046.html
三、ctypes 怎么樣調用 c 的函數庫?
首先,需要 ctypes 加載需要被調用的函數庫(廢話)。
使用 ctypes.CDLL ,其定義如下(引自 Python 3.5 chm 文檔 )
ctypes.
CDLL
(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
另外,在 windows 平台上會忽略 modes 參數。對於 windows 平台來說還可以調用 ctypes.WinDLL,與上面的 CDLL 幾乎一樣,唯一不同點是它假定庫中函數遵循 Windows stdcall 調用約定,其他參數的意義見官方文檔。
如果要調用 test.dll 中的 add 函數可以寫作 :
>>> from ctypes import * >>> dll = CDLL(“test.dll”) # 調用 test.dll >>> dll.add(10, 30) # 調用 add 函數 40
可以看見返回了 40,是不是很簡單?。這是就是我們預期的結果。下面我們再調用 addf 這是 add 的 float 版本,有些人可能會問為什么不直接寫 DLL_API float add(float a, float b) ? 用函數的重載就好了,為什么不這么做?注意,我們使用了 extern“C”聲明函數,所以不支持函數的重載。
接下來我們調用 addf , 猜猜會發生什么?
>>> dll.addf(10, 30)
9108284
哦,這是不是有點出乎你的意料?為什么會這樣?
四、c 類型與 Python 類型, 參數類型、返回類型
之所以會調用 addf 函數“失敗”倒不是 Python 出了問題。原因是你沒有“告訴” Python 這個函數的“容貌”(更正式的說法是“描述”)——函數的形參類型和返回類型。那么為什么我們調用 add 成功了呢?因為 Python 默認函數的參數類型和返回類型為 int 型。理所當然地 Python 以為 addf 返回了一個 int 類型的值。
也就是說,在 ctypes 讀取 dll 時只知道存在這個函數,但是並不知到函數的形參類型和返回值的類型。你可能會疑惑為什么 Python 這么麻煩,還要告訴它共享庫中函數的“容貌”。這就不能怪它了,事實上,就是 Microsoft 自己開發的 C# 語言在調用 dll 的時候都需要告訴 C# 這個函數是什么樣子的。這解釋起來有點煩,還是來專注於我們對 ctypes 用法的研究吧。
那么,對於 Python 來說 c 的類型都有哪些呢?下面就是一張 Python 中的類型對應 c 類型的表(截圖自 Python 3.5 chm 文檔)
然后,怎么告訴 Python 一個外來函數的形參類型和返回的值的類型呢?
這就要需要給函數的兩個屬性 restype 和 argtypes 賦值了。它們分別對應返回類型和參數類型。對於 addf 它的返回值類型是 float, 對應到 Python 里就是 c_float。下面我們進行賦值:
>>> dll.addf.restype = c_float # addf 返回值的類型是 flaot
如果函數的返回值是 void 那么你可以賦值為 None。另外,在不是太低的版本中,可以使用 Python 內置類型(上表中最右邊的一列)“描述”庫函數的返回類型,但是,不可以用 Python 內置類型來描述庫函數的參數。
由於函數的參數不是固定的數量,所以需要使用列表或者是元組來說明:
>>> dll.addf.argtypes = (c_float, c_float) # addf 有兩個形參,都是 float 類型 或者是下面這樣,但是,你知道的,查找元組的效率略高:) >>> dll.addf.argtypes = [c_float, c_float] # addf 有兩個形參,都是 float 類型
該做的都做完了,現在再來調用 addf:
>>> dll.addf(8, 3) 11.0 >>> dll.addf(8.3, 3.1) 11.399999618530273
這就是我們想要的結果。
五、更多地關於 ctypes 類型的創建和使用
我們也可以創建一個 ctypes 的類型(c_int、c_float、c_char……)並給他賦值,例子如下:
>>> i = c_int(45) # 定義一個 int 型變量,值為 45 >>> i.value # 打印變量的值 45 >>> i.value = 56 # 改變該變量的值為 56 >>> i.value # 打印變量的新值 56
沒錯,你要通過 ctypes 的 value 屬性給一個 ctypes 類型賦值——賦一個 Python 內置類型的值。
其他的 ctypes 的函數,如 sizeof(i)(是不是感覺很貼心就像 c 一樣),就不一一介紹了。自行參見文獻第三條和官方文檔吧。
六、結構體、共用體
這是調用 print_point 庫函數的必要成分之一。
如果要在 Python 中定義一個 c 類型的結構體,需要定義一個類,例如 Structu Point 就這么做:
>>> class Point(Structure): ... _fields_ = [("x", c_float), ("y", c_float)] ... >>>
這就定義好了。其中有兩個要點:
1. 類必須繼承自 ctypes.Structure
2. 描述這個結構體的“容貌”
第一點很簡單, class XXX(Structure) 就 OK。
要做到第二點,則必須在自定義的 c 結構體類中定義一個名為 _fields_ 的屬性,並賦值給如上的一個列表。
然后就可以這樣使用了:
>>> p = Point(2,5) # 定義一個 Point 類型的變量,初始值為 x=2, y=5 也可以直接寫 p = Point() >>> p.y = 3 # 修改值 >>> print (p.x, p.y) # 打印變量 2 3
而對於共用體只要類繼承自 ctypes.Union 就成,其他與上面相同。
七、指針
這就是最后一節了,雖然是指針,不過別緊張,且聽我娓娓道來。
如何創建一個 ctypes 的指針呢?這里有三個跟指針有個的 ctypes 里的函數,掌握了他們你自然就會了(可能 pointer POINTER 會有點繞,仔細看看就好)。
函數 | 說明 |
byref(x [, offset]) | 返回 x 的地址,x 必須為 ctypes 類型的一個實例。相當於 c 的 &x 。 offset 表示偏移量。 |
pointer(x) | 創建並返回一個指向 x 的指針實例, x 是一個實例對象。 |
POINTER(type) | 返回一個類型,這個類型是指向 type 類型的指針類型, type 是 ctypes 的一個類型。 |
byref 很好理解,傳遞參數的時候就用這個,用 pointer 創建一個指針變量也行,不過 byref 更快。
而 pointer 和 POINTER 的區別是,pointer 返回一個實例,POINTER 返回一個類型。甚至你可以用 POINTER 來做 pointer 的工作:
>>> a = c_int(66) # 創建一個 c_int 實例 >>> b = pointer(a) # 創建指針 >>> c = POINTER(c_int)(a) # 創建指針 >>> b <__main__.LP_c_long object at 0x00E12AD0> >>> c <__main__.LP_c_long object at 0x00E12B20> >>> b.contents # 輸出 a 的值 c_long(66) >>> c.contents # 輸出 a 的值 c_long(66)
pointer 創建的指針貌似沒方法修改指向的 ctypes 類型值。
該說的都說了,接下來就要調用 print_point 函數了:
>>> dll.print_point.argtypes = (POINTER(Point),) # 指明函數的參數類型 >>> dll.print_point.restype = None # 指明函數的返回類型 >>> >>> p = Point(32.4, -92.1) # 實例化一個 Point >>> dll.print_point(byref(p)) # 調用函數 position x 32.400002 y -92.099998>>>
當然你非要用慢一點的 pointer 也行:
>>> dll.print_point(pointer(p)) # 調用函數 position x 32.400002 y -92.099998>>>
至於為什么輸出的后面出現了畸形 “y -92.099998>>>” ,去翻一翻上面的 c 代碼你就知道了。
參考文獻
更多關於 ctypes 類型的用法可以參加下面的書籍、文檔和網頁:
1. 《Python參考手冊》
2. Python 3.5 官方文檔 “python350.chm”
3. http://www.ibm.com/developerworks/cn/linux/l-cn-pythonandc/