In case this
is OP's homework, let's drown them with useful information.

The following terms will help when one is looking for details on this stuff:
- architecture – the instruction set and/or processor family used on the target
- ABI – the binary interface provided by the compiler and the base libraries
- Common C data models:
- ILP32: int, long, and pointers are all 32 bits in size
- LLP64: int and long are 32-bit, but long long and pointers are 64-bit values
- LP64: int is 32-bit, but long and pointers are 64-bit
(plus some rare others, like ILP64 and SILP64)
- The primitive used for atomic access: CAS (compare and exchange) or LL/SC (load-link, store-conditional)
CAS is based on an instruction that will update target memory atomically if it contains a specific value, and will fail otherwise. LL/SC implements a load instruction that pairs with a store to the same address, with the store only succeeding if nothing modified that memory in between.
- Endianness: when storing multi-byte values, in which order are the bytes stored in.
Note that bits have only one order within a byte, based on their numerical value: bit n has numerical value 2n, with the least significant (integer) bit being bit 0. In an N-bit unit, the most significant bit is bit N-1.
When transmitted using a serial connection like SPI or I2C, the bits can be transmitted either the least significant or the most significant bit first.
Finally, some documentation (like old IBM docs) labels bits starting at 0 at the most significant bit. This can be problematic when trying to map the bit to a specific value (of a register containing bits labelled using such a scheme).
- API or application programming interface – the programming interface that the base libraries and/or the kernel provides in a specific programming language like C.
For example, Windows running on 64-bit Intel and AMD x86-64 processors uses the Windows ABI and API, which has an LLP64 data model, is little-endian, and the base libraries provide a subset of C11, and even C code is intended to be compiled using a C++ compiler (as Microsoft does not provide a C compiler, only a C++ compiler that can compile most C code).
In comparison, Linux running on that same hardware uses the System V ABI, providing almost all of POSIX.1-2008 C, and some additional Linux- and GNU-specific C extensions. It has an LP64 data model, is little-endian.
However, Linux also runs on a lot of other architectures (processors and instruction sets), on both 32-bit and 64-bit ones. The 32-bit ones all have an ILP32 data model, and the 64-bit ones an LP64 data model. (Because of this, in Linux,
long and
unsigned long are the same size as pointers.)
Depending on the architecture, byte order is either little-endian or big-endian. (Some, like many ARM cores, can even switch between the two at run time, but I do not believe it is supported for userspace programs in Linux.)
The
<stdint.h> header is actually provided by the
compiler, in the sense that it is available even for freestanding code (with no standard C library features available). You could say it provides much saner, easily predictable types for one to use in a reliable, portable manner.
To solve byte order issues, one can use wither the
__BYTE_ORDER macro (which matches either
__BIG_ENDIAN or
__LITTLE_ENDIAN, if defined; see
Pre-defined Compiler Macros for further info) at compile time, or C code similar to the following at run time:
#include <stdint.h>
typedef union {
uint64_t u64[1];
int64_t s64[1];
uint32_t u32[2];
int32_t s32[2];
uint16_t u16[4];
int16_t s16[4];
uint8_t u8[8];
int8_t s8[8];
unsigned char uc[8];
signed char sc[8];
char c[8];
double d[1]; /* Assumes IEEE 754 Binary64 - verify at run time */
float f[2]; /* Assumes IEEE 754 Binary32 - verify at run time */
} word64;
typedef union {
uint32_t u32[1];
int32_t s32[1];
uint16_t u16[2];
int16_t s16[2];
uint8_t u8[4];
int8_t s8[4];
unsigned char uc[4];
signed char sc[4];
char c[4];
float f[1]; /* Assumes IEEE 754 Binary32 - verify at run time */
} word32;
static inline word64 byteorder64(word64 w, int_fast8_t order)
{
if (order & 1)
w.u64[0] = ((w.u64[0] >> 8) & UINT64_C(0x00FF00FF00FF00FF))
| ((w.u64[0] & UINT64_C(0x00FF00FF00FF00FF)) << 8);
if (order & 2)
w.u64[0] = ((w.u64[0] >> 16) & UINT64_C(0x0000FFFF0000FFFF))
| ((w.u64[0] & UINT64_C(0x0000FFFF0000FFFF)) << 16);
if (order & 4)
w.u64[0] = ((w.u64[0] >> 32) & UINT64_C(0x00000000FFFFFFFF))
| ((w.u64[0] & UINT64_C(0x00000000FFFFFFFF)) << 16);
return w;
}
static inline word32 byteorder32(word32 w, int_fast8_t order)
{
if (order & 1)
w.u32[0] = ((w.u32[0] >> 8) & UINT32_C(0x00FF00FF))
| ((w.u32[0] & UINT32_C(0x00FF00FF)) << 8);
if (order & 2)
w.u32[0] = ((w.u32[0] >> 16) & UINT32_C(0x0000FFFF))
| ((w.u32[0] & UINT32_C(0x0000FFFF)) << 16);
return w;
}
Note that the
inline above is irrelevant to the C compiler;
static alone suffices. I just use the
static inline as an indicator for us humans that these are
accessor or
helper functions, typically defined in the header file, and not just ordinary "internal" functions (that I declare
static only).
The basic idea here is that type punning via an union is described in a footnote in all C ISO standards as a way to reinterpret the storage of a variable, so we use that to reinterpret the storage representation of the known fixed-size integer types, as well as the two floating-point types that usually match
IEEE 754 binary32 (single precision,
float) and binary64 (double precision,
double) types.
For 32 bit values, there are only four possible byte orders: native (0), swapped (3), or the two intermediate ones (1 and 2) that were only used in certain old systems. If you check
((word32){ .uc = { 0x01, 0x02, 0x03, 0x04 }}).u32[0], it will have value
0x04030201 on little-endian architectures, and
0x01020304 on big-endian architectures. The two other possibilities are
0x03040102 and
0x02010403, but they are nowadays extremely, extremely rare.
For 64 bit values, there are eight possible byte orders, with native (0) and swapped (7) being the most common ones.
If you check
((word64){ .uc = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }}).u64[0], it will have value
UINT64_C(0x0807060504030201) on little-endian architectures, and
UINT64_C(0x0102030405060708) on big-endian architectures. The others are exceedingly rare.
The reason for using the
UINTN_C(value) macros is that since the value might exceed the range
int can represent (see my previous post above),
<stdint.h> provides macros that adds a suffix (
U,
L,
UL,
LL, or
ULL) if needed to tell the compiler the base type of the constant. They can be used even in preprocessor
#if statements.
If you have a peer (or client or server) you are communicating with, and it uses its native byte order but the types used in the above
word32 and
word64 unions, all you need to do is to agree with a
prototype numeric value –– I recommend one double, one float, one 64-bit signed and negative integer, and one 32-bit signed or unsigned integer, for completeness; these take a total of 24 bytes ––, and then do a byte order discovery loop. For example:
int_fast8_t find_byteorder64(word64 w, word64 expected)
{
for (int_fast8_t order = 0; order < 8; order++)
if ((byteorder64(w, order)).u64[0] == expected.u64[0])
return order;
return -1;
}
int_fast8_t find_byteorder32(word32 w, word32 expected)
{
for (int_fast8_t order = 0; order < 4; order++)
if ((byteorder32(w, order)).u32[0] == expected.u32[0])
return order;
return -1;
}
These will return the
order needed for
byteorderN() to convert the byte order to current native byte order, or -1 if no byte order conversion of the given word
w matches the expected word
expected.
You can use a heuristic check to verify that the target architecture has the same byte order for floating-point and integer types, and that the floating-point types match the assumption above, via for example
if (((word32){ .f[0] = 0.0498918667435646f }).u32[0] != UINT32_C(0x3D4C5B6A))
fprintf(stderr, "Warning: Invalid 32-bit byte order, or 'float' not IEEE 754-2008 Binary32.\n");
if (((word64){ .d[0] = -2.125982314494425 }).u64[0] != UINT64_C(0xc001020304050607))
fprintf(stderr, "Warning: Invalid 64-bit byte order, or 'double' not IEEE 754-2008 Binary64.\n");
Most C compilers, when optimizing (
-Og or
-O2) this code, generate no machine code because they can determine at compile time that the code cannot ever issue a warning, given the target properties. Of course, instead of printing a warning to standard error, you can make this an
assert() (
#include <assert.h>).