0

I'm currently writing a binary application protocol in C that sends uint32_t and doubles across the network using sockets. When writing uint32_t's, I always use the htonl() functions to convert the host byte order to network byte order. However, such a function does not exist for doubles. So I ended up writing a new function which checks the host endianness and reverses the byte order of the double if necessary.

double netHostToNetFloat64(double input)
{
    typedef union DoubleData
    {
        double d;
        char c[8];
    } DoubleData;

    DoubleData input_data, output_data;
    
    if(netHostIsBigEndian())
    {
        return input;
    }
    else
    {
        input_data.d = input;
        output_data.c[0] = input_data.c[7];
        output_data.c[1] = input_data.c[6];
        output_data.c[2] = input_data.c[5];
        output_data.c[3] = input_data.c[4];
        output_data.c[4] = input_data.c[3];
        output_data.c[5] = input_data.c[2];
        output_data.c[6] = input_data.c[1];
        output_data.c[7] = input_data.c[0];
        return output_data.d;
    }
}

However, I'm getting some weird results when the network peer reads the double. I'm confident that my function is reversing the byte order, however, I'm curious if storing an invalid double value (i.e. it results in an Inf or NaN) corrupts the output the next time it is read?

Also, I realize that reversing the byte order and placing the result back into a double is stupid, however, I wanted to keep it consistent with the htonl/htons/ntohl/ntohs functions.

Izzo
  • 4,461
  • 13
  • 45
  • 82
  • This could result in trap representation and cause undefined behavior when used. https://stackoverflow.com/questions/6725809/trap-representation . You can't read the result as double anymore. – Eugene Sh. Jan 13 '22 at 15:55
  • Have you checked that the receiver gets the bytes in the correct order? Does the receiver also reverse the bytes? – dbush Jan 13 '22 at 16:00
  • `output_data.c[0] = input_data.c[7];`, so now `c[0]` is overwritten with `c[7]`, so `output_data.c[7] = input_data.c[0];` will essentially write `c[7]` to itself. You're losing half the data, you'll need to store the first 4 bytes in temps so they can be recovered for the latter 4 bytes. – yano Jan 13 '22 at 16:16
  • 1
    @yano No, these are distinct variables. Lets leave the comments here, because you are not the first one. – Eugene Sh. Jan 13 '22 at 16:17
  • @EugeneSh. *face palm* ok, yeah, whoops, I'll go crawl back under my rock – yano Jan 13 '22 at 16:18
  • 2
    While the C standard permits the bytes of `double` to be a trap representation, many C implementations do not generate traps for the `double` type unless you go to some effort to enable floating-point exceptions, and even then, when IEEE-754 binary64 is used, you will only get NaNs that generate signals about 1 time out of 4096 with uniformly distributed data (all 11 exponent bits must be 1, the leading significant bit should be zero, and some other significand bit must be set)… – Eric Postpischil Jan 13 '22 at 16:19
  • 2
    … However, even without traps, some implementations might canonicalize NaNs. But, if you are getting corrupt data, what you ought to be doing is examining the bytes of the `double` objects being swapped, the bytes after swapping, the bytes as they are received over the network, and the bytes after swapping again. Isolate the problem to a specific step. – Eric Postpischil Jan 13 '22 at 16:20
  • 1
    That said, `netHostToNetFloat64` ought to be `HostFloat64ToNetBytes`. In other words, its input should be a native `double`, and its output ought to be a buffer of `unsigned char` in network order. There is no reason to access the bytes prepared for the network as a `double`. Just treat them as bytes, send them, receive them, and then convert them back. – Eric Postpischil Jan 13 '22 at 16:23
  • 1
    Why do you need to convert from host to network byte order? They are simple bytes. If both sides use the same format (endianess) what is the point of converting? – 0___________ Jan 13 '22 at 16:33
  • 1
    @0___________ The client machine is little endian, and the server machine expects data to come in big-endian format. – Izzo Jan 13 '22 at 17:01
  • @EricPostpischil i'm thinking if I change the output data type, things might just start working. – Izzo Jan 13 '22 at 17:02
  • @Izzo when I send the different the numbers as byte stream I usually start with integer `1`. Other side checks is it is still `1`. If not reverses the bytes. – 0___________ Jan 13 '22 at 17:14
  • I would definitely handle the value to be sent as 64-bit integer or 8 bytes instead of a `double` to avoid problems with `NaN` etc... (as 0___ does it in his answer). – Martin Rosenau Jan 13 '22 at 19:26

2 Answers2

0

I would use other way of reversing the value.

uint64_t reversebytes(double d)
{
    uint64_t u;
    memcpy(&u, &d, sizeof(u));

    u = (u >> 56) | (u << 56) |
        ((u & 0x00ff000000000000) >> 40) | ((u & 0x000000000000ff00) << 40) |
        ((u & 0x0000ff0000000000) >> 24) | ((u & 0x0000000000ff0000) << 24) |
        ((u & 0x000000ff00000000) >> 8)  | ((u & 0x00000000ff000000) << 8) ;
    return u;
}

This code is very well optimized by the compilers: https://godbolt.org/z/qn6ar3hvM

Your function is much more difficult for the compiler to optimize: https://godbolt.org/z/M4shPoEnE

0___________
  • 60,014
  • 4
  • 34
  • 74
0

Double is eight bytes, so you need to reverse the eight bytes of the value to send it on the network... normally, when you send a IEEE-752, the protocol specification indicates which byte must be sent first (it's normally the most significant byte of the double value, which has the sign and the most significant byte of the exponent of two.) but quite common (in Intel architecture) to store that byte just in the last byte (following something similar to little endian again) so you must change all the bytes order to appear in reverse, before transmitting. Other architectures I'm not sure what they do, but probably will do something similar. You could use this:

uint64_t llhton(void *p)
{
    uint64_t data = *(uint64_t *)p;
    src = (data & 0xffffffff00000000) >> 32 | (data & 0x00000000ffffffff) << 32;
    src = (data & 0xffff0000ffff0000) >> 16 | (data & 0x0000ffff0000ffff) << 16;
    src = (data & 0xff00ff00ff00ff00) >> 8 | (data & 0x00ff00ff00ff00ff) << 8;
    return data;
}

if you have:

    double value;

you can serialize it for output:

    uint64_t data_in_network_order = llhton(&value);

while data_in_network_order is in network order, it has no meaning in this architecture, so I used a 64bit unsigned data for convenience only, it should be considered opaque data.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31