python与C结构体之间二进制数据转换
python与C结构体之间数据转换
前言
在实际应用中,可能会遇到直接和C进行二进制字节流协议通信,这时要把数据解包成python数据,如果可能,最好与C定义的结构体完全对应上.
python中有2种方式,可处理二进制数据转换
- 用ctypes包的
Structure直接定义结构体 - 用struct包的
pack/unpack函数组装转换
在转换时一定要注意字节序,这两种方式都有各自的方法标志字节序.
使用ctypes包
ctypes中有许多C中的操作接口,如sizeof,memmove等,也提供近似C结构体的模拟类Structure,BigEndianStructure,Union,显然的是BigEndianStructure是网络字节序(大端),方便直接用于网络传输,Union和Structure是主机序(可能是大端,也可能是小端,和本机有关).
Structure/BigEndianStructure使用
from ctypes import *
class SSHead(BigEndianStructure):
_pack_ = 1
_fields_ = [
#(字段名, c类型 )
('nTotalSize', c_uint32),
('nSourceID', c_int32),
('sourceType', c_uint8),
('destType', c_uint8),
('transType', c_uint8),
('nDestID', c_int32),
('nFlag', c_uint8),
('nOptionalLength', c_uint16),
('arrOptional', c_char * 20),
]
def encode(self):
return string_at(addressof(self), sizeof(self))
def decode(self, data):
memmove(addressof(self), data, sizeof(self))
return len(data)
# -------------------
# 使用
sshead = SSHead()
sshead.nSourceID = 20 #省略其他赋值
buf = sshead.encode()
ss = SSHead()
ss.decode(buf)
print(ss.nSourceID)
以上就是一个简单协议结构体定义,对应的C版本如下
struct SSHead
{
uint32_t nTotalSize;
int32_t nSourceID;
uint8_t sourceType;
uint8_t destType;
uint8_t transType;
int32_t nDestID;
int8_t nFlag;
uint16_t nOptionalLength;
char arrOptional[20];
//简单模拟python的打包解包
int encode(char* buf, size_t max_len)
{
memmove(buf, this, sizeof(this));
return 0;
}
int decode(char* buf, size_t len)
{
memmove(this, buf, len);
return 0;
}
}
// c中对应的 打包/解包流程(假设本机字节序为大端)
SSHead sshead = {0};
sshead.nSourceID = 20;
char buf[1024];
sshead.encode(buf);
SSHead ss = {0};
ss.decode(buf, sizeof(ss));
其中_pack_ = 1表示1字节对齐,不然可能会被填充,导致结构体实际所占字节数与表面上的不一样.
_fields_定义C结构体中相对应的字段名和类型,C中每种基础类型在ctypes都有与之对应的类型,如c_uint32对应uint32_t,占4个字节.数组就是后面乘以对应的长度即可,如c_uint8 * 20.另外还支持嵌套定义结构体.在实例化后,字段名会成为成员变量,可直接赋值.
encode会直接得到该对象的二进制数据,如果不考虑字节序,则与C中相同对象的二进制数据是一样的
decode相反,直接解包二进制数据为python数据
这样python和c就可以直接通过结构体定义协议通信了.
注意
- python中的二进制数据是bytes类型,不是str类型
- 在python3.6及之前的版本,是没有
BigEndianUnion类型 - 用来网络传输一定要用
BigEndianStructure,不然会有字节序问题
缺点
此方法只能适用于结构体固定打解包的情况,如果协议中有大数组,但数组中的数据只有前几个是有效的,后面都是无效的,一般在打包的时候只打包有效数据,这种情况用Structure就不合适了.
使用struct包
struct模块是专门用来处理python与C之间的二进制数据转换,总共只有几个函数
下面在原有的SSHead定义中增加2个使用struct打包解包的函数
from ctypes import *
import struct
class SSHead(BigEndianStructure):
_pack_ = 1
_fields_ = [
#(字段名, c类型 )
('nTotalSize', c_uint32),
('nSourceID', c_int32),
('sourceType', c_uint8),
('destType', c_uint8),
('transType', c_uint8),
('nDestID', c_int32),
('nFlag', c_uint8),
('nOptionalLength', c_uint16),
('arrOptional', c_char * 20),
]
def encode(self):
return string_at(addressof(self), sizeof(self))
def decode(self, data):
memmove(addressof(self), data, sizeof(self))
return len(data)
def pack(self):
buffer = struct.pack("!IIBBBIBH20s", self.nTotalSize, self.nSourceID, self.sourceType
, self.destType, self.transType, self.nDestID, self.nFlag, self.nOptionalLength, self.arrOptional)
return buffer
def unpack(self, data):
(self.nTotalSize, self.nSourceID, self.sourceType, self.destType, self.transType, self.nDestID,
self.nFlag, self.nOptionalLength, self.arrOptional) = struct.unpack("!IIBBBIBH20s", data)
# ---------------------------
# 测试
s = SSHead()
s.arrOptional = b'hello'
ss = SSHead()
ss.unpack(s.encode())
print(ss.arrOptional)
pack/unpack的fmt(格式化串)说明
"!IIBBBIBH20B":!表示按照网络序处理,I表示后面的第一变量为4字节的int型,接着的B表示为下一个变量为1字节的uint8_t型,以此类推,20s表示后面是长度20的字节数组
其他参数可参考官方文档.
缺点
上面的例子中如果使用pakc/unpack方法,是不用继承BigEndianStructure,只需自定义相应字段变量.
可以看到,struct.pack/unpack必须对每个字段代表什么类型,几个字节进行描述.与Structure相比,比较灵活,可以自由组合怎么打包,比如在nOptionalLength=0时,不打包arrOptional字段.缺点就是,定义pack/unpack函数时,协议多起来会非常繁琐且容易出错.所以最好是自动化生成pack/unpack函数.
自动化生成pack/unpack
定义结构体成员列表
显然,我们需要知道结构体成员的变量名和类型,参考Structure,有如下定义
class BaseCode(object):
_type_map_index_pack_tag = 1
_type_map_index_pack_size = 2
_type_map = {
# C类型:(说明, 编码标志)
'char': ('int', 'B'),
'uint32_t': ('int', 'I'),
'string': ('str', 'B'),
'int32_t': ('int', 'i'),
'int64_t': ('int', 'q'),
'uint64_t': ('int', 'Q'),
'float': ('float', 'f'),
'double': ('double', 'd'),
}
# 每种基础类型所占字节数
_ctype_size_map = {'I': 4, 'B': 1, 'i': 4, 'b': 1, 'Q': 8, 'q': 8, 'f': 4, 'd': 8}
_fields_index_ctype = 0
_fields_index_value_name = 1
_fields_index_array_length = 2
# 测试
_fields = [
# (C类型, 变量名)
('uint32_t', 'nUint'),
('string', 'szString', '_Const.enmMaxAccountIDLength'),
('int32_t', 'nInt3'),
('uint32_t', 'nUintArray', 4),
]
按序遍历_fields中的字段
对_fields中的每个元素,进行编码,通过变量名可获得实际变量值,通过C类型利用struct.pack/unpack可获得实际编码
下面是添加的类成员函数encode
def encode(self, nest=1):
data = b''
tmp = b''
debug_log("&" * nest, self.__class__.__name__, "encode struct start :")
for one in self._fields:
debug_log("#" * nest, "encode one element:", one)
ctype = one[self._fields_index_ctype]
value = getattr(self, one[self._fields_index_value_name])
if len(one) == 3:
length = one[self._fields_index_array_length]
if type(length) == str:
length = eval(length)
tmp = self._encode_array(ctype, value, length)
else:
# 不是基础类型,即嵌套定义
if ctype not in BaseCode._type_map:
tmp = value.encode(nest+1)
else:
fmt = '!' + self._type_map[ctype][self._type_map_index_pack_tag]
tmp = struct.pack(fmt, value)
# debug_log(fmt, type(value), value)
debug_log("#" * nest,"encode one element:", len(tmp), tmp)
data += tmp
debug_log("&" * nest, self.__class__.__name__, "encode end: len=", len(data), data)
return data
def _encode_array(self, ctype, value, max_length):
"""
打包数组
如果是字符串类型 需要做下特殊处理
:param ctype:
:param value:
:param max_length:
:return:
"""
debug_log('ctype:', ctype, type(ctype))
if ctype == 'string':
max_length -= 1 # 字符串长度需要减一
value = bytes(value, encoding='utf8')
#print(value)
if len(value) > max_length:
raise EncodeError('the length of array is too long')
# pack长度
data = struct.pack('!H', len(value))
debug_log("array count:", len(value), "value:", value, type(value))
# pack数组内容
for one in value:
#debug_log("self._type_map[ctype][1]=", self._type_map[ctype][self._type_map_index_pack_tag], one)
if ctype not in BaseCode._type_map:
data += one.encode()
else:
data += struct.pack('!' + self._type_map[ctype][self._type_map_index_pack_tag], one)
return data
数组类型在python中使用list表示,在打包数组类型之前会添加2字节表示数组长度
字符串类型转换为bytes类型,然后就和普通数组一样,一个元素一个元素处理(实际在for遍历中,一个元素是一个int,和C中一样,所以用B标志打包)
当c类型不是_type_map中的基础类型,那就是自定义的结构体类型,然后嵌套调用encode就可以了
目前没有考虑union的处理
解码,反向处理
def decode(self, data, offset=0, nest=1):
"""
:param data:
:return:
"""
debug_log("&" * nest, self.__class__.__name__, "decode struct start :")
for one in self._fields:
debug_log("#" * nest, "decode one element:", one)
ctype = one[self._fields_index_ctype]
if len(one) == 3:
offset = self._decode_array(one, data, offset, nest)
else:
ctype_attr = self._type_map[ctype]
if ctype not in BaseCode._type_map:
value = eval(ctype + '()')
offset = value.decode(data, offset, nest)
setattr(self, one[self._fields_index_value_name], value)
else:
fmt = '!' + ctype_attr[self._type_map_index_pack_tag]
value, = struct.unpack_from(fmt, data, offset)
offset += self._ctype_size_map[ctype_attr[self._type_map_index_pack_tag]]
debug_log(one, one[self._fields_index_value_name])
setattr(self, one[self._fields_index_value_name], value)
debug_log("#" * nest, "decode one element end:", offset, one)
return offset
def _decode_array(self, field, data, offset, nest):
ctype = field[self._fields_index_ctype]
array_num, = struct.unpack_from('!H', data, offset)
offset += 2
value = []
ctype_attr = self._type_map[ctype]
debug_log("$" * nest, "decode array count", array_num, field)
while array_num > 0:
array_num -= 1
if ctype not in BaseCode._type_map:
one = eval(ctype + '()')
offset = one.decode(data, offset, nest)
value.append(one)
else:
one, = struct.unpack_from('!' + ctype_attr[self._type_map_index_pack_tag], data, offset)
value.append(one)
offset += self._ctype_size_map[ctype_attr[self._type_map_index_pack_tag]]
if ctype == 'string':
# 这里是因为字符串是按照单个字符解包,会解成python的int,通过chr()转化为字符型
# value = [97,98]
# list(map(chr,value)) 后等于 ['a','b']
# ''.join() 就转成'ab'
value = ''.join(list(map(chr, value)))
value = bytes(value, encoding='latin1').decode('utf8')
setattr(self, field[self._fields_index_value_name], value)
debug_log("$" * nest, "decode array ok", array_num, field)
return offset
最后
完整代码:https://gitee.com/iclodq/codes/e81qfpxw3dnkju594yi6b90
包含简单测试和转成字典结构
在python3.5下运行成功
希望帮到各位!!
Buy me a coffee

