python ctypes 探究 ---- python 與 c 的交互

近幾天使用 python 與 c/c++ 程序交互,網上有推薦swig但效果都不理想,因此琢磨琢磨了 python 的 ctypes 模塊。同時,雖然網上有這方面的內容,可是感受仍是沒說清楚。這裏記錄下來作備用,同時也給廣大 python with c/c++ 派留給方便。若是你以爲我寫的很差,能夠參考官方文檔裏對 ctypes 的介紹,那裏說不必定有你想要的。html

若有錯誤,請指正:)。python

測試環境: win 8.1,   Visual Studio 2010,   Python 3.5linux

 

1、介紹

python 與 c/c++ 交互的主要目的一是爲了速度,二大概就是用作腳本了。c++

說是 python 與 c/c++ 交互,但其實是 python 與 c 交互, 由於 python 自己只支持 C API。可是咱們能夠經過調整達到 python 與 c++ 工程協做的目的。下面主要說明 python 使用 ctypes 模塊與 c 交互的要點和疑難點。windows

 

 

2、使用 ctypes 能夠作到什麼?

python 能夠經過使用 ctypes 模塊調用 c 函數,這其中一定包括能夠定義 c 的變量類型(包括結構體類型、指針類型)。ide

官方給的定義是 「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 文件以下:spa

#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

 

 

3、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

哦,這是否是有點出乎你的意料?爲何會這樣?

 

 

4、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 文檔)

c_ype-to-python_type

而後,怎麼告訴 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

這就是咱們想要的結果。

 

 

5、更多地關於 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 同樣),就不一一介紹了。自行參見文獻第三條和官方文檔吧。

 

 

6、結構體、共用體

這是調用 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 就成,其餘與上面相同。

 

 

7、指針

這就是最後一節了,雖然是指針,不過別緊張,且聽我娓娓道來。

如何建立一個 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/