【问题标题】:ctypes variable length structuresctypes 可变长度结构
【发布时间】:2011-08-10 17:55:28
【问题描述】:

自从我阅读了 Dave Beazley 关于二进制 I/O 处理的帖子 (http://dabeaz.blogspot.com/2009/08/python-binary-io-handling.html) 后,我就一直想创建一个 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 实例。

这个问题的pythonic解决方案是什么?

【问题讨论】:

  • 为什么需要结构?你不能只发送缓冲区吗?
  • 我能问一下 C struct 是什么样的吗?我假设 struct { size_t num_points; Point poits[]; } 但我可能错了(尤其是如果您的 C 代码使用 C99 之前的 hack 来实现灵活的数组成员行为)。
  • 没有一个等效的 C 结构体。需要建立一个缓冲区然后发送它。
  • @ChrisLutz 给出的结构定义实际上可以工作,只要你通过 malloc(sizeof(struct 不管)+bytes_used_by_array) 创建了一个指向它的指针。

标签: python python-3.x ctypes


【解决方案1】:

对于您提供的示例,最直接的方法是在您拥有所需信息时定义结构。

一个简单的方法是在你将要使用它的地方创建类,而不是在模块根目录 - 例如,你可以将 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 代码 - 这不是答案。将以另一种方式发布。

【讨论】:

  • 正如您最后指出的那样,这不适用于接收数据。这让我想到了另一种可能的解决方案,尽管该类返回一个适当大小的内部类。
【解决方案2】:

而现在,对于一些完全不同的东西—— 如果您只需要处理数据,那么“最 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)

【讨论】:

  • 使用 Beazley 方法的好处是我不必计算大小和打包/解包数据。
【解决方案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. 这是一个通用的、可重复使用的容器,可以像我的其他结构一样被送到工厂。

我显然更喜欢头文件带一个指针,但这并不总是可能的。这个答案令人沮丧。其他的非常适合数据结构本身,或者需要修改调用者。

【讨论】:

  • 喜欢这个答案,但我对 matching_location 变量感到困惑。在迭代 self._variable_sized_ 时,这个第三个值来自哪里?为了将变量字段添加到列表的末尾,在第一次迭代时,这应该等于“3”,不是吗?考虑到location 是用None 初始化的,3 来自哪里?
  • 你是对的,解释器说ValueError: need more than 2 values to unpack就在那里。这个的最终版本(有点像你的问题暗示的那样)省略了位置信息,并做了一个local_fields.append((variable_sized_field_name, match_type*variable_size))。这也意味着它需要通过在外部循环中循环 variable_sized 来反转排序,以获得与定义顺序相同的附加顺序。
【解决方案4】:

因此,就像在 C 中一样,您不能完全按照自己的意愿去做。 使用在 C 中执行您想要的结构的唯一有用方法是将其作为

struct Points {
   int num_points;
   Point *points;
}

并且有实用程序代码来分配您可以放置​​数据的内存。 除非你有一些安全的 maxsize,并且不想打扰那部分代码 (内存分配) - 代码的网络部分然后将只传输所需的 来自结构内部的数据,而不是整个结构。

要使用具有结构成员的 Python ctypes,该结构成员实际上包含指向数据所在位置的指针(因此,可能是可变长度) - 您还必须手动分配和释放内存(如果您正在填充它python 端) - 或者只是读取数据 - f 创建和销毁数据是在本机代码函数上完成的。

结构创建代码可以是:

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', c.POINTER(Point))
        ]

管理这些数据结构的创建和删除的代码可以是:

__all_buffers = {}
def make_points(num_points):
   data = Points()
   data.num_points = num_points
   buf = c.create_string_buffer(c.sizeof(Point) * num_points)
   __all_buffers[c.addressof(buf)] = buf
   p = Point.from_address(c.addressof(buf))
   data.points = c.pointer(p)
   return data

