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
"""

因此我们发现对于数组的传递也是很简单的

ctypes给扩展模块中的函数传递数组和结构体

相关推荐