The portable way to read a little-endian 64-bit value is very straightforward:
inline static uint64_t load_u64le(const void *p) {
const unsigned char *q = p;
uint64_t result = 0;
result |= q[7]; result <<= 8;
result |= q[6]; result <<= 8;
result |= q[5]; result <<= 8;
result |= q[4]; result <<= 8;
result |= q[3]; result <<= 8;
result |= q[2]; result <<= 8;
result |= q[1]; result <<= 8;
result |= q[0];
return result;
}
inline static int64_t load_i64le(const void *p) {
return (int64_t)load_u64le(p);
}
Simply invoke this helper function as read_i64le(rx_buffer + 1)
. Modern compilers are able to optimize this to a single instruction on architectures where that is possible.
To read a 64-bit value where you specifically know the endianness agrees with the native ABI, you can use this:
inline static uint64_t load_u64(const void *p) {
uint64_t result;
memcpy(&result, p, sizeof(result));
return result;
}
which has even better chances of being optimized into a simple load, assuming only that the compiler optimizes a short memcpy
into an inline memory load.
For best results then, you can use:
inline static uint64_t load_u64le(const void *p) {
uint64_t result = 0;
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
memcpy(&result, p, sizeof(result));
#else
const unsigned char *q = p;
result |= q[7]; result <<= 8;
result |= q[6]; result <<= 8;
result |= q[5]; result <<= 8;
result |= q[4]; result <<= 8;
result |= q[3]; result <<= 8;
result |= q[2]; result <<= 8;
result |= q[1]; result <<= 8;
result |= q[0];
#endif
return result;
}
Now, why you shouldn’t cast an offset pointer like the other answers suggest: first of all, because dereferencing a misaligned pointer is UB. Not every architecture supports reading words wider than 8 bits from arbitrary addresses, and even on those architectures that do support them, the compiler may still make the assumption that all dereferenced addresses are properly aligned when generating code, especially under optimizations. If you ever run your code with UBSan, it will also complain.
The second reason is strict aliasing. The C language stipulates that all memory must be accessed either via a pointer to a character type (char
, signed char
or unsigned char
) or a pointer to the type of which an object is stored in that memory; this ensures that pointers to different types can be assumed not to alias (point to the same memory). In practice, uint8_t
is usually an alias of unsigned char
, which is a character type, exceptionally allowed to alias any type; this makes the strict aliasing concern mostly theoretical, so far. Nevertheless, there is no reason to take that risk either, when avoiding it is so easy and cheap.