python与C结构体之间二进制数据转换的更多相关文章
- FFMPEG中最要害的结构体之间的关系
FFMPEG中最关键的结构体之间的关系 http://www.myexception.cn/program/1404591.html FFMPEG中结构体很多.最关键的结构体可以分成以下几类: a) ...
- c++调用python系列(1): 结构体作为入参及返回结构体
最近在打算用python作测试用例以便对游戏服务器进行功能测试以及压力测试; 因为服务器是用c++写的,采用的TCP协议,当前的架构是打算用python构造结构体,传送给c++层进行socket发送给 ...
- Python与C++结构体交互
需求:根据接口规范,实现与服务端的数据交互 服务端结构体分包头.包体.包尾 包头C++结构体示例如下 typedef struct head { BYTE string1; BYTE string2; ...
- FFMPEG中最关键的结构体之间的关系
FFMPEG中结构体很多.最关键的结构体可以分成以下几类: a) 解协议(http,rtsp,rtmp,mms) AVIOContext,URLProtocol,URLContext主要 ...
- FFmpeg 结构体学习(八):FFMPEG中重要结构体之间的关系
FFMPEG中结构体很多.最关键的结构体可以分成以下几类: 解协议(http,rtsp,rtmp,mms) AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议 ...
- python面向对象与结构成员之间的关系
1面向对象结构分析:----面向对象整体大致分为两块区域:-------第一部分:静态字段(静态变量)部分-------第二部分:方法部分--每个区块可以分为多个小部分 class A: countr ...
- Swift中元组(Tuples),结构体(Struct),枚举(Enums)之间的区别
Swift有许多种存储数据方式,你可以用枚举(enums),元组(tuples),结构体(structs),类(classes),在这篇文章中我们将比较枚举.元组.结构体之间区别,首先从最简单的开始- ...
- C#.NET和C++结构体Socket通信与数据转换
最近在用C#做一个项目的时候,Socket发送消息的时候遇到了服务端需要接收C++结构体的二进制数据流,这个时候就需要用C#仿照C++的结 构体做出一个结构来,然后将其转换成二进制流进行发送,之后将响 ...
- socket编程相关的结构体和字节序转换、IP、PORT转换函数
注意:结构体之间不能直接进行强制转换, 必须先转换成指针类型才可以进行结构体间的类型转换, 这里需要明确的定义就是什么才叫强制转换. 强制转换是将内存中一段代码以另一种不同类型的方式进行解读, 因此转 ...
随机推荐
- 对OOP的理解
OOP是面向对象编程Object Oriented Programming,特征分别是封装.继承.多态.抽象. 封装:封装是指将对象信息状态通过访问权限修饰符隐藏在对象内部,不允许外部程序直接访问,如 ...
- CF587F-Duff is Mad【AC自动机,根号分治】
正题 题目链接:https://www.luogu.com.cn/problem/CF587F 题目大意 给出\(n\)个字符串\(s\).\(q\)次询问给出\(l,r,k\)要求输出\(s_{l. ...
- AT2390-[AGC016F]Games on DAG【状压dp,SG函数】
正题 题目链接:https://www.luogu.com.cn/problem/AT2390 解题思路 \(n\)个点的\(DAG\),\(m\)条边可有可无,\(1\)和\(2\)上有石头.求有多 ...
- 国庆七天假 不如来学学Vue-Router
Vue-Router 基本介绍 Vue-Router是Vue全家桶中至关重要的一个扩展化插件,使用它能够让我们的组件切换更加的方便,更加容易的开发前后端分离项目,目前Vue-Router版本已更新到4 ...
- 跳表--怎么让一个有序链表能够进行"二分"查找?
对于一个有序数组,如果要查找其中的一个数,我们可以使用二分查找(Binary Search)算法,将它的时间复杂度降低为O(logn).那查找一个有序链表,有没有办法将其时间复杂度也降低为O(logn ...
- Windows 11正式版来了,下载、安装教程、一起奉上!
Windows 11正式版已经发布了,今天给大家更新一波Win11系统的安装方法,其实和Win10基本一样,有多种方法. 安装Win11前请先查看电脑是否支持Win11系统,先用微软自家的PC H ...
- IPtable防火墙概念介绍
1.iptables安全优化 1.不配外网,做代理转发或者防火墙映射 2.并发过大,不建议开启防火墙 2.防火墙的工作流程: 按照配置规则的顺序自上而下,从前到后进行过滤 如果匹配上新规则,表明是阻止 ...
- 洛谷4219 BJOI2014大融合(LCT维护子树信息)
QWQ 这个题目是LCT维护子树信息的经典应用 根据题目信息来看,对于一个这条边的两个端点各自的\(size\)乘起来,不过这个应该算呢? 我们可以考虑在LCT上多维护一个\(xv[i]\)表示\(i ...
- js--标签语法的使用
前言 在日常开发中我们经常使用到递归.break.continue.return等语句改变程序运行的位置,其实,在 JavaScript 中还提供了标签语句,用于标记指定的代码块,便于跳转到指定的位置 ...
- PAT (Basic Level) Practice (中文)1061 判断题 (15分)
1061 判断题 (15分) 判断题的评判很简单,本题就要求你写个简单的程序帮助老师判题并统计学生们判断题的得分. 输入格式: 输入在第一行给出两个不超过 100 的正整数 N 和 M,分别是学生人数 ...