How to change double's endianness?
Asked Answered
R

4

5

I need to read data from serial port. They are in little endian, but I need to do it platform independent, so I have to cover the endianness of double. I couldn't find anywhere, how to do it, so I wrote my own function. But I am not sure with it. (and I don't have a machine with big endian to try it on).

Will this work correctly? or is there some better approach I wasn't able to find?

double get_double(uint8_t * buff){
    double value;
    memcpy(&value,buff,sizeof(double));
    uint64_t tmp;
    tmp = le64toh(*(uint64_t*)&value);
    value = *(double*) &tmp;
    return value;
}

p.s. I count with double 8 bytes long, so don't bother with this pls. I know that there might be problems with this

EDIT: After suggestion, that I should use union, I did this:

union double_int{
    double d;
    uint64_t i;
};


double get_double(uint8_t * buff){
    union double_int value;
    memcpy(&value,buff,sizeof(double));
    value.i = le64toh(value.i);
    return value.d;
}

better? (though I don't see much of a difference)

EDIT2: attemtp #3, what do you think now?

double get_double(uint8_t * buff){
    double value;
    uint64_t tmp;
    memcpy(&tmp,buff,sizeof(double));
    tmp = le64toh(tmp);
    memcpy(&value,&tmp,sizeof(double));
    return value;
}

Edit3: I compile it with gcc -std=gnu99 -lpthread -Wall -pedantic

Edit4: After next suggestion I added a condition for endianness order checking. I honestly have no idea, what I am doing right now (shouldn't there be something like __DOUBLE_WORD_ORDER__ ?)

double get_double(uint8_t * buff){
    double value;
    if (__FLOAT_WORD_ORDER__ == __ORDER_BIG_ENDIAN__){
        uint64_t tmp;
        memcpy(&tmp,buff,sizeof(double));
        tmp = le64toh(tmp);
        memcpy(&value,&tmp,sizeof(double));
    }
    else {
        memcpy(&value,buff,sizeof(double));
    }
    return value;
}
Rattlesnake answered 8/4, 2016 at 11:12 Comment(16)
You should use a unionJinja
I did it as you suggested... better? it looks better, but I don't see a significant diffferenceRattlesnake
There are aliasing rules, and the alignment requirements of an uint64_t could be different from double (improbable, yes), and so on.Jinja
ok, so now, as I did it with union, It's goona work?Rattlesnake
Better using memcpy or something else. With usions you can have problem with some GCC versions and -O3 optimization enabled.Kylstra
memcpy rather than what? Sry, I didn't get you.Rattlesnake
Copy double to uint64_t then convert by le64toh uint64_t and copy back to double. "memcpy rather than what? " unionKylstra
ok, so now its better?Rattlesnake
Ah: you need to force at least -std=c99, as in C89 this has undefined behaviour.Jinja
Why the union bother? It is fine to cast any pointer to a char pointer (and then do what you want with the memory, e.g. memcpy it around); that's explicitly allowed as an exception to the normal aliasing restrictions, and C would not work without it, nor would any program written in C. Cf. n1570, 6.5/7.Delphina
Calling le64toh() adjusts,the endian of an integer depending of the endianness of the platform versus "network". The endian-ness of a double is independent of an integer. le64toh() might be useful to perform an endian swap, but not to detect if it is needed for double.Changeful
The union concerns are useful for avoiding the memcpy(&tmp,buff,sizeof(double)); step, but not the "endian" step.Changeful
ive just edited the question, is it correct now?Rattlesnake
Edit 3 was useful as it added missing information. Edits 1,2,4 changed the question repeatedly. Moving targets are frown upon in SO.Changeful
I meant the code in the 4th edit...Rattlesnake
@Rattlesnake If you have an answer to your own (original) question, post your answer in the answer section. This way all can votes on your Q & A independently. Editing your own post with an ever improving answer is not the SO model.Changeful
D
5

I'd just go and copy the bytes manually to a temporary double and then return that. In C (and I think C++) it is fine to cast any pointer to char *; that's one of the explicit permissions for aliasing (in the standard draft n1570, par. 6.5/7), as I mentioned in one of my comments. The exception is absolutely necessary in order to get anything done that is close to hardware; including reversing bytes received over a network :-).

There is no standard compile time way to determine whether that's necessary which is a pity; if you want to avoid branches which is probably a good idea if you deal with lots of data, you should look up your compiler's documentation for proprietary defines so that you can choose the proper code branch at compile time. gcc, for example, has __FLOAT_WORD_ORDER__ set to either __ORDER_LITTLE_ENDIAN__ or __ORDER_BIG_ENDIAN__.

(Because of your question in comments: __FLOAT_WORD_ORDER__ means floating points in general. It would be a very sick mind who designs a FPU that has different byte orders for different data sizes :-). In all reality there aren't many mainstream architectures which have different byte orders for floating point vs. integer types. As Wikipedia says, small systems may differ.)

Basile pointed to ntohd, a conversion function which exists on Windows but apparently not on Linux.

My naive sample implementation would be like

/** copy the bytes at data into a double, reversing the
    byte order, and return that.
*/
double reverseValue(const char *data)
{
    double result;

    char *dest = (char *)&result;

    for(int i=0; i<sizeof(double); i++) 
    {
        dest[i] = data[sizeof(double)-i-1];
    }
    return result;
}

/** Adjust the byte order from network to host.
    On a big endian machine this is a NOP.
*/
double ntohd(double src)
{
#   if      !defined(__FLOAT_WORD_ORDER__) \
        ||  !defined(__ORDER_LITTLE_ENDIAN__)
#       error "oops: unknown byte order"
#   endif

#   if __FLOAT_WORD_ORDER__ == __ORDER_LITTLE_ENDIAN__
        return reverseValue((char *)&src);
#   else
        return src;
#   endif
}

There is a working example here: https://ideone.com/aV9mj4.

An improved version would cater to the given CPU -- it may have an 8 byte swap command.

Delphina answered 8/4, 2016 at 13:18 Comment(3)
Rather than #error, I'd rather fallback to a run-time test.Wellpreserved
@black True. I was happy to have "error handling" at all in this example :-)Delphina
Note: Using "%le" rather than "%lf"is more illustrative for the linked example.Changeful
P
1

