Сериализация двойного и плавающего с C - PullRequest
12 голосов
/ 05 августа 2010

Как я могу сериализовать двойные и плавающие числа в C?

У меня есть следующий код для сериализации шорт, целых и символов.

unsigned char * serialize_char(unsigned char *buffer, char value)
{
    buffer[0] = value;
    return buffer + 1;
}

unsigned char * serialize_int(unsigned char *buffer, int value)
{
    buffer[0] = value >> 24;
    buffer[1] = value >> 16;
    buffer[2] = value >> 8;
    buffer[3] = value;
    return buffer + 4;
}

unsigned char * serialize_short(unsigned char *buffer, short value)
{
    buffer[0] = value >> 8;
    buffer[1] = value;
    return buffer + 2;
}

Edit:

Я нашел эти функции из этого вопроса

Редактировать 2:

Цель сериализации - отправить данные в сокет UDP и гарантировать, что они могут быть десериализованы на другом компьютере, даже если порядковый номер отличается. Существуют ли другие «лучшие практики» для выполнения этой функциональности, учитывая, что мне нужно сериализовать int, double, float и char *?

Ответы [ 8 ]

12 голосов
/ 05 августа 2010

Я помню, как впервые увидел приведение, использованное в моем примере ниже, в старом добром исходном коде Quake процедуры «rsqrt», содержащем самый крутой комментарий, который я видел в то время (Google, он вам понравится)

unsigned char * serialize_float(unsigned char *buffer, float value) 
{ 
    unsigned int ivalue = *((unsigned int*)&value); // warning assumes 32-bit "unsigned int"
    buffer[0] = ivalue >> 24;  
    buffer[1] = ivalue >> 16;  
    buffer[2] = ivalue >> 8;  
    buffer[3] = ivalue;  
    return buffer + 4; 
} 

Надеюсь, я правильно понял ваш вопрос (и пример кода). Дайте мне знать, если это было полезно?

11 голосов
/ 06 августа 2010

Портативный способ: используйте frexp для сериализации (конвертирования в целочисленную мантиссу и экспоненту) и ldexp для десериализации.

Простой способ: предположим, что в 2010 году любая машина, которая вас интересует, использует IEEE float, объявляет объединение с элементом float и uint32_t и использует ваш целочисленный код сериализации для сериализации float.

Способ двоичных файлов-ненавистников: сериализуйте все как текст, включая плавающие. Используйте спецификатор формата "%a" printf для получения шестнадцатеричного числа с плавающей точкой, которое всегда выражается точно (при условии, что вы не ограничиваете точность чем-то вроде "%.4a") и не подвержено ошибкам округления. Вы можете прочитать их обратно с помощью strtod или любой из функций семейства scanf.

8 голосов
/ 06 августа 2010

Это упаковывает значение с плавающей запятой в пары int и long long, которые затем можно сериализовать с другими вашими функциями. Функция unpack() используется для десериализации.

Пара чисел представляет экспоненту и дробную часть числа соответственно.

#define FRAC_MAX 9223372036854775807LL /* 2**63 - 1 */

struct dbl_packed
{
    int exp;
    long long frac;
};

void pack(double x, struct dbl_packed *r)
{
    double xf = fabs(frexp(x, &r->exp)) - 0.5;

    if (xf < 0.0)
    {
        r->frac = 0;
        return;
    }

    r->frac = 1 + (long long)(xf * 2.0 * (FRAC_MAX - 1));

    if (x < 0.0)
        r->frac = -r->frac;
}

double unpack(const struct dbl_packed *p)
{
    double xf, x;

    if (p->frac == 0)
        return 0.0;

    xf = ((double)(llabs(p->frac) - 1) / (FRAC_MAX - 1)) / 2.0;

    x = ldexp(xf + 0.5, p->exp);

    if (p->frac < 0)
        x = -x;

    return x;
}
5 голосов
/ 06 августа 2010

Вы можете переносить сериализацию в IEEE-754 независимо от собственного представления:

int fwriteieee754(double x, FILE * fp, int bigendian)
{
    int                     shift;
    unsigned long           sign, exp, hibits, hilong, lowlong;
    double                  fnorm, significand;
    int                     expbits = 11;
    int                     significandbits = 52;

    /* zero (can't handle signed zero) */
    if(x == 0) {
        hilong = 0;
        lowlong = 0;
        goto writedata;
    }
    /* infinity */
    if(x > DBL_MAX) {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        lowlong = 0;
        goto writedata;
    }
    /* -infinity */
    if(x < -DBL_MAX) {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        hilong |= (1 << 31);
        lowlong = 0;
        goto writedata;
    }
    /* NaN - dodgy because many compilers optimise out this test
     * isnan() is C99, POSIX.1 only, use it if you will.
     */
    if(x != x) {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        lowlong = 1234;
        goto writedata;
    }

    /* get the sign */
    if(x < 0) {
        sign = 1;
        fnorm = -x;
    } else {
        sign = 0;
        fnorm = x;
    }

    /* get the normalized form of f and track the exponent */
    shift = 0;
    while(fnorm >= 2.0) {
        fnorm /= 2.0;
        shift++;
    }
    while(fnorm < 1.0) {
        fnorm *= 2.0;
        shift--;
    }

    /* check for denormalized numbers */
    if(shift < -1022) {
        while(shift < -1022) {
            fnorm /= 2.0;
            shift++;
        }
        shift = -1023;
    } else {
        /* take the significant bit off mantissa */
        fnorm = fnorm - 1.0;
    }
    /* calculate the integer form of the significand */
    /* hold it in a  double for now */

    significand = fnorm * ((1LL << significandbits) + 0.5f);

    /* get the biased exponent */
    exp = shift + ((1 << (expbits - 1)) - 1);   /* shift + bias */

    /* put the data into two longs */
    hibits = (long)(significand / 4294967296);  /* 0x100000000 */
    hilong = (sign << 31) | (exp << (31 - expbits)) | hibits;
    lowlong = (unsigned long)(significand - hibits * 4294967296);

 writedata:
    /* write the bytes out to the stream */
    if(bigendian) {
        fputc((hilong >> 24) & 0xFF, fp);
        fputc((hilong >> 16) & 0xFF, fp);
        fputc((hilong >> 8) & 0xFF, fp);
        fputc(hilong & 0xFF, fp);

        fputc((lowlong >> 24) & 0xFF, fp);
        fputc((lowlong >> 16) & 0xFF, fp);
        fputc((lowlong >> 8) & 0xFF, fp);
        fputc(lowlong & 0xFF, fp);
    } else {
        fputc(lowlong & 0xFF, fp);
        fputc((lowlong >> 8) & 0xFF, fp);
        fputc((lowlong >> 16) & 0xFF, fp);
        fputc((lowlong >> 24) & 0xFF, fp);

        fputc(hilong & 0xFF, fp);
        fputc((hilong >> 8) & 0xFF, fp);
        fputc((hilong >> 16) & 0xFF, fp);
        fputc((hilong >> 24) & 0xFF, fp);
    }
    return ferror(fp);
}

В машинах, использующих IEEE-754 (т.е. общий случай ), всенужно сделать, чтобы получить номер fread().В противном случае, декодируйте байты самостоятельно (sign * 2^(exponent-127) * 1.mantissa).

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

Надеюсь, это поможет.

3 голосов
/ 06 августа 2010

Для узкого вопроса о float обратите внимание, что вы, вероятно, в конечном итоге предполагаете, что оба конца провода используют одно и то же представление для плавающей запятой.Сегодня это может быть безопасным, учитывая повсеместное использование IEEE-754, но обратите внимание, что некоторые современные DSP (я полагаю, черные плавники) используют другое представление.В старые времена было как минимум столько же представлений для чисел с плавающей запятой, сколько было производителей оборудования и библиотек, так что это было более серьезной проблемой.

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

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

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

Если вы настаиваете на том, чтобы свернуть свое собственное, позаботьтесь о тонких вопросах, таких как16 бит, 32 бита или даже 64 бита в дополнение к представлению float и double.

2 голосов
/ 05 августа 2010

Вы всегда можете использовать объединения для сериализации:

void serialize_double (unsigned char* buffer, double x) {
    int i;
    union {
        double         d;
        unsigned char  bytes[sizeof(double)];
    } u;

    u.d = x;
    for (i=0; i<sizeof(double); ++i)
        buffer[i] = u.bytes[i];
}

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

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

2 голосов
/ 05 августа 2010

После обновления вы упоминаете, что данные должны передаваться по протоколу UDP, и запрашиваете рекомендации.Я настоятельно рекомендую отправлять данные в виде текста, возможно, даже с некоторой добавленной разметкой (XML).Отладка связанных с порядком байтов ошибок в линии передачи - это пустая трата времени каждого

Только мои 2 цента на часть "наилучшей практики" вашего вопроса

1 голос
/ 06 августа 2010

Для начала, вы никогда не должны предполагать, что short, int и т. Д. Имеют одинаковую ширину с обеих сторон.Было бы намного лучше использовать типы uint32_t и т.д. (без знака), которые имеют ширину с обеих сторон.

Тогда, чтобы убедиться, что у вас нет проблем с порядком байтов, есть макросы / функции ntoh htos и т. Д., Которые обычно намного более эффективны, чем все, что вы можете сделать самостоятельно.(на оборудовании Intel это, например, всего лишь одна инструкция на ассемблере.) Поэтому вам не нужно писать функции преобразования, в основном они уже есть, просто приведите указатель buffer к указателю правильного целочисленного типа.

Для float вы, вероятно, можете предположить, что они являются 32-битными и имеют одинаковое представление с обеих сторон.Поэтому я думаю, что хорошей стратегией было бы использование указателя, приведенного к uint32_t*, а затем той же стратегии, что и выше.

Если вы думаете, что у вас могут быть разные представления float, вам придется разделить на мантиссуи показатель.Возможно, вы могли бы использовать frexpf для этого.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...