Обращение с порядком байтов универсально, а не мгновенно - PullRequest
0 голосов
/ 11 января 2020

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

В настоящее время я работаю с порядком байтов при помощи операторов if, один из которых обычно читает файл, а другой использует byteswap intrinsics :

// source.h
class File {
public:
    enum class Endian {
        Little = 1,
        Big = 2
    };
};
// ...removed...

// source.cpp
#include "source.h"
#include <fstream>
std::ifstream file;
File::Endian endianness;

// ...removed...

bool GetPlatform() {
    uint32_t platform;
    file.read(reinterpret_cast<char*>(&platform), sizeof(platform));
    if (platform == 1) {
        endianness = File::Endian::Little;
    }
    else if (platform == 2 << 24) {
        endianness = File::Endian::Big;
    }
    // ...removed...
}

void ReadData() {
    uint32_t data;
    uint32_t dataLittle;

    if (endianness == File::Endian::Little) {
        file.read(reinterpret_cast<char*>(&data), sizeof(data));
    }
    else if (endianness == File::Endian::Big) {
        file.read(reinterpret_cast<char*>(&data), sizeof(data));
        dataLittle = _byteswap_ulong(data);
    }
}

Мой вопрос: возможно ли для go поменять местами каждое значение с большим порядком байтов и вместо этого установить универсальный порядок байтов? Ниже приведен потенциальный пример того, что я имею в виду:

bool GetPlatform() {
    uint32_t platform;
    file.read(reinterpret_cast<char*>(&platform), sizeof(platform));
    if (platform == 1) {
        // Universally set the endianness to little endian
    }
    else if (platform == 2 << 24) {
        // Universally set the endianness to big endian
    }
    // ...removed...
}

void ReadData() {
    uint32_t data;
    file.read(reinterpret_cast<char*>(&data), sizeof(data)); // Data is now read correctly regardless of endianness
}

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

Кроме того, может ли std :: endian быть полезен для этой задачи? Его примеры только указывают на использование при обнаружении порядка байтов хоста, но я не уверен, есть ли у него дальнейшее использование или нет.

Ответы [ 3 ]

0 голосов
/ 12 января 2020

Единственный способ «автоматического» чтения с правильным порядком байтов состоит в том, чтобы исходный порядковый номер процессора совпадал с порядком байтов в файле. Если они не совпадают, то что-то в вашем коде необходимо знать, чтобы выполнить необходимые перестановки байтов от порядкового номера файла до порядкового номера ЦП (при чтении из файла) и наоборот (при записи в файл). ).

Узнайте, как ntohl () и htonl () реализованы для работы с данными сетевого порядка (иначе говоря, с прямым порядком байтов) - на больших Платформы с прямым порядком байтов (например, PowerP C) - это простые операции, которые дословно возвращают свои аргументы. На платформах с прямым порядком байтов (таких как Intel) они возвращают свои аргументы с заменой байтов. Таким образом, код, который их вызывает, не должен выполнять никаких условных тестов во время выполнения, чтобы выяснить, подходит ли байт-своп, или нет, он просто безоговорочно запускает все данные, которые он читает через ntohl() или * 1008. * и верит, что они будут правильно делать с данными на всех платформах. Аналогично, при записи данных он безоговорочно пропускает все значения данных через htonl() или htons() перед отправкой данных в файл / сеть / что угодно.

Ваша программа может сделать что-то подобное, либо вызвав эти фактические функции или (если вам нужно прочитать больше типов данных, чем просто 16-разрядные и / или 32-разрядные целые числа), найдя или написав свои собственные функции, которые аналогичны функциям в духе, например что-то вроде:

inline uint32_t NativeToLittleEndianUint32(uint32_t val) {...}
inline uint32_t LittleEndianToNativeUint32(uint32_t val) {...}

inline uint32_t NativeToBigEndianUint32(uint32_t val) {...}
inline uint32_t BigEndianToNativeUint32(uint32_t val) {...}
[...]

inline uint64_t NativeToLittleEndianUint64(uint64_t val) {...}
inline uint64_t NativeToBigEndianUint64(uint64_t val) {...}

inline uint64_t LittleEndianToNativeUint64(uint64_t val) {...}
inline uint64_t BigEndianToNativeUint64(uint64_t val) {...}

[...]

... и так далее. Все тысячи предложений if / then в вашем коде исчезают и заменяются условными логиками времени компиляции c. Это делает код более эффективным, простым в тестировании и намного менее подверженным ошибкам. Если вам нравятся templated-функции, вы можете использовать их, чтобы уменьшить количество имен функций, которые должен помнить писатель вызывающего кода (например, вы можете иметь inline template<T> NativeToLittleEndian(T val) {...} с переопределением шаблонов, чтобы сделать правильные вещи для всех типов, которые вам нужны support)

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

Примечание: будьте осторожны при реализации этих функций для типов с плавающей точкой; некоторые архитектуры ЦП (например, Intel) неявно изменяют неожиданные битовые шаблоны с плавающей запятой, что означает, что, например, при перестановке 32-битного значения с плавающей запятой с порядком байтов вы должны хранить не родное / внешнее / замененное байтами представление это значение как uint32_t, а не как "float". Если вы хотите увидеть пример того, как я справился с этой проблемой, в моем коде, посмотрите, например, определения макросов B_HOST_TO_BENDIAN_IFLOAT и B_BENDIAN_TO_HOST_IFLOAT в этом файле .

0 голосов
/ 12 января 2020

