在做自动化测试的过程中,我需要使用python调用C++的SDK,完成接口测试的工作。由于团队只提供了C++的SDK源码,所以我需要做下面几件事:
- 封装C++的接口,进行接口的导出
- 打包为DLL,动态链接库
- 使用ctypes库,调用dll,完成测试
网上关于python通过ctypes调用C++动态链接库的教程非常多,我就不再写了,这里就写一下我在使用过程中遇到的两个问题:
- 接口返回为应字符串,C++代码中声明为char *,如何在python中获取字符串的返回值?
- 参数中有结构体,返回值是结构体,返回值或参数中有结构体数组的时候在python中如何传值和解析返回值?
C++接口导出
在看问题之前,我们顺便带一下C++代码如何封装。作为测试人员,我拿到的是C++的源码,要使用python调用它,首先我需要先导出我要测试的接口。为了让导出接口的代码不与SDK源代码混在一起,我们需要在项目中新建一个export.cpp专门用于导出,这样便于在git上与开发人员一起协同工作,互不干扰。
这个文件的内容,就是导出接口,这里直接看个例子,不做过多解释了
extern "C" __declspec(dllexport) int __stdcall setDeviceName(char* ip, unsigned short port, char* name) {
TestObject* obj = new TestObject(ip, port);
return obj->setDeviceName(name);
}
这里我们要注意一下,原C++的接口调用是需要先实例化一个类,然后使用类的方法来调用,但是我们封装的时候无法使用自定义类,所以我们需要在封装的时候完成这种操作,编程嘛,随机应变。
char * 返回值的解析
我们封装了这样一个接口:
extern "C" __declspec(dllexport) char* __stdcall getDeviceName(char* ip, unsigned short port) {
TestObject* obj = new TestObject(ip, port);
return obj->getDeviceName();
}
这个接口返回一个字符串,定义的返回类型为char *,我们看看在python中如何调用
libc = cdll.LoadLibrary('SDK.dll')
libc.getDeviceName.restype = c_char_p
s = bytes('192.168.1.113', encoding='utf-8')
deviceName = libc.getDeviceName(s, 5000).decode()
看上面的代码我们学到了以下几点:
- 我们可以通过设置要调用的方法的restype,来告诉ctypes返回值的类型
- 字符串不能直接作为char *类型的参数传递,需要转化了bytes
- 使用decode方法转char *为字符串
结构体,结构体指针,结构体数组
我们都知道一个接口只能有一个返回值,如果想返回多个值,在python中我们可以使用列表,元组,在C++中我们可以使用数组,结构体,这一小节我们就讨论返回多个值的问题。
我们看一个比较综合的例子,然后分析以下:
C++中结构体的定义:
struct BrightnessItem
{
int environment;
int screen;
};
对应的python中结构体的定义:
class BrightnessItem(Structure):
_fields_ = [
('environment', c_int),
('screen', c_int)
]
# 注意,使用Structure必须import ctypes
C++中,参数为结构体指针数组的封装方法:
extern "C" __declspec(dllexport) int __stdcall testMethod(char* ip, unsigned short port, BrightnessItem* items) {
TestObject* obj = new TestObject(ip, port);
return obj->testMethod(items);
}
在python中调用之前,我们要明白一个事实,单看导出的定义,我们并不知道这是仅仅表示一个结构体指针,还是表示一个结构体数组,那我们就得深入代码中去看了,在我拿到的代码中,我发现开发人员拿到这个参数,会作为长度为8的数组进行解析,所以我明白我需要传入一个长度为8的结构体数组(如果你碰到这种情况,也需要看代码进行分析),在分析完成之后,我就可以写出调用的python程序了:
list = []
for i in range(8):
item = BrightnessItem()
item.screen = 40
item.environment = 60
list.append(item)
# 参数为结构体
data = POINTER(BrightnessItem * 8)((BrightnessItem * 8)(*list))
libc.testMethod(s, 5000, data)
上面的长难句我们拆开看一下:
data = POINTER(BrightnessItem * 8)((BrightnessItem * 8)(*list))
// 上面的可以拆开为:
temp = (BrightnessItem * 8)(*list)
data = POINTER(BrightnessItem * 8)(temp)
这样是不是就清晰多了,会了最难的,单纯传结构体,或者是结构体指针就都不在话下了。
最后我们看一下返回值为结构体数组的情况,还是先看C++的导出定义:
extern "C" __declspec(dllexport) BrightnessItem * __stdcall testMethod(char* ip, unsigned short port) {
TestObject* obj = new TestObject(ip, port);
return obj->testMethod();
}
经过上面的学习,我们知道,要想让python解析返回值,最关键的就是如何告诉python返回的是什么类型,上面的返回值,我们同样需要看C++的代码确定返回的是一个结构体数组还是结构体指针,如果是数组,具体是多长?这里不多赘述,我看过代码之后知道返回了一个长度为8的结构体数组,于是我在python中这样指定返回值类型:
libc.testMethod.restype = POINTER(BrightnessItem * 8)
调用方法之后,我这样解析返回值:
struct_res = libc.testMethod(s, 5000)
for i in range(8):
ret_string = 'number:{index} screen:{screen} environment:{environment}'.format(index=i, screen=struct_res.contents[i].screen, environment=struct_res.contents[i].environment)
会了最难的结构体数组,单纯返回值为结构体和结构体指针的情况,你能不能举一反三了呢?
更多推荐
python通过ctypes调用C++ DLL过程中返回值的指定和结构体数组的使用
发布评论