添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Python和C语言交互--ctypes,struct

python和c语言进行数据交互,涉及类型转换,字节对齐,字节序大小端转换等。相关模块ctypes,struct,memoryview。

一.ctypes:python和c语言使用结构体数据进行交互

场景:有一个C语言生成的动态链接库,python需要调用动态库里的函数处理数据。函数的入参是结构体类型的指针,出参是一个buffer,那么如何把python的数据转换成c语言中的结构体类型?

1.ctypes的使用
C语言代码如下

#include <stdio.h>
typedef struct student{
    char name;
    short class;
    double num;
    int age;
}stu;
typedef struct stu_struct_array{
    stu stu_array[2];
}stu_struct_array_t;
int struct_test(stu *msg, stu_struct_array_t *nest_msg, char *buff){
    int index = 0;  
    printf("stu name: %d\n", msg->name);
    printf("stu class: %d\n", msg->class);
    printf("stu num: %f\n", msg->num);
    printf("stu age: %d\n", msg->age);
    memcpy(nest_msg->stu_array, msg, sizeof(stu));
    printf("stu array index 0 name: %d\n", nest_msg->stu_array[0].name);
    printf("stu array index 0 class: %d\n", nest_msg->stu_array[0].class);
    memcpy(buff, msg, sizeof(stu));
    printf("buff: %d %d", buff[0], buff[1]);
    return 1;

通过-fPIC -shared选项生成动态链接库,编译命令 gcc -Wall -g -fPIC -shared -o libstruct.so.0 struct_array.c
此时需要通过python调用 struct_test() 函数,那么如何利用python传入结构体参数呢?
方法就是利用ctypes模块组装结构体
(1)首先是结构体的组装

ctypes定义了一些和C兼容的基本数据类型:

_ fields _需要包括 (构体成员名称, C语言中的数据类型) 组成的元组列表来初始化

from ctypes import *
# 根据结构体类型组装数据
fields_list = [("name", c_char),
                ("class", c_short),
                ("num", c_double),
                ("age", c_int)]
stu_value_list = [c_char(b'\x05'), c_short(1), c_double(10244096), c_int(2)]
# 创建结构体对象
class StuStruct(Structure):
    # _fields_是容纳每个结构体成员类型和值的列表,可以配合自动生成fields list和value list的函数使用
    _fields_ = fields_list
    # 也可以直接初始化,适用于结构体数量不多的情况
    _fields_ = [("name", c_char, b'\x05),
                ("class", c_short, 1),
                ("num", c_double, 10244096),
                ("age", c_int, 2)]
# 实例化并初始化结构体成员的值
stu_obj = StuStruct(*stu_value_list)
print("stu name: %s" % stu_obj.name)
# 这里使用的时候需要注意,结构体成员的名称不能和python内置关键字重复,如果真出现了这种情况。。。
# print(stu_obj.class)
print("stu num: %s" % stu_obj.num)
print("stu age: %s" % stu_obj.age)
# 创建嵌套结构体对象
class NestStu(Structure):
    _fields_ = [("stu_array1", StuStruct * 2)
# 创建StuStruct的数组
stu_array = StuStruct * 2
stu_obj_list = [stu_obj, stu_obj]
# 实例化stu_array
stu_array_obj = stu_array(*stu_obj_list)
# 实例化NestStu,因为stu_array1成员是结构体数组类型,只能以同类型的实例进行初始化
nest_obj = NestStu(stu_array_obj)
# 打印信息
print("name: %s" % nest_obj.stu_array1[0].name)
print("num: %s" % nest_obj.stu_array1[0].num)
print("age: %s" % nest_obj.stu_array1[0].age)
# 载入动态链接库
struct_so = cdll.LoadLibrary("./libstruct.so.0")
# 调用函数,根据入参类型需要把结构体转换成对应的指针
stu_p = pointer(stu_obj)
nest_stu_p = pointer(nest_obj)
# ctypes模块提供了快速创建char类型数组的方法
in_buff =create_string_buffer(b"", size=100)
rest = struct_so.struct_test(stu_p, nest_stu_p, in_buff)
# 一般情况下若被调用的函数没有返回值,成功执行后则会返回0,若有其他返回值则返回对应的值
print("rest: %s" % rest)

这里使用的时候需要注意,结构体成员的名称不能和python内置关键字重复,如上述的class,如果真出现了这种情况。。。

(2)调用动态链接库,查看打印,获取输出

stu name: b'\x05'
stu num: 10244096.0
stu age: 2
name: b'\x05'
num: 10244096.0
age: 2
stu name: 5
stu class: 1
stu num: 10244096.000000
stu age: 2
stu array index 0 name: 5
stu array index 0 class: 1
rest: 1
buff: 5 0

此处应该注意的一个问题是字节对齐的问题,ctypes模块提供了 _pack_ 属性来设置字节对齐,默认不设置则跟编译器设置相同4字节对齐,如果设置为1字节对齐,需要更改代码,比如在StuStruct中加入_ pack _ = 1,

# 创建结构体对象
class StuStruct(Structure):
    # _fields_是容纳每个结构体成员类型和值的列表,可以配合自动生成fields list和value list的函数使用
    _fields_ = fields_list
        _pack_ = 1

print(sizeof(StuStruct)) 的输出为15,不指定字节对齐则为24。

此外,除了实现字节对齐以外,ctypes模块还提供了 class BigEndingStructure() class LittleEndingStructure(() 用于创建大小端字节序的结构体,
更多方法请参照我的另一篇文章,里面详细介绍了使用Python组装C语言数据类型的方法。

  • 指针类型
  • 指针数组类型
  • 结构体指针类型
  • 结构体指针数组类型
  • 函数指针
  • 类型转换
  • 获取函数返回值类型

二.处理字节流

在使用python处理二进制数据或者使用socket通信的时候,python提供了struct模块将数据转换为字节流进行处理。
1.内置方法:

  • def calcsize(fmt)
    根据给定的fmt计算calsize大小
  • def pack(fmt, *args)
    fmt:格式控制符,主要用于指定每一个需要解析的数据大小,格式控制符对应c语言的数据类型和size如下

*args:需要pack的值的列表
return:字节对象

  • def pack_into(fmt, buffer, offset, *args)
    将args列表内的数据pack为字节流。然后写入buffer内偏移量为offset以后的区域
  • def unpack(fmt, string)
    将string根据fmt解析为一个元组然后返回
  • def unpack_from(fmt, buffer, offset=0)
    从buffer区域offset位置开始截取字节流然后进行转换,返回一个元组
  • def iter_unpack(*fmt, **string)
    先使用calsize计算fmt的大小,然后每次转换string中长度为每个fmt对饮大小的字节,返回的是每次unpack产生的值组成的一个unpack_iterator。
import struct
int_byte1 = b'\x01\x00\x00\x00\x02\x00\x03\x00\x00\x00'
fmt = "=IHI"
rest = struct.iter_unpack(fmt, int_byte1)
print(type(rest))
for item in rest:
    print(item)

输出:

(1, 2, 3)

2.字节序的转换
因为个人业务遇到了一种情况,本机为小端字节序,但是在转换为字节流的时候需要需要转换为大端字节序且需要满足4字节对齐的情况,这个时候struct模块提供的格式控制符就不能满足需求了,无论是'>'控制符还是'!'控制符均以本机字节序和1字节对齐为准进行转换。那么要实现上述的需求,只能先转换为本机字节序的字节流,再进行字节序的转换。

# 本机字节序,4字节对齐
print(struct.pack("@BH", 1, 2))
# 大端字节序,1字节对齐
print(struct.pack(">BH", 1, 2))
# 本机字节序,1字节对齐
print(struct.pack("=BH", 1, 2))

输出:

b'\x01\x00\x02\x00'
b'\x01\x00\x02'
b'\x01\x02\x00'

实现方法: 字节序的不同本质上是数据在内存内部的存放顺序不同,要完成字节序的转换只要改变数据再内存中的存放顺序即可,python提供了memoryview模块让我们能够直接操作内存,实现方法如下:

import struct
import sys
# 查看本机字节序
print(sys.byteorder)
# 使用本机字节序进行转换
bytes_stream = struct.pack("@I", 2)
print("little ending strm: %s" % bytes_stream)
# memoryview只接受bytearray对象,此处需要转换
array_stream = bytearray(bytes_stream)
mem_str = memoryview(array_stream)
stream_len = mem_str.__len__()
print("msg len: %s" % stream_len)
# 改变内存内值的排列顺序
for ite in range(0, stream_len):
    tmp = mem_str[ite:ite + 1].tobytes()
    mem_str[ite:ite + 1] = mem_str[stream_len - ite - 1:stream_len - ite]