Если я понимаю вашу ситуацию, ваша основная проблема c в том, что вам не хватает уровня абстракции. У вас есть несколько функций, которые читают различные структуры данных из вашего файла. Поскольку эти функции напрямую вызывают std::ifstream::read, им всем необходимо знать как структуру, которую они читают, так и структуру файла. Это две задачи, что более чем идеально. Вам лучше разбить эту логику c на два уровня абстракции. Давайте назовем функции для нового уровня ReadBytes, поскольку они сосредоточены на получении байтов из файла. Поскольку Microsoft предоставляет три встроенных байтовых замены, таких функций будет три. Вот первый удар по одному для 4-байтовых значений.

void ReadBytes(std::ifstream & file, File::Endian endianness, uint32_t & data) {
    file.read(reinterpret_cast<char*>(&data), sizeof(data));
    if (endianness == File::Endian::Big) {
        data = _byteswap_ulong(data);
    }
}

Обратите внимание, что я вернул данные через параметр. Это позволяет всем трем функциям иметь одинаковое имя; тип этого параметра указывает компилятору, какую перегрузку использовать. (Существуют и другие подходы. Стили кодирования различаются.)

Существуют и другие улучшения, но этого достаточно для создания нового уровня абстракции. Ваши различные функции, которые читают данные из файла, изменится и будут выглядеть следующим образом:

void ReadData() {
    uint32_t data;

    ReadBytes(file, endianness, data);
    // More processing here, maybe more reads.
}

С этим небольшим примером кода экономия не сразу очевидна. Однако вы указали, что может быть множество функций, выполняющих роль ReadData. Этот подход переносит бремя исправления порядка байтов с этих функций на новые ReadBytes функции. Количество операторов if сокращено с "сотен, если не тысяч" до трех.

Это изменение мотивировано принципом программирования, часто называемым "не повторяйся". Этот же принцип может мотивировать такие вопросы, как «почему существует более одной функции, которая нуждается в этом коде?»


Еще одна проблема, осложняющая для вас, заключается в том, что вы, похоже, применили процедурный подход к проблема, а не объектно-ориентированная. Симптомы процедурного подхода могут включать в себя чрезмерные параметры функции (например, endianness в качестве параметра) и глобальные переменные. Интерфейс будет проще в использовании, если он будет заключен в класс. Вот начало в направлении объявления такого класса (т.е. начало файла заголовка). Обратите внимание, что порядковый номер является частным, и этот заголовок не имеет указаний на то, как определяется порядковый номер. Если у вас хорошая инкапсуляция, код вне этого класса не будет заботиться о том, какая платформа создала файл.

// Designed as a drop-in replacement for an ifstream.
// (Non-public inheritance *might* be appropriate if you want to restrict the interface.)
class IFile : public std::ifstream {
private:
    File::Endian endianness;

public:
    // Mimic the constructors of std::ifstream that you need.
    explicit IFile(const std::string & filename);

    // It should be possible to use some template magic to simplify the
    // definition of these three functions, but since there are only three:
    void ReadBytes(uint16_t & data) {
        file.read(reinterpret_cast<char*>(&data), sizeof(data));
        if (endianness == File::Endian::Big) {
            data = _byteswap_ushort(data);
        }
    }
    void ReadBytes(uint32_t & data) {
        file.read(reinterpret_cast<char*>(&data), sizeof(data));
        if (endianness == File::Endian::Big) {
            data = _byteswap_ulong(data);
        }
    }
    void ReadBytes(uint64_t & data) {
        file.read(reinterpret_cast<char*>(&data), sizeof(data));
        if (endianness == File::Endian::Big) {
            data = _byteswap_uint64(data);
        }
    }
};

Это только начало. Интерфейс требует больше работы, с одной стороны. Кроме того, функции ReadBytes могут быть написаны немного более переносимо, возможно, используя std::endian вместо предположения little-endian. (Boost имеет библиотеку с порядком байтов , которая может помочь вам создать действительно переносимый код. Он даже по умолчанию использует встроенные функции, когда они доступны.)

Определение порядка следования выполняется в реализации (источник ) файл. Кажется, что это должно быть сделано как часть открытия файла. Я поместил это как часть конструктора для этого примера, но вам может потребоваться больше гибкости (используйте интерфейс ifstream в качестве руководства). В любом случае, логика c для обнаружения платформы не должна быть доступна вне реализации этого класса. Вот начало реализации.

// Helper function, not needed outside this class.
// This should be either static or put into an anonymous namespace.
static File::Endian ReadEndian(std::ifstream & file) {
    uint32_t platform;
    file.read(reinterpret_cast<char*>(&platform), sizeof(platform));
    if (platform == 1) {
        return File::Endian::Little;
    }
    else if (platform == 2 << 24) {
        return File::Endian::Big;
    }
    // Handle unrecognized platform here
}

IFile::IFile(const std::string & filename) : std::ifstream(filename),
    endianness(ReadEndian(file))
{}

На этом этапе ваши различные функции ReadData могут выглядеть следующим образом (без использования глобальных переменных).

void ReadData(IFile & file) {
    uint32_t data;

    file.ReadBytes(data);
}

Это даже проще, чем вы искали, поскольку повторяющийся код еще меньше. (Приведение к char* и получение размера больше не нужно повторять повсюду.)


Таким образом, есть две основные области для улучшения.

  1. Не повторяйте себя. Код, который часто повторяется, следует перенести в отдельную функцию.
  2. Объектно-ориентированный. Полагайтесь на объекты, обрабатывающие рутинные задачи, а не делегируя их людям, использующим объекты.

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

0 голосов
/ 12 января 2020

Я думаю, что обычный ответ #ifdef определение функции, как read64:

int64_t read64(char *pos) {
#ifdef IS_BIG_ENDIAN
  ...
#elif IS_LITTLE_ENDIAN
  ...
#else
  // probably # error
#endif
}
...