在做自动化测试的过程中,我需要使用python调用C++的SDK,完成接口测试的工作。由于团队只提供了C++的SDK源码,所以我需要做下面几件事:

  1. 封装C++的接口,进行接口的导出
  2. 打包为DLL,动态链接库
  3. 使用ctypes库,调用dll,完成测试

网上关于python通过ctypes调用C++动态链接库的教程非常多,我就不再写了,这里就写一下我在使用过程中遇到的两个问题:

  1. 接口返回为应字符串,C++代码中声明为char *,如何在python中获取字符串的返回值?
  2. 参数中有结构体,返回值是结构体,返回值或参数中有结构体数组的时候在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过程中返回值的指定和结构体数组的使用