ctypes给扩展模块中的函数传递数组和结构体
传递数组
楔子
下面我们来看看如何使用ctypes传递数组,这里我们只讲传递,不讲返回。因为C语言返回数组给python实际上会存在很多问题,比如:返回的数组的内存由谁来管理,不用了之后空间由谁来释放,事实上ctypes内部对于返回数组支持的也不是很好。因此我们一般不会向python返回一个C语言中的数组,因为C语言中的数组传递给python涉及到效率的问题,python中的列表传递直接传递一个引用即可,但是C语言中的数组过来肯定是要拷贝一份的,所以这里我们只讲python如何通过ctypes给扩展模块传递数组,不会介绍扩展模块如何返回数组给python。
如何传递
我们知道python中没有数组,或者说C中的数组在python中是一个list,我们可以通过list来得到数组,方式也很简单。
from ctypes import * # 创建一个数组,假设叫[1, 2, 3, 4, 5] a5 = (c_int * 5)(1, 2, 3, 4, 5) print(a5) # <__main__.c_long_Array_5 object at 0x00000162428968C0> # 上面这种方式就得到了一个数组 # 当然还可以使用list a5 = (c_int * 5)(*range(1, 6)) print(a5) # <__main__.c_long_Array_5 object at 0x0000016242896940>
下面演示一下。
//字符数组默认是以\0作为结束的,我们可以通过strlen来计算长度。 //但是对于整型的数组来说我们不知道有多长 //因此有两种声明参数的方式,一种是int a[n],指定数组的长度 //另一种是通过指定int *a的同时,再指定一个参数int size,调用函数的时候告诉函数这个数组有多长 int test1(int a[5]) { //可能有人会问了,难道不能通过sizeof计算吗?答案是不能,无论是int *a还是int a[n] //当它作为函数的参数,我们调用的时候,传递的都是指针,指针在64位机器上默认占8个字节。 //所以int a[] = {...}这种形式,如果直接在当前函数中计算的话,那么sizeof(a)就是数组里面所有元素的总大小,因为a是一个数组名 //但是当把a传递给一个函数的时候,那么等价于将a的首地址拷贝一份传过去,此时在新的函数中再计算sizeof(a)的时候就是一个指针的大小 //至于int *a这种声明方式,不管在什么地方,sizeof(a)则都是一个指针的大小 int i; int sum = 0; a[3] = 10; a[4] = 20; for (i = 0;i < 5; i++){ sum += a[i]; } return sum; }
from ctypes import * lib = CDLL("./mmp.dll") # 创建5个元素的数组,但是只给3个元素 arr = (c_int * 5)(1, 2, 3) # 在扩展模块中,设置剩余两个元素 # 所以如果没问题的话,结果应该是1 + 2 + 3 + 10 + 20 print(lib.test1(arr)) # 36
传递结构体
定义一个结构体
有了前面的数据结构还不够,我们还要看看结构体是如何传递的,有了结构体的传递,我们就能发挥更强大的功能。那么我们来看看如何使用ctypes定义一个结构体:
from ctypes import * # 对于这样一个结构体应该如何定义呢? """ struct Girl { //姓名 char *name; //年龄 int age; //性别 char *gender; //班级 int class; }; """ # 定义一个类,必须继承自ctypes.Structure class Girl(Structure): # 创建一个_fields_变量,必须是这个名字,注意开始和结尾都只有一个下划线 # 然后就可以写结构体的字段了,具体怎么写估计一看就清晰了 _fields_ = [ ("name", c_char_p), ("age", c_int), ("gender", c_char_p), ("class", c_int) ]
如何传递
我们向C中传递一个结构体,然后再返回:
struct Girl { char *name; int age; char *gender; int class; }; //接收一个结构体,返回一个结构体 struct Girl test1(struct Girl g){ g.name = "古明地觉"; g.age = 17; g.gender = "female"; g.class = 2; return g; }
from ctypes import * lib = CDLL("./mmp.dll") class Girl(Structure): _fields_ = [ ("name", c_char_p), ("age", c_int), ("gender", c_char_p), ("class", c_int) ] # 此时返回值类型就是一个Girl类型,另外我们这里的类型和C中结构体的名字不一样也是可以的 lib.test1.restype = Girl # 传入一个实例,拿到返回值 g = Girl() res = lib.test1(g) print(res, type(res)) # <__main__.Girl object at 0x0000015423A06840> <class '__main__.Girl'> print(res.name, str(res.name, encoding="utf-8")) # b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89' 古明地觉 print(res.age) # 17 print(res.gender) # b'female' print(getattr(res, "class")) # 2
如果是结构体指针呢?
struct Girl { char *name; int age; char *gender; int class; }; //接收一个指针,返回一个指针 struct Girl *test1(struct Girl *g){ g -> name = "mashiro"; g -> age = 17; g -> gender = "female"; g -> class = 2; return g; }
from ctypes import * lib = CDLL("./mmp.dll") class Girl(Structure): _fields_ = [ ("name", c_char_p), ("age", c_int), ("gender", c_char_p), ("class", c_int) ] # 此时指定为Girl类型的指针 lib.test1.restype = POINTER(Girl) # 传入一个实例,拿到返回值 # 如果lib.test1.restype指定的类型不是结构体指针,那么函数返回的就是该结构体(Girl)实例 # 但返回的是指针,我们还需要手动调用一个contents才可以拿到对应的值。 g = Girl() res = lib.test1(byref(g)) print(str(res.contents.name, encoding="utf-8")) # mashiro print(res.contents.age) # 16 print(res.contents.gender) # b'female' print(getattr(res.contents, "class")) # 3 # 另外我们不仅可以通过返回的res去调用,因为我们传递的是g的指针 # 修改指针指向的内存就相当于修改g # 所以我们通过g来调用也是可以的 print(str(g.name, encoding="utf-8")) # mashiro
因此对于结构体来说,我们先创建一个结构体(Girl)
实例g,如果扩展模块的函数中接收的是结构体,那么直接把g传进去等价于将g拷贝了一份,此时函数中进行任何修改都不会影响原来的g。但如果函数中接收的是结构体指针,我们传入byref(g)相当于把g的指针拷贝了一份,在函数中修改是会影响g的。而返回的res也是一个指针,所以我们除了通过res.contents来获取结构体中的值之外,还可以通过g来获取。再举个栗子对比一下:
struct Num { int x; int y; }; struct Num test1(struct Num n){ n.x += 1; n.y += 1; return n; } struct Num *test2(struct Num *n){ n->x += 1; n->y += 1; return n; }
from ctypes import * lib = CDLL("./mmp.dll") class Num(Structure): _fields_ = [ ("x", c_int), ("y", c_int), ] # 我们在创建的时候是可以传递参数的 num = Num(x=1, y=2) print(num.x, num.y) # 1 2 lib.test1.restype = Num res = lib.test1(num) # 我们看到通过res得到的结果是修改之后的值 # 但是对于num来说没有变 print(res.x, res.y) # 2 3 print(num.x, num.y) # 1 2 """ 因为我们将num传进去之后,相当于将num拷贝了一份。 函数里面的结构体和这里的num尽管长得一样,但是没有任何关系,自增1之后返回交给res。 所以res获取的结果是自增之后的结果,但是num还是之前的num """ # 我们来试试传递指针,将byref(num)再传进去 lib.test2.restype = POINTER(Num) res = lib.test2(byref(num)) print(num.x, num.y) # 2 3 """ 我们看到将指针传进去之后,相当于把num的指针拷贝了一份。 然后在函数中修改,相当于修改指针指向的内存,所以是会影响外面的num的 而扩展模块的函数中返回的是参数中的结构体指针,而我们传递的byref(num)也是这里的num的指针 尽管传递指针的时候也是拷贝了一份,两个指针本身来说虽然也没有任何联系,但是它们存储的地址是一样的 那么通过res.contents获取到的内容就相当于是这里的num 因此此时我们通过res.contents获取和通过num来获取都是一样的。 """ print(res.contents.x, res.contents.y) # 2 3 # 另外还需要注意的一点就是:如果传递的是指针,一定要先创建一个变量 # 比如这里,一定是:先要num = Num(),然后再byref(num)。不可以直接就byref(Num()) # 原因很简单,因为Num()这种形式在创建完Num实例之后就销毁了,因为没有变量保存它,那么此时再修改指针指向的内存就会有问题,因为内存的值已经被回收了 # 如果不是指针,那么可以直接传递Num(),因为拷贝了一份
传递结构体数组
我们来一个难度高的,其实也不难,我们可以传递一个结构体数组。
#include <stdio.h> typedef struct { char *name; int age; char *gender; }Girl; void print_info(Girl *g, int size) { int i; for (i=0;i<size;i++){ printf("%s %d %s\n", g[i].name, g[i].age, g[i].gender); } }
from ctypes import * lib = CDLL("./mmp.dll") class Girl(Structure): _fields_ = [ ("name", c_char_p), ("age", c_int), ("gender", c_char_p), ] g1, g2, g3 = Girl(c_char_p(b"mashiro"), 16, c_char_p(b"female")), Girl(c_char_p(b"satori"), 17, c_char_p(b"female")), Girl(c_char_p(b"koishi"), 16, c_char_p(b"female")) g = (Girl * 3)(*[g1, g2, g3]) # 指定返回值类型 lib.print_info.restype = (Girl * 3) lib.print_info(g, 3) """ mashiro 16 female satori 17 female koishi 16 female """
因此我们发现对于数组的传递也是很简单的