ctypes可变长度结构
自从我读了Dave Beazley关于二进制输入输出处理的文章后,我就一直想为某种通信协议创建一个Python库。不过,我找不到处理可变长度结构的最佳方法。以下是我想做的事情:
import ctypes as c
class Point(c.Structure):
_fields_ = [
('x',c.c_double),
('y',c.c_double),
('z',c.c_double)
]
class Points(c.Structure):
_fields_ = [
('num_points', c.c_uint32),
('points', Point*num_points) # num_points not yet defined!
]
这个Points
类不能工作,因为num_points
还没有定义。我可以在num_points
确定后重新定义_fields_
这个变量,但因为它是一个类变量,这样会影响到所有其他的Points
实例。
那么,有什么Python风格的解决方案可以解决这个问题呢?
7 个回答
3
这个问题真的很老了:
我有一个更简单的答案,虽然听起来有点奇怪,但它避免了使用元类,并解决了ctypes不允许我直接用C语言定义的结构体来构建的问题。
下面是一个来自内核的C语言结构体示例:
struct some_struct {
__u32 static;
__u64 another_static;
__u32 len;
__u8 data[0];
};
使用ctypes的实现:
import ctypes
import copy
class StructureVariableSized(ctypes.Structure):
_variable_sized_ = []
def __new__(self, variable_sized=(), **kwargs):
def name_builder(name, variable_sized):
for variable_sized_field_name, variable_size in variable_sized:
name += variable_sized_field_name.title() + '[{0}]'.format(variable_size)
return name
local_fields = copy.deepcopy(self._fields_)
for matching_field_name, matching_type in self._variable_sized_:
match_type = None
for variable_sized_field_name, variable_size in variable_sized:
if variable_sized_field_name == matching_field_name:
match_type = matching_type
break
if match_type is None:
raise Exception
local_fields.append((variable_sized_field_name, match_type*variable_size))
name = name_builder(self.__name__, variable_sized)
class BaseCtypesStruct(ctypes.Structure):
_fields_ = local_fields
_variable_sized_ = self._variable_sized_
classdef = BaseCtypesStruct
classdef.__name__ = name
return BaseCtypesStruct(**kwargs)
class StructwithVariableArrayLength(StructureVariableSized):
_fields_ = [
('static', ctypes.c_uint32),
('another_static', ctypes.c_uint64),
('len', ctypes.c_uint32),
]
_variable_sized_ = [
('data', ctypes.c_uint8)
]
struct_map = {
1: StructwithVariableArrayLength
}
sval32 = struct_map[1](variable_sized=(('data', 32),),)
print sval32
print sval32.data
sval128 = struct_map[1](variable_sized=(('data', 128),),)
print sval128
print sval128.data
以及示例输出:
machine:~ user$ python svs.py
<__main__.StructwithVariableArrayLengthData[32] object at 0x10dae07a0>
<__main__.c_ubyte_Array_32 object at 0x10dae0830>
<__main__.StructwithVariableArrayLengthData[128] object at 0x10dae0830>
<__main__.c_ubyte_Array_128 object at 0x10dae08c0>
这个答案对我有用,主要有几个原因:
- 构造函数的参数可以被序列化,并且没有类型的引用。
- 我在StructwithVariableArrayLength的定义中定义了所有的结构。
- 对调用者来说,这个结构看起来和我直接在_fields_中定义数组是一样的。
- 我无法修改头文件中定义的底层结构,但我可以在不改变任何底层代码的情况下实现我的目标。
- 我不需要修改任何解析/打包逻辑,这个方法只做我想做的事情,就是构建一个带有可变长度数组的类定义。
- 这是一个通用的、可重用的容器,可以像我的其他结构一样被送入工厂。
我当然更希望头文件能接受一个指针,但这并不总是可能。之前的答案让我感到沮丧。其他的答案要么非常针对特定的数据结构,要么需要修改调用者的代码。
3
现在,我们来聊点完全不同的内容——
如果你只需要处理数据,可能最“Pythonic”的方式就是根本不使用ctypes来处理内存中的原始数据。
这种方法只需要用到struct.pack和.unpack来在数据进出你的应用时进行序列化和反序列化。“Points”类可以接受原始字节,并从中创建Python对象,还可以通过“get_data”方法将数据序列化。除此之外,它就是一个普通的Python列表。
import struct
class Point(object):
def __init__(self, x=0.0, y=0.0, z= 0.0):
self.x, self.y, self.z = x,y,z
def get_data(self):
return struct.pack("ddd", self.x, self.y, self.z)
class Points(list):
def __init__(self, data=None):
if data is None:
return
pointsize = struct.calcsize("ddd")
for index in xrange(struct.calcsize("i"), len(data) - struct.calcsize("i"), pointsize):
point_data = struct.unpack("ddd", data[index: index + pointsize])
self.append(Point(*point_data))
def get_data(self):
return struct.pack("i", len(self)) + "".join(p.get_data() for p in self)
14
最简单的方法,就是在你需要信息的时候定义结构。
一种简单的做法是在你使用它的地方创建类,而不是在模块的最开始。你可以把class
的内容放在一个函数里面,这样这个函数就像一个工厂一样。我觉得这样写最容易理解。
import ctypes as c
class Point(c.Structure):
_fields_ = [
('x',c.c_double),
('y',c.c_double),
('z',c.c_double)
]
def points_factory(num_points):
class Points(c.Structure):
_fields_ = [
('num_points', c.c_uint32),
('points', Point*num_points)
]
return Points
#and when you need it in the code:
Points = points_factory(5)
抱歉——这是C代码,它会为你“填充”值——这不是答案。我会再发一种方法。