LINUX.ORG.RU

Как вычислить crc структуры в complie-time?

 , c++17


0

2

Привет, лор.

Речь про плюсы. Хочу разместить в начале прошивки структуру, содержащую некоторую инфу. Типа такого:

// вот упрощённая структура
struct AppInfo
{
    uint32_t signature;
    uint32_t version;
    char name[20];
    uint32_t crc;
}

// и вот я пытаюсь её снабдить CRC:
constexpr AppInfo appInfo __attribute__((section(".app_info"), used)) =
{
    0x0002,
    0x0001,
    "My Device",
    calcCrc32(&appInfo, offsetof(AppInfo, crc))
};

// вот как выглядит функция calcCrc32:
static constexpr uint32_t calcCrc32(const uint8_t* buf, size_t bufLen)
{
    // тут вычисление crc
}

И вот тут ну никак не получается. Компилятор отказывается преобразовывать указатель в const uint8_t* в compile-time. Я нагуглил кучу вопросов про это, и ни одного работающего решения. Вроде как в c++26 обещают что-то подкрутить. Единственное (очень костыльное) решение, которое я придумал - превратить структуру в массив uint8_t:

struct AppHeader
{
    uint8_t bytes[128];
    uint32_t crc;
};

constexpr AppInfo appInfo __attribute__((section(".app_info"), used)) =
{
    .bytes =
        "\0"
        "\1"
        "My Device",
    .crc = calcCrc32(appInfo.bytes, sizeof(appInfo.bytes))
};```

Это коряво, придётся потом вручную парсить массив байтов, вычленяя элементы. Но деваться некуда, таков уж современный c++. Или так, или добавлять в процесс сборки отдельный скрипт для подсчёта crc32. Грустно.
★★

Последнее исправление: Beewek (всего исправлений: 1)
Ответ на: комментарий от RectalReactor

gcc умеет во все архитектуры.

Если кому-то хочется пердолится с проприетарными компиляторами или шлангом - то это его проблемы. Хотя шланг вроде тоже умеет безымянные struct и union.-

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 2)
Ответ на: комментарий от RectalReactor

union сам по себе непортабельный, так как размещение элементов структуры implementation defined. А по теме — очередное натягивание совы на глобус. Всю дорогу такие вещи делались внешними утилитами на дополнительном шаге сборки. Если нынешний стандарт и предусматривает возможность вычислений в compile time, совершенно не обязательно ей пользоваться именно для такого.

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

alegz ★★★★
()
Ответ на: комментарий от RectalReactor

Нормальный выход — это как раз парсить байты ручками

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

Beewek ★★
() автор топика
Ответ на: комментарий от alegz

Тут корень проблемы не в union, а в том, что запрещено преобразовывать const void* в const char* в constexpr функцях. Вот и получается, что мы не можем в constexpr-функции пройтись по байтам любого объекта, отличного от массива char.

Beewek ★★
() автор топика
Последнее исправление: Beewek (всего исправлений: 1)
Ответ на: комментарий от Beewek

Что же тут нормального?

Нет зависимости от порядка байтов и того, как компилятор padding расставит. Можно будет руками упаковать как надо.

Если дело не в байтах, то почему не посчитать CRC от полей структуры по очереди?

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

Лямбда‐калькулус тоже Тьюринг‐полный, почему ты на нём CRC не считаешь?

RectalReactor
()
Ответ на: комментарий от RectalReactor

Нет зависимости от порядка байтов и того, как компилятор padding расставит. Можно будет руками упаковать как надо.

Меня порядок байтов и паддинги не волнуют. У меня не обмен между разными устройствами, а доступ к одной структуре в пределах одного устройства. Поэтому компилятор строго один и тот же, и архитектура одна и та же.

Я не понимаю, почему в рантайме допустимо преобразовывать void* в char*, а в compile-time - нет. Уж на что у раста строгий компилятор, и тот позволяет сказать: здесь - unsafe, и делать что нужно. А в плюсах - категорически никак.

Beewek ★★
() автор топика
Ответ на: комментарий от kvpfs_2

Покопался ещё, получилось в имеющемся компиляторе на 17 версии стандарта, используя __builtin_bit_cast(S, *buf) вместо std::bit_cast<S>(*buf). Ну хоть что-то.

Есть два недостатка:

  1. Любой неинициализированный член структуры приводит к ошибке «error: accessing uninitialized array element».
  2. Строки (char description[41]) не работают никак - «error: accessing uninitialized array element».
Beewek ★★
() автор топика
Ответ на: комментарий от Beewek

Не совсем понял в каком месте ошибка, но может дело в этом:

constexpr AppInfo appInfo __attribute__((section(".app_info"), used)) =
{
    0x0002,
    0x0001,
    "My Device",
    calcCrc32(&appInfo, offsetof(AppInfo, crc))
};

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

struct AppInfo
{
    uint32_t signature;
    uint32_t version;
    char name[20];
};
constexpr AppInfo app_info_inst_0 {...};
constexpr uint32_t appInfo_crc_inst_0 = ...;

PS: я твой пример со своей calcCrc32() скомпилил, потом заменил bit_cast на __builtin_bit_cast, ошибок не было. Но поля лучше разнести

kvpfs_2
()
Последнее исправление: kvpfs_2 (всего исправлений: 1)
Ответ на: комментарий от kvpfs_2

Какая-то дерьмовая идея инициализировать член структуры функцией от других членов тоё же структуры

Я на одно сообщение выше на это ответил, можно crc и отдельно, не суть. Главное - его посчитать.

(Хотя я не вижу препятствий к использованию уже инициализированных элементов структуры при инициализации других. Члены структуры не зависят друг от друга. (Вот мини-тест).

А ошибка в __builtin_bit_cast(), видимо потому, что мой компилятор ещё не поддерживает c++20 - ещё не допилили.

Beewek ★★
() автор топика
Последнее исправление: Beewek (всего исправлений: 2)
Ответ на: комментарий от Beewek

(Хотя я не вижу препятствий к использованию уже инициализированных элементов структуры при инициализации других. Члены структуры не зависят друг от друга.

Ну в общем как бы да

In list-initialization, every value computation and side effect of a given initializer clause is sequenced before every value computation and side effect associated with any initializer clause that follows it in the brace-enclosed comma-separated list of initializers.

Но как бы берется указатель на весь объект и копируется объект целиком вместе с полем crc (в случае с bit_cast), которое инициализируется в данный момент

kvpfs_2
()
Ответ на: комментарий от Beewek

Если это при том, что ты структуру в юнионе проинициализровал, то что-то куда-то не туда плюсы топают. А если попробовать стандарт С++ пониже выставить?

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 1)
Ответ на: комментарий от Stanson

Если это при том, что ты структуру в юнионе проинициализровал

Да, компилятор отслеживает «активный» элемент union-а, и при обращении к другому ругается. Следит за aliasing-ом, типа.

что-то куда-то не туда плюсы топают.

В целом мне нравится, куда они топают. Но есть и неприятные моменты, да. Хотя вот в 20 стандарте уже придумали для таких штук bit_cast(). Молодцы.

Beewek ★★
() автор топика
Ответ на: комментарий от Stanson

В С++ всегда только последний записанный элемент в union мог быть валидным. По моему это исправили только в каком то из последних стандартов, и стало как в С, а может наоборот точно точно описали, в общем найдешь если интересно.

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от Stanson

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

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

Есть как минимум два варианта, как на хост машине, или как на машине цели.

Поведение в constexpr контексте должно совпадать с поведением на целевой машине.

m0xf
()
Ответ на: комментарий от Stanson

Тебе компилятор не даёт откровенное говнище написать, а ты всё равно руку поглубже в сортир запихиваешь, чтобы аж со дна самого вкусного зачерпнуть; ещё и черпак требуешь. Настоящий инженер, низкий поклон!!

RectalReactor
()
Ответ на: комментарий от rumgot

Ну, допустим, одиночный uint32_t я смогу преобразовать в массив char. А как перебрать всю структуру? Это уже что-то типа рефлексии нужно, как я понимаю. На такие подвиги я не готов:)

Beewek ★★
() автор топика
Ответ на: комментарий от Beewek

А ошибка в __builtin_bit_cast(), видимо потому, что мой компилятор ещё не поддерживает c++20 - ещё не допилили.

Я так полагаю у тебя gcc 11. Непосредственно приведённый пример c crc32 я воспроизвести не смог, там с выравниванием всё ок, но у 11-го gcc есть проблема с паддингами в __builtin_bit_cast.

Это можно обойти, но паддинги нужно явно прописать и инициализировать. Соответственно нужен constexpr конструктор.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Ну и если у тебя 11-ый gcc, то там std::bit_cast уже есть, если включить c++20, так что если у ты завязан именно на компилятор, а не на стандарт, то имеет смысл включить 20-ый стандарт, хоть он и не полностью поддерживается.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Beewek

А как перебрать всю структуру? Это уже что-то типа рефлексии нужно, как я понимаю. На такие подвиги я не готов:)

А вы готовы учитывать возможные паддинги для полей ваших структур? Если между полями окажутся «дырки» из-за выравнивания, то в compile-time вы можете получить один CRC, если считаете CRC по всем байтам, а в run-time – другой, т.к. в run-time эти самые «дырки» окажутся забиты другими значениями.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Вообще обычно это не проблема, т.к. всё применение у таких штук - это считать побайтово с флеша в память, привести память к структуре и проверить crc. А дальше уже брать версию и т.п.
В рантайме что-то создавать/менять для таких штук как правило не надо.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Я понимаю. Но, кмк, у нас есть вот какая ситуация. Допустим, есть тип:

struct header {
  std::uint32_t a;
  std::uint8_t b;
  std::uint16_t c;
};

constexpr header g_header{ ... };

и нам нужно подсчитать в compile-time CRC для g_header при том, что копия g_header будет сохранена где-то в ro-data и будет доступна в run-time.

Так вот, копия g_header из ro-data, скорее всего, будет иметь нулевой байт между header::b и header::c.

Тогда как значение g_header в compile-time, когда мы считаем CRC, не обязано иметь там ноль. Т.к. это значение конструируется в памяти работающего компилятора, там может оказаться какой-нибудь мусор. Этот мусор будет учтен при подсчете CRC если TC тупо бежит по всем байтам g_header.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

что копия g_header будет сохранена где-то в ro-data и будет доступна в run-time

Ну в таком случае возможно, зависит от того, как будет инициализироваться ro-data в раме. Если простым memcpy для всего сегмента, то проблемы не будет. В паддингах будет тоже, что было в compile-time.

Но используется это как правило не так. Обычно такие штуки кладут не в rodata, а в конкретный сегмент с фиксированным адресом на флешке, в eeprom и т.п. И считывается он вручную по необходимости. А тогда можно спокойно считывать всю структуру вместе с паддингами.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Я так полагаю у тебя gcc 11

У меня даже новее: arm-none-eabi-gcc (Arm GNU Toolchain 12.3.Rel1 (Build arm-12.35)) 12.3.1 20230626, но при попытке включить c++20 валятся ошибки в каких-то библиотеках.

Это можно обойти, но паддинги нужно явно прописать и инициализировать.

Спасибо, попробую!

Beewek ★★
() автор топика
Ответ на: комментарий от eao197

В моём случае паддинги не могут навредить подсчёту crc, потому что crc считается однократно по уже сформированному образу флеша, зашивается в него же, и проверяется именно по этому образу. Но это при нормальном, побайтовом подсчёте crc. Если пойти сложным путём, с рефлексией и разбиением каждого члена структуры на байты, то тут да, мусор в дырках между членами будет не учтён. Придётся либо упаковывать структуру, либо при проверке использовать такой же хитровывернутый алгоритм.

Beewek ★★
() автор топика
Ответ на: комментарий от Ivan_qrt

Да, дело было в выравнивании. Мне хватило задать длины строк кратные 4, и указать инициализаторы для строк:

struct AppHeader
{
	uint32_t signature;
	uint32_t hwVersion;
	uint32_t flags;
	char description[32] {};
	uint32_t crc;
};

constexpr AppHeader appHeader __attribute__((section(".app_header"), used)) =
{
	0x12345678,
	2,
	0,
	"My cool app",
	.crc = compileCrc32(&appHeader, offsetof(AppHeader, crc))
};

Ещё раз спасибо за подсказку.

Beewek ★★
() автор топика
Последнее исправление: Beewek (всего исправлений: 1)
Ответ на: комментарий от Beewek

Поскольку я пометил тему как решённую, выложу окончательный вариант функции подсчёта crc32 в compile-time:

static constexpr uint32_t crcTable[256] = {
... почикал, не суть
};

template <typename T>
static constexpr uint32_t compileCrc32(const T* t, size_t bufLen)
{
	uint32_t crc32 = 0xFFFFFFFF;
	struct S {
		uint8_t b[sizeof(T)] {};
	} bytes = __builtin_bit_cast(S, *t);

	for (size_t i = 0; i < bufLen; ++i)
	{
		crc32 = (crc32 >> 8) ^ crcTable[(crc32 ^ bytes.b[i]) & 0xFF];
	}
	return crc32 ^ 0xFFFFFFFF;
}

Beewek ★★
() автор топика
#include <cstring>
#include <iostream>
#include "inttypes.h"
#include <array>
using namespace std;

template<typename UIntT>
constexpr std::array<uint8_t, sizeof(UIntT)> to_bytes(UIntT v){
    const size_t len = sizeof(UIntT);
    std::array<uint8_t, len> ret{};
    for(size_t i=0; i<len; i++) {
        ret[i] = static_cast<uint8_t>(v>>8*i & 0xff);
    }
    return ret;
}
constexpr std::array<uint8_t, sizeof(uint32_t)> uint32_t_to_bytes(uint32_t v) {
    return to_bytes<uint32_t>(v);
} //

typedef char InfoStr[20];
constexpr std::array<uint8_t, sizeof(InfoStr)> InfoStr_to_bytes(const char* str){
    std::array<uint8_t, sizeof(InfoStr)> ret{}; size_t i=0;
    while(str[i] && i<sizeof(InfoStr)) { ret[i] = static_cast<uint8_t>(str[i]); i++; }
    return ret;
}
// здесь задаём поля и значения(и только здесь)
#define APPINFO_MEMBS \
    X(sign, 2, uint32_t)  \
    X(vers, 1, uint32_t)  \
    X(name, "My Device", InfoStr)\
    X(crc, 0, uint32_t)
//================================
static constexpr uint32_t calcCrc32(const uint8_t* buf, size_t bufLen) { return 42; }

constexpr size_t app_info_sz() {
    size_t len = 0;
    #define X(m, v, T) len += sizeof(T);
        APPINFO_MEMBS
    #undef X
    return len;
}
constexpr std::array<uint8_t, app_info_sz()> make_app_info(){
    std::array<uint8_t, app_info_sz()> ret{}; size_t pos = 0;
    #define X(m, v, T) \
        auto m##_bs = T##_to_bytes(v); \
        for(size_t i=0; i<m##_bs.size(); i++) { ret[pos+i] = m##_bs[i];} \
        pos += m##_bs.size();
    APPINFO_MEMBS
    #undef X
    uint32_t crc = calcCrc32(&ret[0], app_info_sz()-sizeof(crc));
    auto crcbs = uint32_t_to_bytes(crc);
    for(size_t i=0; i<crcbs.size(); i++) { ret[pos-sizeof(crc)+i] = crcbs[i];}
    return ret;
}
struct AppInfo {
    #define X(m, v, T) T m;
    APPINFO_MEMBS
    #undef X
    AppInfo(const std::array<uint8_t, app_info_sz()> &bys){
        size_t pos = 0;
        #define X(m, v, T) \
        memcpy(&this->m, &bys[pos], sizeof(T)); pos += sizeof(T);
        APPINFO_MEMBS
        #undef X
    }
};

int main() {
    const auto ap_inf_bs = make_app_info();

    auto app_inf = AppInfo(ap_inf_bs);
    std::cout<<"name: "<<app_inf.name<<", vers: "<<app_inf.vers<<", crc:"<<app_inf.crc<<endl;
}

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

zurg
()