Russian Qt Forum
Ноябрь 22, 2024, 00:31 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: [1] 2 3 4   Вниз
  Печать  
Автор Тема: чтение и запись битовых структур данных  (Прочитано 21059 раз)
kambala
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4747



Просмотр профиля WWW
« : Апрель 23, 2021, 23:40 »

Есть некоторый бинарный формат, в котором данные имеют размер в битах. Синтетический пример: (тип int взят от балды)
Код
C++ (Qt)
struct A {
 int a; // 2 bits
 int b; // 15
};
 
struct B {
 A a;
 int b; // 26
 int c; // 1
 int d; // 53
};
 
struct C {
 int a; // 42
 B b[];
 int c; // 12
};
и так далее. Причем размер не всех полей известен на момент компиляции. Только точно известно, что не бывает полей шире, чем 64 бита.

1) Есть какие-то готовые библиотеки, которые смогут буфер с байтами превратить в заданные структуры? (ну и обратный процесс)
2) как описать эти структуры данных? Хочется чего-то более проверяемого на момент компиляции, чем обычный сишный bitfield. Какие трюки можно использовать на шаблонах/трейтах/концептах? Наверное, в идеале хотелось бы, чтобы правильный тип полю назначался в зависимости от заданной битовой ширины, что-то типа такого:
Код
C++ (Qt)
template<N, S>
struct Field {
 bool v // if N == 1
 int8_t v // if N <= 8 && S=signed
 uint8_t v // if N <= 8 && S=unsigned
 ...
};

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

Изучением C++ вымощена дорога в Qt.

UTF-8 has been around since 1993 and Unicode 2.0 since 1996; if you have created any 8-bit character content since 1996 in anything other than UTF-8, then I hate you. © Matt Gallagher
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #1 : Апрель 24, 2021, 07:13 »

Не слышал о таких либах. Если велик-вариант интересен, то

Если  "размер не всех полей известен на момент компиляции", то отделаться bitfield все равно не удастся. Я бы хранил в памяти "нормальные" члены класса (int, short, char), и нормальные вектора (если это не противоречит др требованиям). А для чтения/записи в биты нужно иметь таблицы битовых размеров, возможно мапа с ключом typeof()
Записан
Old
Джедай : наставник для всех
*******
Online Online

Сообщений: 4350



Просмотр профиля
« Ответ #2 : Апрель 24, 2021, 07:54 »

Причем размер не всех полей известен на момент компиляции. Только точно известно, что не бывает полей шире, чем 64 бита.
Т.е. в формате вместе со значением поля указывается и его размер в битах?
Или вы имеете ввиду, что пользователь этого формата должен иметь возможность выбирать любой размер поля от 1-64 бита?

https://github.com/CrustyAuklet/bitpacker
https://github.com/wkaras/C-plus-plus-library-bit-fields
« Последнее редактирование: Апрель 24, 2021, 08:05 от Old » Записан
Old
Джедай : наставник для всех
*******
Online Online

Сообщений: 4350



Просмотр профиля
« Ответ #3 : Апрель 24, 2021, 11:41 »

А вообще тема конечно обширная. Все зависит от того, что вы хотите: если вам нужны только pack/unpack - это будет одно решение. Если вы хотите хранить такие структуры в памяти и работать с ними из бизнес-логики, то другое.

Для примера:
Код
C++ (Qt)
// Схемы пакетов
auto ShemaPacket1 = std::make_tuple( 3, 3, 10, 52 );
auto ShemaPacket2 = std::make_tuple( 9, 11, 37, 60, 1, 1 );
 
// Возвращает упакованный bit_array
auto bits1 = pack( ShemaPacket1, 1, 2, 8, 1024 );
auto bits2 = pack( ShemaPacket1, data.val1, data.val2, data.val3, data.val4 );
auto bits3 = pack( ShemaPacket2, 15, 0, 0, 1024, 1, 0 );
 
