Python:使用ctypes庫呼叫外部DLL(附帶ctypes c 型別轉換圖)
2010-04-04 23:36 by 無常, 22558 閱讀, 6 評論, 收藏, 編輯
前言
朋友的公司是做GPS的,上週聯絡到我要幫做個程式把他們平臺的車輛定位跟蹤資料和省裡的平臺對接。看一下官方提供的三個文件,洋洋灑灑共一百多頁,一大堆協議的定義甚是齊全,好在官方的檔案中也帶有個封裝好通訊功能的DLL和一個呼叫此介面的c++ DEMO程式,既然有現成的可用,那就不必去看他的協議了。
說實話,參加工作之後就基本沒用過c++,生疏了。特別是要用c++操作資料庫,對我來說比割幾刀還要痛苦。官方的API中已經很詳盡,要做的就是從現有平臺的資料庫中獲取車輛定位資訊,通過官方的API傳送到省中心平臺。
本想用C#給官方API做個包裝,省得再去動用C++,可是看到此API中定義有幾個Struct,而且下行資料都是通過回撥函式方式提供,google了一下,似乎C#對呼叫有回撥函式的C DLL不是很順暢,於是放棄了,想到了Python。
一、Python之ctypes
ctypes是Python的一個外部庫,提供和C語言相容的資料型別,可以很方便地呼叫C DLL中的函式。在Python2.5官方安裝包都帶有ctypes 1.1版。ctypes的官方文件在這裡。
ctypes的使用非常簡明,如呼叫cdecl方式的DLL只需這樣:
from ctypes import *;
h=CDLL('msvcrt.dll' )
h.printf('a=%d,b=%d,a+b=%d',1,2,1+2);
from ctypes import *;
h=CDLL('msvcrt.dll')
h.printf('a=%d,b=%d,a+b=%d',1,2,1+2);
以上程式碼執行後輸出 a=1,b=2,a+b=3。
二、載入庫和普通函式的呼叫
官方API提供的庫中有幾個主要的函式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//初始化
int DCSPCLIENTDLL InitInterface( const char *pCenterIP, const unsigned short nUpLinkSvrPort,const unsigned short nDownLinkSvrPort );
//釋放資源
int DCSPCLIENTDLL FiniInterface( void );
//登入
int DCSPCLIENTDLL Login( const unsigned int uiBranchPlatformID, const unsigned int nUserID, const char *pPassword );
//登出
int DCSPCLIENTDLL Logout( const unsigned int uiBranchPlatformID, const unsigned int nUserID, const char *pPassword );
//發車輛實時定位資料
int DCSPCLIENTDLL SendUPRealLocation( const char * const pDeviceId, const char cDeviceColor,
const unsigned short nMsgCode, const _stBPDynamicData * const pStGpsData );
//初始化
int DCSPCLIENTDLL InitInterface( const char *pCenterIP, const unsigned short nUpLinkSvrPort,const unsigned short nDownLinkSvrPort );
//釋放資源
int DCSPCLIENTDLL FiniInterface( void );
//登入
int DCSPCLIENTDLL Login( const unsigned int uiBranchPlatformID, const unsigned int nUserID, const char *pPassword );
//登出
int DCSPCLIENTDLL Logout( const unsigned int uiBranchPlatformID, const unsigned int nUserID, const char *pPassword );
//發車輛實時定位資料
int DCSPCLIENTDLL SendUPRealLocation( const char * const pDeviceId, const char cDeviceColor,
const unsigned short nMsgCode, const _stBPDynamicData * const pStGpsData );
在Python中載入使用:
from ctypes import *
#載入API庫
api = CDLL('DCSPClientDLL.dll');
#初始化函式的引數型別
api.InitInterface.argtypes=[c_char_p,c_ushort,c_ushort]
api.Login.argtypes=[c_uint,c_uint,c_char_p]
api.Logout.argtypes=[c_uint,c_uint,c_char_p]
#初始化並登入
api.InitInterface(u"中心伺服器地址" , u'上行服務端埠' , u'下行客戶端埠')
api.Login(platformID,userID,password);
#.....其它操作
api.Logout(platformID,userID,password); #登出
from ctypes import *
#載入API庫
api = CDLL('DCSPClientDLL.dll');
#初始化函式的引數型別
api.InitInterface.argtypes=[c_char_p,c_ushort,c_ushort]
api.Login.argtypes=[c_uint,c_uint,c_char_p]
api.Logout.argtypes=[c_uint,c_uint,c_char_p]
#初始化並登入
api.InitInterface(u"中心伺服器地址" , u'上行服務端埠' , u'下行客戶端埠')
api.Login(platformID,userID,password);
#.....其它操作
api.Logout(platformID,userID,password); #登出
引數型別可以像上面的程式碼一樣預先設定好,或者在呼叫函式時再把引數轉成相應的c_***型別。ctypes的型別對應如下:
image
如此,完成了簡單的第一步。
三、C語言中的Struct資料結構
在傳送實時定位資料的函式SendUPRealLocation中有一個引數是結構體型別 _stBPDynamicData。python中沒有struct這種資料結構,ctypes很周全,對C的struct和union這二種資料型別都提供很好的支援。stBPDynamicData結構的定義如下:
// 車輛動態資料結構體
struct _stBPDynamicData
{
// 加密狀態
unsigned char encrypt;
// GPS 時間
_StructTime gpsTime;
// 經度
unsigned int longitude;
// 緯度
unsigned int latitude;
// GPS速度
unsigned short unGpsSpeed;
// 行駛記錄儀速度
unsigned short unTachographSpeed;
// 車輛當前總里程數
unsigned int uiMileageTotal;
// 角度
unsigned short angle;
// 車輛狀態
unsigned short state;
// 報警狀態
unsigned short alarm;
};
// 車輛動態資料結構體
struct _stBPDynamicData
{
// 加密狀態
unsigned char encrypt;
// GPS 時間
_StructTime gpsTime;
// 經度
unsigned int longitude;
// 緯度
unsigned int latitude;
// GPS速度
unsigned short unGpsSpeed;
// 行駛記錄儀速度
unsigned short unTachographSpeed;
// 車輛當前總里程數
unsigned int uiMileageTotal;
// 角度
unsigned short angle;
// 車輛狀態
unsigned short state;
// 報警狀態
unsigned short alarm;
};
在python中,需要定義一個與這相容的類,繼承於ctypes.Structure,其中還用到一個_StructTime結構,這裡一併貼出程式碼:
class _StructTime(Structure):
_fields_ =[('day',c_ubyte),
('month',c_ubyte),
('year',c_ushort),
('hour',c_ubyte),
('minute',c_ubyte),
('second',c_ubyte)];
def __str__(self):
return '{0}-{1}-{2} {3}:{4}:{5}'.format(self.year,self.month,self.day,self.hour,self.minute,self.second);
class _stBPDynamicData(Structure):
_fields_ =[('encrypt',c_ubyte),
('gpsTime',_StructTime),
('longitude',c_uint),
('latitude',c_uint),
('unGpsSpeed',c_ushort),
('unTachographSpeed',c_ushort),
('uiMileageTotal',c_uint),
('angle',c_ushort),
('state',c_ushort),
('alarm',c_ushort)];
def __str__(self):
return u'({0},{1}),{6},sp:{2},angle:{3},st:{4},al:{5}'.format(self.longitude,
self.latitude,self.unGpsSpeed,
self.angle ,self.state,self.alarm,self.gpsTime );
class gpsData(Structure):
_fields_ =[('strDeviceID',c_char_p),
('cDeviceColor',c_char),
('nMsgCode',c_ushort),
('stBPD',_stBPDynamicData)];
def __str__(self):
return u'{0},{1}'.format(self.strDeviceID,self.stBPD );
gpsData是我自己加的一個類,用於記錄每輛車的資訊。
現在就可以使用SendUPRealLocation函式傳送車輛實時資料了:
tm=_StructTime();
tm.year=2010;tm.month=4;tm.day=3;tm.hour=11;tm.minute=2;tm.second=11;
bpd=_stBPDynamicData();
bpd.gpsTime=tm;bpd.longitude=1234567;bpd.latitude=246898;#...其它引數
data=gpsData();
data.strDeviceID=u'桂Coo007';data.stBPD=bpd;
#呼叫 API傳送資料
api.SendUPRealLocation( data.strDeviceID, data.cDeviceColor ,
data.nMsgCode, addressof( data.stBPD ) );
注意SendUPRealLocation第三個引數是_stBPDynamicData * 指標型別,所以要用ctypes.addressof()取引數的地址。
四、回撥函式
寫到這裡就忍不住嘮叨幾句,這個系統的協議設計的太有 “個性”了。這個系統的功能說起來也不復雜,就是要GPS運營商把指定的車輛位置資訊傳送到中心平臺,同時中心平臺可以向各GPS終端傳送一些資料和指令,比如傳送文字資訊到終端,或者要求終端拍張照片反饋到中心。
這個協議流程是這樣,運營商端主動連線到中心伺服器,然後此連線只用於傳輸向中心平臺主動傳送的資料。登入成功了之後呢,中心平臺再向運營商的IP建立一個連線,用於中心下發的資料和指令。官方稱為“雙鏈路”。
於是,就要求運營商必須要有固定的公網IP(這個不是問題,據瞭解GPS運營商伺服器都有固定IP),而且這個程式必須執行在有公網IP的電腦上或採用埠對映之類的方法。可是俺開發設計時是在大教育區域網中的,搞個埠對映都不可能更別談公網IP了。於是,在除錯下行資料部分功能時就只能遠端到運營商伺服器上去除錯了。
迴歸正題。
要使用回撥函式,需要先用 CFUNCTYPE 定義回撥函式的型別,官方API中有十多個回撥函式註冊,定義摘抄:
#define DCSPCLIENTDLL __declspec(dllexport)
typedef void (*pDownTextInfoFv) ( const char *const pDeviceID,
const char cDeviceColor, const char *const pInfo );
typedef void (*pDownCommunicateReqFv) ( const char *const pDeviceID,
const char cDeviceColor, const char *const pCalledTel );
extern "C"
{
void DCSPCLIENTDLL RegDownTextInfoFunc( pDownTextInfoFv pFv );
void DCSPCLIENTDLL RegDownCommunicateReqFunc( pDownCommunicateReqFv pFv );
};
在python中,定義相應的型別和回撥處理函式:
"""下發文字資訊"""
def downTextInfo(pDeviceID,cDeviceColor,pInfo):
print(u'<-[下發文字]:{0},{1}'.format(str(pDeviceID),str(pInfo)) );
r=api.SendUpTextInfoAck(pDeviceID, cDeviceColor, True );
if r==0:
print(u'->回覆下發文字成功。');
else:
print(u'->回覆下發文字失敗。');
pDownTextInfoFv = CFUNCTYPE(c_void_p,c_char_p, c_char, c_char_p) #回撥函式型別定義
pDownTextInfoHandle = pDownTextInfoFv(downTextInfo);
api.RegDownTextInfoFunc(pDownTextInfoHandle); #註冊回撥函式
其中SendUpCommunicateAck是迴應中心,告知已經收到資訊。二個引數型別和downTextInfo中的引數型別一到,所以可以不用初始化宣告此函式的引數定義。
其餘的回撥函式用相同的方法處理。
結尾
除錯完API對接部分功能後,在想用哪個py庫操作資料庫比較方便呢,找了一下之後才想到為何不用ironPython而可以直接使用ado.net訪問資料庫,豈不是更爽。
於是把程式碼搬到ironPython2.6中試試,讓我十分驚喜的是不用做任何個性程式碼直接執行成功!ironPython 2.6中的ctypes和Python2.6的一樣都是1.1.0版。