Python:使用ctypes库调用外部DLL(转)
前言
朋友的公司是做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只需这样:
123 | 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提供的库中有几个主要的函数:
1234567891011121314 | //初始化 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中加载使用:
1234567891011121314 | 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的类型对应如下:
如此,完成了简单的第一步。
三、C语言中的Struct数据结构
在发送实时定位数据的函数SendUPRealLocation中有一个参数是结构体类型 _stBPDynamicData。python中没有struct这种数据结构,ctypes很周全,对C的struct和union这二种数据类型都提供很好的支持。stBPDynamicData结构的定义如下:
123456789101112131415161718192021222324 | // 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结构,这里一并贴出代码:
123456789101112131415161718192021222324252627282930313233 | 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函数发送车辆实时数据了:
123456789 | 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.SendUPRealLocation( data.nMsgCode, addressof( data.stBPD ) ); |
注意SendUPRealLocation第三个参数是_stBPDynamicData * 指针类型,所以要用ctypes.addressof()取参数的地址。
四、回调函数
写到这里就忍不住唠叨几句,这个系统的协议设计的太有 “个性”了。这个系统的功能说起来也不复杂,就是要GPS运营商把指定的车辆位置信息发送到中心平台,同时中心平台可以向各GPS终端发送一些数据和指令,比如传送文字信息到终端,或者要求终端拍张照片反馈到中心。
这个协议流程是这样,运营商端主动连接到中心服务器,然后此连接只用于传输向中心平台主动发送的数据。登录成功了之后呢,中心平台再向运营商的IP建立一个连接,用于中心下发的数据和指令。官方称为“双链路”。
于是,就要求运营商必须要有固定的公网IP(这个不是问题,据了解GPS运营商服务器都有固定IP),而且这个程序必须运行在有公网IP的电脑上或采用端口映射之类的方法。可是俺开发设计时是在大教育局域网中的,搞个端口映射都不可能更别谈公网IP了。于是,在调试下行数据部分功能时就只能远程到运营商服务器上去调试了。
回归正题。
要使用回调函数,需要先用 CFUNCTYPE 定义回调函数的类型,官方API中有十多个回调函数注册,定义摘抄:
123456789101112 | #define 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中,定义相应的类型和回调处理函数:
12345678910111213 | "" "下发文字信息" "" def print(u '<-[下发文字]:{0},{1}' .format(str(pDeviceID),str(pInfo)) ); r=api.SendUpTextInfoAck(pDeviceID, cDeviceColor, True ); if r==0: print(u '->回复下发文字成功。' ); else : print(u '->回复下发文字失败。' ); pDownTextInfoFv pDownTextInfoHandle api.RegDownTextInfoFunc(pDownTextInfoHandle); |
其中SendUpCommunicateAck是回应中心,告知已经收到信息。二个参数类型和downTextInfo中的参数类型一到,所以可以不用初始化声明此函数的参数定义。
其余的回调函数用相同的方法处理。
结尾
调试完API对接部分功能后,在想用哪个py库操作数据库比较方便呢,找了一下之后才想到为何不用ironPython而可以直接使用ado.net访问数据库,岂不是更爽。
于是把代码搬到ironPython2.6中试试,让我十分惊喜的是不用做任何个性代码直接运行成功!ironPython 2.6中的ctypes和Python2.6的一样都是1.1.0版。