// Возвращает кортеж значений по указанной схеме или exception
auto result1 = unpack( bits1, ShemaPacket1 )
auto [val1, val2, val3, val4] = unpack( bits1, ShemaPacket1 )
auto result3 = unpack( bits3, ShemaPacket2 )
 
 
« Последнее редактирование: Апрель 24, 2021, 11:48 от Old » Записан
AkonResumed
Чайник
*
Offline Offline

Сообщений: 81


Просмотр профиля
« Ответ #4 : Апрель 24, 2021, 15:58 »

В плане использования на уровне бизнес-логики - просто наложить placement new:
Код:
// Packed data
struct A {
  int a; // 2 bits
  int b; // 15
};

// State less business-logic class
class UnpackedA
{
public:
int a() const { return *reinterpret_cast<const int*>(buf()) & 0x3; }
int b() const { return *reinterpret_cast<const int*>(buf() + sizeof(int)) & 0x7FFF; }

private:
// Returns buffer of packed data
const char* buf() const { return reinterpret_cast<const char*>(this); }
};

int foo() {
A a;  // e.g. got from network
auto ua = new (&a) UnpackedA;
}

Это я привел вариант для чтения (распаковки) данных. Для записи (упаковки) - абсолютно тоже самое, только в другую сторону.

Да, и здесь класс бизнес логики предполагает время жизни буфера большим своего. Короче, этот класс - интерпретатор, создаваемый по месту. Ну как бы не совсем бизнес-логика, а скорее хелпер.
« Последнее редактирование: Апрель 24, 2021, 16:04 от AkonResumed » Записан
Racheengel
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2679


