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: %dn", msg->name);
printf("stu class: %dn", msg->class);
printf("stu num: %fn", msg->num);
printf("stu age: %dn", msg->age);
memcpy(nest_msg->stu_array, msg, sizeof(stu));
printf("stu array index 0 name: %dn", nest_msg->stu_array[0].name);
printf("stu array index 0 class: %dn", 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语言数据类型的方法。
INnoVation:Python--ctypes(数据类型详细踩坑指南)zhuanlan.zhihu.com
- 指针类型
- 指针数组类型
- 结构体指针类型
- 结构体指针数组类型
- 函数指针
- 类型转换
- 获取函数返回值类型
二.处理字节流
在使用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'x01x00x00x00x02x00x03x00x00x00'
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'x01x00x02x00'
b'x01x00x02'
b'x01x02x00'
实现方法:字节序的不同本质上是数据在内存内部的存放顺序不同,要完成字节序的转换只要改变数据再内存中的存放顺序即可,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]
mem_str[stream_len - ite - 1:stream_len - ite] = tmp
if ite + 1 == stream_len - 1 - ite:
break
mem_bytes = mem_str.tobytes()
print("big ending strm: %s" % mem_bytes)
输出:
little
little ending strm: b'x02x00x00x00'
msg len: 4
big ending strm: b'x00x00x00x02'
此处演示只是对单个数据进行大小端处理,多数情况下一条字节流里可能含有多个数据,那样就需要根据fmt中每个数据的长度对字节流先进行切片,然后再进行大小端转换。