def del_points(points):
    del __all_buffers[c.addressof(m.points[0])
    points.num_points = 0 

使用 f 全局变量“__all_buffers”保持对 python创建的缓冲区对象,以便python不会破坏它 离开 make_points 结构。对此的替代方法是参考 libc(在unix上)或winapi,并自己调用系统的mallocfreefunctions

或者 - 你可以使用普通的旧“struct” Python 模块,而不是使用 ctypes - 如果您根本没有 C 代码,并且只是使用 ctypes “结构”方便。

【讨论】:

  • 这就是我在 C 代码中的表现。我希望有一种方法可以利用 Python 的动态特性。不过,您的答案很棒,涵盖了可能的解决方案!
【解决方案5】:

这是我到目前为止的想法(仍然有点粗糙):

import ctypes as c

MAX_PACKET_SIZE = 8*1024
MAX_SIZE = 10

class Points(c.Structure):
    _fields_ = [
        ('_buffer', c.c_byte*MAX_PACKET_SIZE)
    ]
    _inner_fields = [
        ('num_points', c.c_uint32),
        ('points', 'Point*self.num_points')
    ]

    def __init__(self):
        self.num_points = 0
        self.points = [0,]*MAX_SIZE

    def parse(self):
        fields = []
        for name, ctype in self._inner_fields:
            if type(ctype) == str:
                ctype = eval(ctype)
            fields.append((name, ctype))
            class Inner(c.Structure, PrettyPrinter):
                _fields_ = fields
            inner = Inner.from_address(c.addressof(self._buffer))
            setattr(self, name, getattr(inner, name))
        self = inner
        return self

    def pack(self):
        fields = []
        for name, ctype in self._inner_fields:
            if type(ctype) == str:
                ctype = eval(ctype)
            fields.append((name, ctype))
        class Inner(c.Structure, PrettyPrinter):
            _fields_ = fields
        inner = Inner()
        for name, ctype in self._inner_fields:
            value = getattr(self, name)
            if type(value) == list:
                l = getattr(inner, name)
                for i in range(len(l)):
                    l[i] = getattr(self, name)[i]
            else:
                setattr(inner, name, value)
        return inner

parsepack 方法是通用的,因此可以将它们移动到元类中。这将使它的使用几乎和我第一次发布的 sn-p 一样简单。

对此解决方案有何评论?仍在寻找更简单的东西,不确定是否存在。

【讨论】:

    【解决方案6】:

    您可以使用 ctypes 指针来执行此操作。

    C 结构

    struct some_struct {
        uint  length;
        uchar data[1];
    };
    

    Python 代码

    from ctypes import *
    
    class SomeStruct(Structure):
        _fields_ = [('length', c_uint), ('data', c_ubyte)]
    
    #read data into SomeStruct
    s = SomeStruct()
    ptr_data = pointer(s.data)
    for i in range(s.length):
        print ptr_data[i]
    

    【讨论】:

      【解决方案7】:

      如果您愿意考虑第三方软件包,您可以使用Construct

      让我们采用您提供的结构:

      import ctypes
      
      class CPoint(ctypes.Structure):
          _fields_ = [
              ('x',ctypes.c_double),
              ('y',ctypes.c_double),
              ('z',ctypes.c_double)
          ]
      

      使用 Construct 的语法,我们将等价定义如下:

      import construct
      
      Point = construct.Struct(
          "x" / construct.Float64l,
          "y" / construct.Float64l,
          "z" / construct.Float64l
      )
      

      我们可以检查它们是否相同:

      >>> point_coordinates = {"x": 3.14, "y": 2.71, "z": 1.41}
      >>> c_point = CPoint(**point_coordinates)
      >>> point = Point.build(point_coordinates)
      >>> bytes(c_point) == bytes(point)
      True
      

      现在我们根据Construct语法定义Points结构:

      Points = construct.Struct(
          "num_points" / construct.Int32ul,
          "points" / construct.Array(construct.this.num_points, Point)
      )
      

      Construct 会根据num_points 自动创建一个points 的数组。

      我们可以序列化一个Points结构:

      >>> Points.build({"num_points": 2, "points": [{"x": 3.14, "y": 2.71, "z": 1.41}, {"x": 1.73, "y": 1.20, "z": 1.61}]})
      b'\x02\x00\x00\x00\x1f\x85\xebQ\xb8\x1e\t@\xaeG\xe1z\x14\xae\x05@\x8f\xc2\xf5(\\\x8f\xf6?\xaeG\xe1z\x14\xae\xfb?333333\xf3?\xc3\xf5(\\\x8f\xc2\xf9?'
      

      或者反序列化:

      >>> res = Points.parse(b'\x02\x00\x00\x00\x1f\x85\xebQ\xb8\x1e\t@\xaeG\xe1z\x14\xae\x05@\x8f\xc2\xf5(\\\x8f\xf6?\xaeG\xe1z\x14\xae\xfb?333333\xf3?\xc3\xf5(\\\x8f\xc2\xf9?')
      >>> print(res)
      Container:
          num_points = 2
          points = ListContainer:
              Container:
                  x = 3.14
                  y = 2.71
                  z = 1.41
              Container:
                  x = 1.73
                  y = 1.2
                  z = 1.61
      

      当然还有访问结构字段:

      >>> for i in range(res.num_points):
      ...     print(res.points[i].x)
      ...
      3.14
      1.73
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-10-03
        • 2023-03-04
        • 2015-08-09
        • 1970-01-01
        • 2021-07-30
        相关资源
        最近更新 更多