Я работал с дискетам 5.25 :(


Просмотр профиля
« Ответ #5 : Апрель 26, 2021, 14:09 »

MessagePack?

https://msgpack.org/
Записан

What is the 11 in the C++11? It’s the number of feet they glued to C++ trying to obtain a better octopus.

COVID не волк, в лес не уйдёт
kambala
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4747



Просмотр профиля WWW
« Ответ #6 : Апрель 26, 2021, 19:30 »

спасибо! отвечу по порядку.
Не слышал о таких либах. Если велик-вариант интересен, то

Если  "размер не всех полей известен на момент компиляции", то отделаться bitfield все равно не удастся. Я бы хранил в памяти "нормальные" члены класса (int, short, char), и нормальные вектора (если это не противоречит др требованиям). А для чтения/записи в биты нужно иметь таблицы битовых размеров, возможно мапа с ключом typeof()
велик интересен, если ничего пригодного не найдется Улыбающийся

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

как хранить "нормальные" члены класса не очень понял, ведь точно известно только то, что значение уместится в int64.
Причем размер не всех полей известен на момент компиляции. Только точно известно, что не бывает полей шире, чем 64 бита.
Т.е. в формате вместе со значением поля указывается и его размер в битах?
Или вы имеете ввиду, что пользователь этого формата должен иметь возможность выбирать любой размер поля от 1-64 бита?

https://github.com/CrustyAuklet/bitpacker
https://github.com/wkaras/C-plus-plus-library-bit-fields
да на выделенное. хотелось бы задавать размер поля прямо в описании структуры данных (для наглядности), а пользовательский код будет просто читать это значение при необходимости. Это не сверхкритично, иначе придется просто поместить эти размеры в код (де)сериализации.

вторая ссылка: открыл пдф документацию и... ничего не понял Улыбающийся файл с тестами тоже понимания особого не принес. также в единственном issue пишут, что
Цитировать
G++ uses more than 3GB of memory (I estimated 6, although I couldn't be sure as the memory monitor stopped working the moment thrashing started). CLang++ only uses up to 1.4 GB of memory although it can take 3-5 minutes too and use up one core fully (ensure you have proper cooling).
что как-то не особо приятно.

первая ссылка: вот тут вменяемая документация Улыбающийся даже удалось тестовый код написать. но там неприятная штука есть:
Цитировать
little endian byte order not yet implemented for packing/unpacking (yet...)
у меня данные как раз в little endian. для «обычных» чисел можно выкрутиться простым переворотом байт, а вот с битами оказалось чуть сложнее...

пример существующего кода, который «просто работает» (случай big endian архитектур не берем):
Код
C++ (Qt)
static const unsigned int BitsInByte = 8;
 
unsigned long readBits(unsigned char bits, unsigned char *buf, unsigned int *pBufPos)
{
   unsigned int bufPos = *pBufPos, newBufPos = bufPos + bits;
   unsigned int bufIndexOfFirstByte = bufPos / BitsInByte;
   unsigned int ignoreBitsInFirstByte = bufPos % BitsInByte;
   unsigned int fullByteStart = bufPos + (BitsInByte - ignoreBitsInFirstByte);
   unsigned int additionalBits = newBufPos - fullByteStart;
   unsigned int bitsInLastByte = additionalBits - (additionalBits / BitsInByte) * BitsInByte;
   unsigned int bufIndexOfLastByte = (newBufPos - 1) / BitsInByte + (bitsInLastByte ? 0 : 1);
 
   // start with remainder bits of the first byte
   unsigned long value = buf[bufIndexOfFirstByte] >> ignoreBitsInFirstByte;
   // add "full" bytes in between
   for (unsigned int fullByteIndex = fullByteStart / BitsInByte, lshiftBits = BitsInByte - ignoreBitsInFirstByte; fullByteIndex < bufIndexOfLastByte; ++fullByteIndex, lshiftBits += BitsInByte)
       value |= buf[fullByteIndex] << lshiftBits;
   // end with a piece of the last byte
   if (bitsInLastByte)
       value |= (buf[bufIndexOfLastByte] & ((1 << bitsInLastByte) - 1)) << (bits - bitsInLastByte);
 
   *pBufPos = newBufPos;
   return value;
}
 
int main()
{
 unsigned char buf[] = {0, 0x1E, 0x40};
 unsigned int pos = 0;
 std::cout << readBits(9, buf, &pos) << ' ' << readBits(13, buf, &pos) << '\n'; // 0 15
 return 0;
}
получить то же самое на этой либе получилось лишь с помощью переворота всех битов в буфере и с последующим чтением с LSB:
Код
C++ (Qt)
int main()
{
 std::array c = {std::byte{0}, std::byte{0x78}, std::byte{0x2}}; // swapped
 auto u = bitpacker::unpack(BP_STRING("<u9u13"), c); // < означает LSB
 std::cout << std::get<0>(u) << ' ' << std::get<1>(u) << '\n';
 return 0;
}
придется или ждать пока автор допилит поддержку LE (хотя уже год не было коммитов, только в другой ветке, но там другая тема) или самому ее добавлять (для начала разобрать этот супершаблонный код) или писать велик с блэкджеком и шлюхами Улыбающийся

для полей неизвестной ширины надо будет пользоваться функциями get / store (а может и известные тоже проще через эти функции), т.к. все равно надо будет делать обертки для порядка полей / автоматического сдвига счетчика.
А вообще тема конечно обширная. Все зависит от того, что вы хотите: если вам нужны только pack/unpack - это будет одно решение. Если вы хотите хранить такие структуры в памяти и работать с ними из бизнес-логики, то другое.
думаю, что лучше иметь сырые битовые данные в одних структурах («сырых»), которые и будут читаться/писаться из/в файл, а бизнес-логику на других, более понятных структурах, которые будут конвертироваться из/в «сырые» структуры.
В плане использования на уровне бизнес-логики - просто наложить placement new:

Это я привел вариант для чтения (распаковки) данных. Для записи (упаковки) - абсолютно тоже самое, только в другую сторону.

Да, и здесь класс бизнес логики предполагает время жизни буфера большим своего. Короче, этот класс - интерпретатор, создаваемый по месту. Ну как бы не совсем бизнес-логика, а скорее хелпер.
мне этот вариант не нравится тем, что в большинстве случаев я не могу работать с сырыми данными напрямую — их надо преобразовывать в более понятные пользователю (и коду) вещи.
насколько я понял, это что-то типа протобафа. такое не подходит, потому что у меня известная жесткая внутренняя структура (файла) — надо именно с таким бинарным форматом и работать.

сделал еще небольшую попытку повелосипедить (до того, как начал смотреть те библиотеки).
Код
C++ (Qt)
// C++17
#include <type_traits>
#include <iostream>
 
struct I
{
 virtual int64_t value() const = 0;
 virtual void print() const = 0;
};
 
template<int N>
struct A : public I
{
 static constexpr auto size = N;
 
 std::conditional_t<size == 1, bool,
   std::conditional_t<size <= 8, char, int>
 > v;
 
 A(decltype(v) vv) : v(vv) {}
 
 int64_t value() const override { return v; }
 void print() const override { std::cout << size << ' ' << std::boolalpha << v << '\n'; }
};
 
int main()
{
 A<1> a{true};
 A<8> b{'!'};
 A<10> c{42};
 
 static_assert(a.size == 1);
 static_assert(std::is_same_v<decltype(a.v), bool>);
 static_assert(std::is_same_v<decltype(b.v), char>);
 static_assert(std::is_same_v<decltype(c.v), int>);
 
 for (auto const i : std::initializer_list<I const *>{&a, &b, &c})
 {
   std::cout << i->value() << '\t';
   i->print();
 }
 
 return 0;
}
работает, но мне кажется можно как-то получше Улыбающийся потом еще заметил, что в BitPacker тот же трюк используется:
Код
C++ (Qt)
       /// finds the smallest fixed width unsigned integer that can fit NumBits bits
       template <size_type NumBits>
       using unsigned_type = std::conditional_t<NumBits <= 8, uint8_t,
               std::conditional_t<NumBits <= 16, uint16_t,
                       std::conditional_t<NumBits <= 32, uint32_t,
                               std::conditional_t<NumBits <= 64, uint64_t,
                                       void >>>>;
 
       /// finds the smallest fixed width signed integer that can fit NumBits bits
       template <size_type NumBits>
       using signed_type = std::conditional_t<NumBits <= 8, int8_t,
               std::conditional_t<NumBits <= 16, int16_t,
                       std::conditional_t<NumBits <= 32, int32_t,
                               std::conditional_t<NumBits <= 64, int64_t,
                                       void >>>>;

P.S. вопрос знатокам Улыбающийся как задать массив (обычный или std::array) из std::byte по-человечески? у меня получается только с явным вызовом конструктора для каждого элемента как в коде выше. ([unsigned] char предлагать не надо Улыбающийся )
Код
C++ (Qt)
std::array c = {std::byte{0}, std::byte{0x78}, std::byte{0x2}};
Записан

Изучением C++ вымощена дорога в Qt.

UTF-8 has been around since 1993 and Unicode 2.0 since 1996; if you have created any 8-bit character content since 1996 in anything other than UTF-8, then I hate you. © Matt Gallagher
juvf
Программист
*****
Offline Offline

Сообщений: 570


Просмотр профиля
« Ответ #7 : Апрель 27, 2021, 06:53 »

1) Есть какие-то готовые библиотеки, которые смогут буфер с байтами превратить в заданные структуры? (ну и обратный процесс)
Я делаю через юнион

Код:
struct LCanOpen
{
union
{
uint64_t data;
struct
{
int16_t x;
int16_t y;
int16_t rezerv;
struct
{
uint16_t nx :2;
uint16_t ny :2;
uint16_t rezerv1 :12;
};
};
};
};

struct Bjm
{
union
{
uint64_t data;
struct
{
uint64_t neutralX :2;
uint64_t left :2;
uint64_t right :2;
uint64_t x :10;
uint64_t neutralY :2;
uint64_t back :2;
uint64_t forward :2;
uint64_t y :10;
uint64_t rezerv :8;
uint64_t but4 :2;
uint64_t but3 :2;
uint64_t but2 :2;
uint64_t but1 :2;
uint64_t but8 :2;
uint64_t but7 :2;
uint64_t but6 :2;
uint64_t but5 :2;
uint64_t but12 :2;
uint64_t but11 :2;
uint64_t but10 :2;
uint64_t but9 :2;
};
};
};

При получении данных я не распихиваю каждый битик сдвигами в нужное поле, а сразу весь буфер помещаю в data

Код:
uint64_t recData;
uint8_t recBuf[100];

recive(port1, &recData);
recive(port2, recBuf);

struct Bjm bjm;
struct LCanOpen can;

bjm.data = recData;//заполенение структуры простым "="
memcpy(&can.data, &recbuf[13], 8); //заполнение структуры, в случае, если принятые данные не выровнены.

recive(port1, &can.data);
//обратный процесс
*(uint64_t*)recBuf = bjm.data;//опасно, но быстро.
//или
memcpy(recBuf+13, &bjm.data, 8);//безопасно
Такими юнионами легко одним "=" можно получить заполнить Bjm.x, который состоит из 10 бит и разбросан в 2-х байтах. Доступ

Код:
unsigned int x = bjm.x;
unsigned int y = can.ny


зы только сейчас заметил "Причем размер не всех полей известен на момент компиляции". Мой вариант тут наверно не подойдет.

« Последнее редактирование: Апрель 27, 2021, 06:54 от juvf » Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #8 : Апрель 27, 2021, 08:35 »

как хранить "нормальные" члены класса не очень понял, ведь точно известно только то, что значение уместится в int64.
"Нормальные" имелось ввиду что все песни с битами только для I/O, а так нормальные структуры. Пока неясно нужно ли битовое представление в памяти. Даже если "иногда нужно" то может проще использовать ф-ции I/O на лету, расходы невелики. Уточните этот момент чтобы не гонять воздух.

Если "только I/O" то надо заряжать какой-то CBitStream и операторы << и >> для каждой структуры, собсно это все.

велик интересен, если ничего пригодного не найдется
"Готовые проверенные" хороши для типовых, хорошо изученных задач. Отсюда частенько делается вывод что, мол, для всего , и "все уже давно написано" Улыбающийся Но стоит задаче быть просто "не такой уж популярной" - и число "готового" резко падает, а с "проверенным" проблемы. Напр здесь найденное особого впечатления не производит, хотя возможно и пригодно.
Записан
Old
Джедай : наставник для всех
*******
Online Online

Сообщений: 4350



Просмотр профиля
« Ответ #9 : Апрель 27, 2021, 08:47 »

Отсюда частенько делается вывод что, мол, для всего , и "все уже давно написано" Улыбающийся
Вы уже много лет делаете этот ошибочный вывод. Это не так. Улыбающийся
Но есть много проектов с интересными идеями, которые могут подтолкнуть. Не нужно тащить к себе и пытаться использовать все подряд, а просмотр может оказаться полезным.
Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #10 : Апрель 27, 2021, 09:28 »

Да, если нужны "и так и сяк", то есть приятная специфика - влазит в 64 бита. Напрашивается
Код
C++ (Qt)
struct A {
 int m_B;
 int m_C;
 ...
 qint64 m_bits;
}
И нахрюкать сеттеров которые обновляют m_bits;
Записан
kambala
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4747



Просмотр профиля WWW
« Ответ #11 : Апрель 27, 2021, 09:51 »

Я делаю через юнион
писать в одно поле юниона, а читать из другого законно только в С, в С++ же:
Цитировать
It's undefined behavior to read from the member of the union that wasn't most recently written. Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union.
оно может и работает в clang/gcc/msvc, но полагаться на это не хочется Улыбающийся
как хранить "нормальные" члены класса не очень понял, ведь точно известно только то, что значение уместится в int64.
"Нормальные" имелось ввиду что все песни с битами только для I/O, а так нормальные структуры. Пока неясно нужно ли битовое представление в памяти.
нет, это только для I/O:
думаю, что лучше иметь сырые битовые данные в одних структурах («сырых»), которые и будут читаться/писаться из/в файл, а бизнес-логику на других, более понятных структурах, которые будут конвертироваться из/в «сырые» структуры.
Да, если нужны "и так и сяк", то есть приятная специфика - влазит в 64 бита. Напрашивается
Код
C++ (Qt)
struct A {
 int m_B;
 int m_C;
 ...
 qint64 m_bits;
}
И нахрюкать сеттеров которые обновляют m_bits;
в 64 бита влезает одно поле, а структуры там побольше будут
Записан

Изучением C++ вымощена дорога в Qt.

UTF-8 has been around since 1993 and Unicode 2.0 since 1996; if you have created any 8-bit character content since 1996 in anything other than UTF-8, then I hate you. © Matt Gallagher
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #12 : Апрель 27, 2021, 10:24 »

нет, это только для I/O:
Тогда не вижу в чем проблема, и зачем что-то искать. Пишем операторы потока для каждого класса - и все дела

оно может и работает в clang/gcc/msvc, но полагаться на это не хочется
Ну хорошо хоть так Улыбающийся Не первый раз вижу это, и обычно делается вывод типа "юнионы не работают, применять их низзя" - таков обычный итог широких познаний Улыбающийся

в 64 бита влезает одно поле, а структуры там побольше будут
Ну пришлось бы разориться на array или vector, смысл тот же
Записан
kambala
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4747



Просмотр профиля WWW
« Ответ #13 : Апрель 27, 2021, 10:50 »

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

1) велосипед или готовое (частично ответили)
2) как сделать ширину поля частью структуры данных, а не частью парсера (попытки выше есть, комментариев к ним пока нет)
Записан

