Структуры переменной длины ctypes

С тех пор как я прочитал сообщение Dave Beazley по обработке двоичных операций ввода-вывода (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 решение этой проблемы?

Ответ 1

Самый простой способ, с примером, который вы дали, - определить структуру, только когда у вас есть необходимая информация.

Простым способом создания этого класса является создание класса в том месте, где вы его используете, а не в корне модуля - вы можете, например, просто поместить тело class внутри функции, которая будет действовать как factory - Я думаю, что это самый читаемый способ.

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, который "заполнит" значения для вас - это не ответ на них. WIll опубликует другой способ.

Ответ 2

Итак, как и в C, вы не можете делать то, что хотите. Единственный полезный способ работы со структурой, которая делает то, что вы хотите на C, - иметь ее как struct Points { int num_points; Point *points; }

И у вас есть код утилиты для размещения вашей памяти, где вы можете поместить свои данные. Если у вас нет безопасного maxsize и вы не хотите беспокоиться об этой части кода (выделение памяти) - сетевая часть кода будет передавать только необходимые данных изнутри структуры, а не всего.

Чтобы работать с типами Python со структурным членом, который фактически содержит указатель на то, где ваши данные (и, следовательно, может быть переменной длины), вам также придется выделять и освобождать память вручную (если вы ее заполняете сторона 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 (в unixes), либо winapi, а система вызовов malloc и free выполняет функции

ИЛИ - вы можете просто пойти с простым старым модулем "struct" Python, вместо использования ctypes - вдвойне, если у вас вообще не будет кода на C, и просто используйте ctypes для "structs" удобство.

Ответ 3

И теперь, для чего-то совершенно другого - Если все, что вам нужно, связано с данными, возможно, "самый Pythonic" способ не пытается использовать ctypes для обработки необработанных данных в памяти вообще.

Этот подход просто использует struct.pack и .unpack для serialiase/unserialize данных, когда он перемещается в/из вашего приложения. Класс "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)

Ответ 4

Вот что я до сих пор придумал (еще немного грубо):

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 являются общими, поэтому их можно перенести в метакласс. Это сделало бы его почти таким же простым, как и первый фрагмент.

Комментарии к этому решению? Все еще ищете что-то более простое, не уверен, что оно существует.

Ответ 5

Этот вопрос действительно, действительно, старый:

У меня более простой ответ, который кажется странным, но избегает метаклассов и устраняет проблему, из-за которой ctypes не позволяет мне напрямую строить структуру с тем же определением, что и я в C.

Пример C struct, исходящий из ядра:

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>

Этот ответ работает для меня по нескольким причинам:

  • Аргумент конструктора может быть маринован и не имеет ссылок на типы.
  • Я определяю всю структуру внутри определения StructwithVariableArrayLength.
  • Для вызывающего, структура выглядит идентично, как если бы я только что определил массив внутри _fields _
  • У меня нет возможности модифицировать базовую структуру, определенную в файле заголовка, и выполнить мои цели, не изменяя базовый код.
  • Мне не нужно изменять какую-либо логику parse/pack, это только делает то, что я пытаюсь сделать, что создает определение класса с массивом переменной длины.
  • Это универсальный контейнер многократного использования, который отправляется в factory, как и мои другие структуры.

Я бы предпочел, чтобы заголовочный файл взял указатель, но это не всегда возможно. Этот ответ был разочаровывающим. Другие были очень адаптированы к самой структуре данных или потребовали модификации вызывающего абонента.