python 與 c/c++ 的交互實踐教程

專案需要用到 python 與 c/c++ 程序交互,網上有推薦swig但不符合專案需求,琢磨了 python 的 ctypes 模塊感覺不錯,這裏記錄下來做備用,同時也給廣大 python with c/c++ 派留給方便。

測試環境: 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 代碼。


三、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 與 c/c++ 的交互實踐教程

然後,怎麽告訴 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/

更多相關文章
  • Celery是Python開發的分布式任務調度模塊,本文我們深入學習一下 celery 模塊的配置,感興趣的朋友可以參考一下.celery的官方文檔其實相對還是寫的很不錯的.但是在一些深層次的使用上面卻顯得雜亂甚至就
  • 本文分享一個js與mvc數據的交互實例教程,js向後台提交數據的方法一般是get傳值和post傳值,數據格式兼容比較強的是json串.在做考試系統中,遇到最頭疼的問題,就是前台與後台的交互.對于使用easyui來說,剛接觸不久,有點陌生.查著文檔,看著Demo.做起來著實有點頭疼.首先,前台使用的是 ...
  • BCP導出導入 SQL SERVER 大容量數據實踐教程
    BCP是SYBASE公司提供專門用于數據庫表一級數據備份的工具.本文我們講講用BCP導出導入大容量SQL SERVER數據的實踐教程本教程我們介紹大容量數據導出導入的利器--BCP實用工具.同時在後面也介紹BULK INSERT導入大容量數據,以及BCP結合BULK INSERT做數據接口的實踐(在
  • 因爲ezdpl不是開箱即用的,得根據自己的應用環境定制,所以對初次使用困難很多,本文的重點是將 ezdpl Linux自動化部署步驟分享出來.申明:ezdpl不是開箱即用的,需要根據自己的應用環境定制.對初學者來說使用起來反倒困難更多.風險更大.它不是一個通用的專案,更多的是提供一種思路,將繁瑣的操
  • 本文我們來講講iOS系統上通用的本地Objective-C代碼與Javascript互操作的基本方法,本文還涉及到html5的應用,這裏把實現方法簡單介紹.專案中要用到html5來實現,涉及到Objective-C調用JS,以及JS調用Objective-C的方法,這裏把遇到的問題以及實現方法介紹一 ...
  • Lua 是一個小巧的腳本語言,Lua腳本可以很容易的被C/C++ 代碼調用,也可以反過來調用C/C++的函數,本文我們講講爲 lua 配一個合適的記憶體分配器的一些想法及實踐教程.爲 lua 配一個合適的記憶體分配器
  • FLEX與JAVA交互實例代碼與詳細說明下載FLEX的插件step 1.下載 flex 3.0 plugin 插件 裝在 我的myeclips教程e 6.0 上 1) 下載地址:http://trials.adobe.com/Applications/Flex/FlexBuilder/3/FB3_W ...
  • Python的異常處理能力是很強大的,可向用戶准確反饋出錯信息.在Python中,異常也是對象,可對它進行操作.所有異常都是基類Exception的成員.Python 異常處理 python提供了兩個非常重要的功能來處理python程序在運行中出現的異常和錯誤.你可以使用該功能來調試python程序 ...
一周排行
  • 生成RSS非常的簡單只需要使用rss格式然後生成輸出到浏覽器就可以了,重點就是要告訴浏覽器你是rss格式的數據即可,具體來看個例子.rss(簡易信息聚合也叫聚合內容)是一種描述和同步網站內容的格式.下面的生成RSS訂
  • 手機端CSS Sprite圖標定位的學習筆記
    Sprite技術我們在各種大型網站都會看到它們用到了,Sprite技術可以減少伺服器數據連 ...
  • iphone5s/5c短信設置與使用技巧詳解
    iphone5s使用的是ios7系統,下面小編來給大家介紹ios7的短信設置與使用技巧,歡
  • 本文章簡單的介紹一下關于session_destroy(),session_unset()區別說明,有需要的朋友可以參考一下.session_unset()You should know that on recent
  • 在php中獲取文件的mime類型方法有很多種,我們來介紹直接利用mime_content_type()函數判斷獲取mime類型即可了.mime_content_type返回指定文件的MIME類型,用法: 代碼如下 e ...
  • 我們先來分析流程一,數據庫教程,二,數據庫文件dbconn.php三,vote.php投票頁面四,投票功能頁面vote_add.php五,查看所有投票數據viewpoll.php現在我們來看看數據庫結構 代碼如下 d
  • asp.net C連接oracle數據庫並顯示數據 本款是是由asp.net 與oracle數據庫連接,並且顯示數據中的所有數據哦.asp教程.net c連接oracle數據庫教程並顯示數據本款是是由asp.net教
  • 現在爲大家分析一下爲什麽沒有收入. 1.推廣方法不正確. 並不是只要每天去發發鏈接就可以了.如你發在阿裏媽媽論壇,會用效果嗎,當然沒有任何效果.就算論壇不刪你的貼子.你天天發,每天發100個貼子,同樣你的收入還是0.
  • 在php中刪除文件與目錄其實很簡單只要兩個函數一個是unlink一個rmdir函數,如果要實現刪除目錄及目錄下的文件我們需要利用遞歸來操作.函數代碼:僅刪除指定目錄下的文件,不刪除目錄文件夾. 代碼如下 class
  • 三星SCX-4521FSCX-4521FH印表機驅動不能安裝,如何解決
    昨天有朋友問爲什麽自己的三星SCX-4521F(SCX-4521FH)印表機驅動不能安裝呢