ctypes可变长度结构

15 投票
7 回答
12238 浏览
提问于 2025-04-16 23:18

自从我读了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>

这个答案对我有用,主要有几个原因:

  1. 构造函数的参数可以被序列化,并且没有类型的引用。
  2. 我在StructwithVariableArrayLength的定义中定义了所有的结构。
  3. 对调用者来说,这个结构看起来和我直接在_fields_中定义数组是一样的。
  4. 我无法修改头文件中定义的底层结构,但我可以在不改变任何底层代码的情况下实现我的目标。
  5. 我不需要修改任何解析/打包逻辑,这个方法只做我想做的事情,就是构建一个带有可变长度数组的类定义。
  6. 这是一个通用的、可重用的容器,可以像我的其他结构一样被送入工厂。

我当然更希望头文件能接受一个指针,但这并不总是可能。之前的答案让我感到沮丧。其他的答案要么非常针对特定的数据结构,要么需要修改调用者的代码。

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代码,它会为你“填充”值——这不是答案。我会再发一种方法。

撰写回答