If you can adapt the software both sides (emitter & receiver) you could use some serialization library and format. It could be using old XDR routines like xdr_double in your case (see xdr(3)...). You could also consider ASN1 or using textual formats like JSON.

XDR is big endian. You might try to find some NDR implementation, is is little endian.

See also this related question, and STFW for htond

Pastis answered 8/4, 2016 at 13:43 Comment(1)
unfortunetly I can not. Emmiter is a GPS reciever.Rattlesnake
R
1

Ok, so finally, thanks to you all, I have found the best solution. Now my code looks like this:

double reverseDouble(const char *data){
    double result;
    char *dest = (char *)&result;
    for(int i=0; i<sizeof(double); i++) 
        dest[i] = data[sizeof(double)-i-1];
    return result;
}

double get_double(uint8_t * buff){
    double value;
    memcpy(&value,buff,sizeof(double));

    if (__FLOAT_WORD_ORDER__ == __ORDER_BIG_ENDIAN__)
        return reverseDouble((char *)&value);
    else
        return value;
}

p.s. (checking for defines etc. is somewhere else)

Rattlesnake answered 8/4, 2016 at 14:43 Comment(0)
U
1

If you can assume the endianness of doubles is the same as for integers (which you can't, generally, but it's almost always the same), you can read it as an integer by shifting it byte by byte, and then cast the representation to a double.

double get_double_from_little_endian(uint8_t * buff) {
    uint64_t u64 = ((uint64_t)buff[0] << 0  |
                    (uint64_t)buff[1] << 8  |
                    (uint64_t)buff[2] << 16 |
                    (uint64_t)buff[3] << 24 |
                    (uint64_t)buff[4] << 32 |
                    (uint64_t)buff[5] << 40 |
                    (uint64_t)buff[6] << 48 |
                    (uint64_t)buff[7] << 56);
    return *(double *)(char *)&u64;
}

This is only standard C and it's optimized well by compilers. The C standard doesn't define how the floats are represented in memory though, so compiler-dependent macros like __FLOAT_WORD_ORDER__ may give better results, but it gets more complex if you want to cover also the "mixed-endian IEEE format" which is found on ARM old-ABI.

Unseasoned answered 29/9, 2021 at 14:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.