Изучением C++ вымощена дорога в Qt.

UTF-8 has been around since 1993 and Unicode 2.0 since 1996; if you have created any 8-bit character content since 1996 in anything other than UTF-8, then I hate you. © Matt Gallagher
juvf
Программист
*****
Offline Offline

Сообщений: 570


Просмотр профиля
« Ответ #14 : Апрель 27, 2021, 11:11 »

писать в одно поле юниона, а читать из другого законно только в С, в С++ же:
Цитировать
It's undefined behavior to read from the member of the union that wasn't most recently written. Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union.
оно может и работает в clang/gcc/msvc, но полагаться на это не хочется Улыбающийся
Писать в одно поле юниона, а читать из другово (в моих примерах) законно в любом языке. То, о чем говориться по вашей ссылке к моему случаю не имеет отношения.

на поальцах, про эту граблю.... если
Код:
union S
{
    std::int32_t n;     // occupies 4 bytes
    std::uint8_t c;     // occupies 1 byte
};
то записали в n одно, а читаем из с - получаем It's undefined behavior to read. Потому что sizeof(c) < sizeof(n).
Если будет
Код:
union S
{
    std::int32_t n;     // occupies 4 bytes
    std::uint32_t c;     // occupies 4 byte
    float m;     // occupies 4 byte
    MyType g; // occupies 4 byte
};
То ни каких undefined behavior to read не случиться. Вы можете безопасно писать  и читать в любой член юниона. Потому что размер S равен hold its largest data member. А размер любого member == largest data member и == размеру S.

В моем случае примере под юнионом объединены два члена: uint64_t и struct (битовое поле). Каждый член занимает 8 байт.
« Последнее редактирование: Апрель 27, 2021, 11:23 от juvf » Записан
Страниц: [1] 2 3 4   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.254 секунд. Запросов: 22.