问题 ctypes可变长度结构


自从我读到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 尚未定义。我可以重新定义 _fields_ 变量稍后一次 num_points 是已知的,但由于它是一个类变量,它会影响所有其他变量 Points 实例。

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


12684
2017-08-10 17:55


起源

你为什么需要一个结构?你不能只发送缓冲区吗? - David Heffernan
我可以问一下C struct 好像?我在假设 struct { size_t num_points; Point poits[]; } 但我可能错了(特别是如果你的C代码使用前C99黑客来实现灵活的阵列成员行为)。 - Chris Lutz
没有单一的C struct等价物。人们需要建立一个缓冲区然后发送它。 - Jake
@ChrisLutz给出的结构定义实际上可行,只要你通过malloc(sizeof(struct whatever)+ bytes_used_by_array)创建了一个指向它的指针。 - Michael


答案:


最简单的方法,您给出的示例是在您拥有所需信息时定义结构。

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


10
2017-08-10 18:46



正如您在最后所述,这对于接收数据无效。它让我想到了另一种可能的解决方案,尽管类返回了适当大小的内部类。 - Jake


而现在,对于完全不同的东西 - 如果您只需要处理数据,那么“最Pythonic”方式可能不会尝试使用ctypes来处理内存中的原始数据。

这种方法只是使用struct.pack和.unpack来串行/取消序列化数据,因为它可以在你的app上移动。 “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)

2
2017-08-10 20:14



使用Beazley方法的好处是我不必计算大小并打包/解包数据。 - Jake


所以,就像在C中一样,你无法完全按照自己的意愿行事。 使用在C中执行所需操作的结构的唯一有用方法是将其作为 struct Points { int num_points; Point *points; }

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

使用具有结构成员的Python ctypes,该结构成员实际上包含指向数据所在位置的指针(因此,可能是可变长度) - 您还必须手动分配和释放内存(如果您在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', 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 

使用全局变量“__all_buffers”保持对该引用的引用 python创建的缓冲区对象,以便python不会破坏它 离开make_points结构。另一种方法是获得参考 libc(在unixes上)或winapi,以及调用系统 malloc 和 free功能你自己

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


2
2017-08-10 19:41



这就是我在C代码中可以做到的。我希望有一种方法可以利用Python的动态特性。你的答案很棒,涵盖了各种可能的解决方案! - Jake


这是我到目前为止所提出的(仍然有点粗糙):

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

方法 parse 和 pack 是通用的,因此可以将它们移动到元类。这将使它的使用几乎像我第一次发布的片段一样简单。

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


1
2017-08-11 21:17





这个问题真的很老了:

我有一个更简单的答案,这似乎很奇怪,但避免了元类,并解决了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 variable_sized_field_name, variable_size in variable_sized:
            match_type = None
            location = None
            for matching_field_name, matching_type, matching_location in self._variable_sized_:
                if variable_sized_field_name == matching_field_name:
                    match_type = matching_type
                    location = matching_location
                    break
            if match_type is None:
                raise Exception
            local_fields.insert(location, (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. 构造函数的参数可以被pickle,并且没有对类型的引用。
  2. 我定义了StructwithVariableArrayLength定义中的所有结构。
  3. 对于调用者来说,结构看起来完全相同,好像我刚刚在_fields_中定义了数组一样
  4. 我无法修改头文件中定义的底层结构,并且无需更改任何底层代码即可实现我的目标。
  5. 我没有必要修改任何解析/包逻辑,这只是我正在尝试做的事情,即使用可变长度数组构建类定义。
  6. 这是一个通用的,可重复使用的容器,可以像我的其他结构一样发送到工厂。

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


1
2018-03-16 19:52