В качестве исследовательского проекта мы пишем уровень абстракции поверх стандартной библиотеки обработки файлов C (BINARY) (stdio), предоставляя несколько дополнительных функций для обработки файлов с транзакциями.
Рабочий процесс следующий:
fopen
). Оба возвращаются FILE*
, Файл открыт в двоичном режиме!fwrite
)TRANSACTION a = trans_start(FILE* )
TRANSACTION
объект (set_validator(TRANSACTION, int(*)(char*))
int trans_write_string(TRANSACTION*, char*, length)
trans_commit(TRANSACTION)
для того, чтобы на самом деле записать данные в файл. Теперь, в зависимости от флагов, установленных валидаторами, это может НЕ записывать данные в файл, а сообщать пользователю об ошибке (которая может быть исправлена программно. … не так важна для вопроса).fclose
,До сих пор у нас есть только метод обработки строк API (trans_write_string
), который работает хорошо. Он создает свой собственный в буфере данных памяти, при необходимости изменяет содержимое, вызывает валидаторы и т. Д. При последовательных вызовах добавляет новые данные в буфер внутренней памяти, обрабатывает распределение и т. Д., И при успешном коммите записывает данные в файл с помощью fwrite
(Да, это в основном проект C, однако ответы C ++ также не будут исключены).
Но теперь мы хотим (… должны) расширить API, чтобы иметь возможность записывать также числа (16-битные, 32-битные, 64-битные) и тоже плавающие … очень похоже на стандарт C
API-интерфейс stdio делает это. Используя уже существующую реализацию для строки, это предполагает, что у нас есть буфер данных в памяти, который содержит N
байтов символов (сама строка), нам может потребоваться 2 байта для 16-битного значения, а затем другое M
байты для другой строки, 8 байтов для 64-битного значения, 2 байта для 16-битного значения и т. д.
Мы застряли в точке «как представить число в файле, чтобы его мог прочитать кто-то другой, кто использует другой компьютер / архитектура / os / endianness».
Вставка числа в поток памяти теоретически возможна посредством приведения к типу char (char* addr = &my_16bit_int
) и место *(addr)
а также *(addr + 1)
на требуемый адрес (то есть: после N
символы строки) и запись его в файл также возможна, но что если я захочу прочитать полученный файл в другой архитектуре, где endiannes отличается? А что, если «другой» компьютер — это всего лишь 16-битный кусок металла? Что произойдет в этом случае с 64-битными значениями, записанными в файле?
Какие хорошие практики существуют для решения такого рода проблем?
РЕДАКТИРОВАТЬ: Целевой файл должен быть двоичным, к нему будет добавлен текстовый файл (XML), описывающий его формат (например: N
8-байтовые символы, 1
16-битное значение и т. Д.) (Этот текстовый файл создается на основе результатов наших любимых валидаторов). Валидатор «говорит» что-то вроде: ДА, я принимаю это 16-битное значение, нет, я отклоняю эту длинную строку и т. Д. … и кто-то еще создает формат данных XML на основе этого «вывода».
EDIT2Да, нам нужно обмениваться файлами на разных платформах, даже в огромных 20-летних коробках размером с холодильник 🙂
EDIT3Да, нам тоже нужно плавать!
Литье не достаточно, я думаю, метод сокетов htons
а также htonl
будет достаточным решением для int16 и int32. для int64 вы должны собрать его самостоятельно, поскольку официального метода не существует:
Обратите внимание, что все функции меняют порядок байтов только при необходимости, так что вы также можете использовать тот же метод, чтобы «исправить» число обратно в нормальное состояние.
typedef union{
unsigned char c[2];
unsigned short s;
}U2;
//you can use the standard htons or this
unsigned short htons(unsigned short s)
{
U2 mask,res;
unsigned char* p = (unsigned char*)&s;
mask.s = 0x0001;
res.c[mask.c[0]] = p[0];
res.c[mask.c[1]] = p[1];
return res.s;
}
//the same for 4 bytes
typedef union{
unsigned char c[4];
unsigned short s[2];
unsigned long l;
}U4;
//you can use the standard htonl or this
unsigned long htonl(unsigned long l)
{
U4 mask,res;
unsigned char* p = (unsigned char*)&l;
mask.l = 0x00010203;
res.c[mask.c[0]] = p[0];
res.c[mask.c[1]] = p[1];
res.c[mask.c[2]] = p[2];
res.c[mask.c[3]] = p[3];
return res.l;
}
typedef union{
unsigned char c[8];
unsigned char c2[2][4];
unsigned short s[4];
unsigned long l[2];
unsigned long long ll;
}U8;
//for int64 you can use the int64 and do the same, or you can to do it with 2*4 like i did
//you can give a void pointer as well..
unsigned long long htonll(unsigned long long ll)//void htonll(void* arg, void* result)
{
U2 mask1;
U4 mask2;
U8 res;
unsigned char* p = (unsigned char*)≪ //or (unsigned char*)arg
mask1.s = 0x0001;
mask2.l = 0x00010203;
//I didn't use the int64 for convertion
res.c2[mask1.c[0]][mask2.c[0]] = p[0];
res.c2[mask1.c[0]][mask2.c[1]] = p[1];
res.c2[mask1.c[0]][mask2.c[2]] = p[2];
res.c2[mask1.c[0]][mask2.c[3]] = p[3];
res.c2[mask1.c[1]][mask2.c[0]] = p[4];
res.c2[mask1.c[1]][mask2.c[1]] = p[5];
res.c2[mask1.c[1]][mask2.c[2]] = p[6];
res.c2[mask1.c[1]][mask2.c[3]] = p[7];
//memcpy(result,res.c,8);
return res.ll;
}
//or if you want to use the htonl:
unsigned long long htonll2(unsigned long long ll)
{
U2 mask1;
U8 res;
mask1.s = 0x0001;
unsigned long* p = (unsigned long*)≪
res.l[0] = htonl(p[mask1.c[0]]);
res.l[1] = htonl(p[mask1.c[1]]);
return res.ll;
}
int main()
{
unsigned short s = 0x1122;
cout<<hex<<htons(s)<<endl;
unsigned long l = 0x11223344;
cout<<hex<<htonl(l)<<endl;
unsigned long long ll=0x1122334455667788;
cout<<hex<<htonll(ll)<<endl;
cout<<hex<<htonll2(ll)<<endl;
return 0;
}
Вы должны определить формат или выбрать существующий двоичный файл
форматировать как XDR, и читать и писать. Так, например, чтобы
написать 32-битное целое число в XDR:
void
write32Bits( FILE* dest, uint_least32_t value )
{
putc( (value >> 24) & 0xFF, dest );
putc( (value >> 16) & 0xFF, dest );
putc( (value >> 8) & 0xFF, dest );
putc( (value ) & 0xFF, dest );
}
С плавающей точкой сложнее, но если вы готовы
ограничьте свои платформы теми, которые поддерживают IEEE float, вы можете
типа каламбур float
в uint32_t
а также double
в uint64_t
, а также
выведите его как беззнаковое целое. Точно так же, если вы ограничиваете
самостоятельно до 2-х дополнительных машин с 32-битным интегральным типом,
Вы также можете использовать процедуру сдвига и маски выше для подписанного
значения (и целочисленные типы будут uint32_t
а также
int32_t
).
Что касается портативности: я думаю, что IEEE универсален,
кроме мэйнфреймов, и дополнение 2 универсально, за исключением
для очень экзотических мейнфреймов. (Мэйнфреймы IBM являются дополнением 2,
но не IEEE. Мэйнфреймы Unisys не являются дополнением 2, а
не имеют 32-битного целочисленного типа. Я не уверен, что другое
мэйнфреймы все еще существуют, но в прошлом были
всякие экзотики.)
Если вы используете glibc, вы можете использовать его функции для <-> быть преобразованиями из «endian.h»:
SYNOPSIS
#define _BSD_SOURCE /* See feature_test_macros(7) */
#include <endian.h>
uint16_t htobe16(uint16_t host_16bits);
uint16_t htole16(uint16_t host_16bits);
uint16_t be16toh(uint16_t big_endian_16bits);
uint16_t le16toh(uint16_t little_endian_16bits);
uint32_t htobe32(uint32_t host_32bits);
uint32_t htole32(uint32_t host_32bits);
uint32_t be32toh(uint32_t big_endian_32bits);
uint32_t le32toh(uint32_t little_endian_32bits);
uint64_t htobe64(uint64_t host_64bits);
uint64_t htole64(uint64_t host_64bits);
uint64_t be64toh(uint64_t big_endian_64bits);
uint64_t le64toh(uint64_t little_endian_64bits);
Если вы не используете glibc, вы можете просто взглянуть на glibc-2.18 / bits / byteswap.h